diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs index d57126bc..5da7b494 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs @@ -143,6 +143,27 @@ public static partial class OpenIddictClientWebIntegrationHandlers var request = context.Transaction.GetHttpRequestMessage() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); + // Note: Bitly only supports using "client_secret_post" for the authorization code grant but not for + // the resource owner password credentials grant, that requires using "client_secret_basic" instead. + if (context.Registration.ProviderType is ProviderTypes.Bitly && + context.GrantType is GrantTypes.Password && + !string.IsNullOrEmpty(context.Request.ClientId) && + !string.IsNullOrEmpty(context.Request.ClientSecret)) + { + // Important: the credentials MUST be formURL-encoded before being base64-encoded. + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() + .Append(EscapeDataString(context.Request.ClientId)) + .Append(':') + .Append(EscapeDataString(context.Request.ClientSecret)) + .ToString())); + + // Attach the authorization header containing the client credentials to the HTTP request. + request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials); + + // Remove the client credentials from the request payload to ensure they are not sent twice. + context.Request.ClientId = context.Request.ClientSecret = null; + } + // These providers require using basic authentication to flow the client_id // for all types of client applications, even when there's no client_secret. if (context.Registration.ProviderType is ProviderTypes.Reddit && diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 578a6e04..0198afe2 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -209,19 +209,58 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Errors that are not handled here will be automatically handled // by the standard handler present in the core OpenIddict client. - if (context.Registration.ProviderType is ProviderTypes.Deezer) + if (context.Registration.ProviderType is ProviderTypes.Bitly && + (bool?) context.Request["invalid"] is true) + { + // Note: Bitly uses custom "invalid" and "reason" parameters instead of + // the standard "error" parameter defined by the OAuth 2.0 specification. + var error = (string?) context.Request["reason"]; + + context.Reject( + error: error switch + { + "deny" => Errors.AccessDenied, + _ => Errors.ServerError + }, + description: error switch + { + "deny" => SR.GetResourceString(SR.ID2149), + _ => SR.GetResourceString(SR.ID2152) + }, + uri: error switch + { + "deny" => SR.FormatID8000(SR.ID2149), + _ => SR.FormatID8000(SR.ID2152) + }); + + return default; + } + + else if (context.Registration.ProviderType is ProviderTypes.Deezer) { // Note: Deezer uses a custom "error_reason" parameter instead of the // standard "error" parameter defined by the OAuth 2.0 specification. // // See https://developers.deezer.com/api/oauth for more information. var error = (string?) context.Request["error_reason"]; - if (string.Equals(error, "user_denied", StringComparison.Ordinal)) + if (!string.IsNullOrEmpty(error)) { context.Reject( - error: Errors.AccessDenied, - description: SR.GetResourceString(SR.ID2149), - uri: SR.FormatID8000(SR.ID2149)); + error: error switch + { + "user_denied" => Errors.AccessDenied, + _ => Errors.ServerError + }, + description: error switch + { + "user_denied" => SR.GetResourceString(SR.ID2149), + _ => SR.GetResourceString(SR.ID2152) + }, + uri: error switch + { + "user_denied" => SR.FormatID8000(SR.ID2149), + _ => SR.FormatID8000(SR.ID2152) + }); return default; } @@ -1091,6 +1130,13 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Basecamp returns the email address as a custom "email_address" node: ProviderTypes.Basecamp => (string?) context.UserinfoResponse?["email_address"], + // Bitly returns one or more email addresses as a custom "emails" node: + ProviderTypes.Bitly => context.UserinfoResponse?["emails"] + ?.GetUnnamedParameters() + ?.Where(parameter => (bool?) parameter["is_primary"] is true) + ?.Select(parameter => (string?) parameter["email"]) + ?.FirstOrDefault(), + // HubSpot returns the email address as a custom "user" node: ProviderTypes.HubSpot => (string?) context.UserinfoResponse?["user"], @@ -1208,6 +1254,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Bitbucket returns the user identifier as a custom "uuid" node: ProviderTypes.Bitbucket => (string?) context.UserinfoResponse?["uuid"], + // Bitly returns the user identifier as a custom "login" node: + ProviderTypes.Bitly => (string?) context.UserinfoResponse?["login"], + // DeviantArt returns the user identifier as a custom "userid" node: ProviderTypes.DeviantArt => (string?) context.UserinfoResponse?["userid"], diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 1ce283c1..8c481730 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -244,6 +244,26 @@ + + + + + + + + + + +