Browse Source

Add Zoho to the list of supported providers

pull/2131/head
Kévin Chalet 2 years ago
parent
commit
91a1fe7378
  1. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 2
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs
  3. 4
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
  4. 129
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  5. 79
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1686,6 +1686,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0450" xml:space="preserve">
<value>An HTTP/HTTPS redirect_uri or post_logout_redirect_uri cannot be used when using AS web authentication sessions. Make sure you're using a custom protocol scheme for all the callback URIs attached to the client registration.</value>
</data>
<data name="ID0451" xml:space="preserve">
<value>The Zoho integration requires sending the region of the server when using the client credentials or refresh token grants. For that, attach a ".location" authentication property containing the region to use.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

2
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs

@ -17,7 +17,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create([
/*
* Token response extraction:
* Device authorization response extraction:
*/
MapNonStandardResponseParameters.Descriptor
]);

4
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs

@ -192,8 +192,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// are manually added to the list of supported code challenge methods by this handler.
if (context.Registration.ProviderType is
ProviderTypes.Adobe or ProviderTypes.Autodesk or
ProviderTypes.FaceIt or ProviderTypes.Microsoft)
ProviderTypes.Adobe or ProviderTypes.Autodesk or
ProviderTypes.FaceIt or ProviderTypes.Microsoft or ProviderTypes.Zoho)
{
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain);
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256);

129
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -389,6 +389,37 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
// Zoho returns the region of the authenticated user as a non-standard "location" parameter
// that must be used to compute the address of the token and userinfo endpoints.
else if (context.Registration.ProviderType is ProviderTypes.Zoho)
{
var location = (string?) context.Request["location"];
if (string.IsNullOrEmpty(location))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2029("location"),
uri: SR.FormatID8000(SR.ID2029));
return default;
}
// Ensure the specified location corresponds to well-known region.
if (location.ToUpperInvariant() is not ( "AU" or "CA" or "EU" or "IN" or "JP" or "SA" or "US"))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052("location"),
uri: SR.FormatID8000(SR.ID2052));
return default;
}
// Store the validated location as an authentication property
// so it can be resolved later to determine the user region.
context.Properties[Zoho.Properties.Location] = location;
}
return default;
}
}
@ -435,6 +466,37 @@ public static partial class OpenIddictClientWebIntegrationHandlers
ProviderTypes.Trovo when context.GrantType is GrantTypes.RefreshToken
=> new Uri("https://open-api.trovo.live/openplatform/refreshtoken", UriKind.Absolute),
// Zoho requires using a region-specific token endpoint determined using
// the "location" parameter returned from the authorization endpoint.
//
// For more information, see
// https://www.zoho.com/accounts/protocol/oauth/multi-dc/client-authorization.html.
ProviderTypes.Zoho when context.GrantType is GrantTypes.AuthorizationCode
=> ((string?) context.Request?["location"])?.ToUpperInvariant() switch
{
"AU" => new Uri("https://accounts.zoho.com.au/oauth/v2/token", UriKind.Absolute),
"CA" => new Uri("https://accounts.zohocloud.ca/oauth/v2/token", UriKind.Absolute),
"EU" => new Uri("https://accounts.zoho.eu/oauth/v2/token", UriKind.Absolute),
"IN" => new Uri("https://accounts.zoho.in/oauth/v2/token", UriKind.Absolute),
"JP" => new Uri("https://accounts.zoho.jp/oauth/v2/token", UriKind.Absolute),
"SA" => new Uri("https://accounts.zoho.sa/oauth/v2/token", UriKind.Absolute),
_ => new Uri("https://accounts.zoho.com/oauth/v2/token", UriKind.Absolute)
},
ProviderTypes.Zoho when context.GrantType is GrantTypes.RefreshToken
=> !context.Properties.TryGetValue(Zoho.Properties.Location, out string? location) ||
string.IsNullOrEmpty(location) ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0451)) :
location?.ToUpperInvariant() switch
{
"AU" => new Uri("https://accounts.zoho.com.au/oauth/v2/token", UriKind.Absolute),
"CA" => new Uri("https://accounts.zohocloud.ca/oauth/v2/token", UriKind.Absolute),
"EU" => new Uri("https://accounts.zoho.eu/oauth/v2/token", UriKind.Absolute),
"IN" => new Uri("https://accounts.zoho.in/oauth/v2/token", UriKind.Absolute),
"JP" => new Uri("https://accounts.zoho.jp/oauth/v2/token", UriKind.Absolute),
"SA" => new Uri("https://accounts.zoho.sa/oauth/v2/token", UriKind.Absolute),
_ => new Uri("https://accounts.zoho.com/oauth/v2/token", UriKind.Absolute)
},
_ => context.TokenEndpoint
};
@ -779,6 +841,37 @@ public static partial class OpenIddictClientWebIntegrationHandlers
Uri.TryCreate(principal.GetClaim("http://schemes.superoffice.net/identity/webapi_url"), UriKind.Absolute, out Uri? uri)
=> OpenIddictHelpers.CreateAbsoluteUri(uri, new Uri("v1/user/currentPrincipal", UriKind.Relative)),
// Zoho requires using a region-specific userinfo endpoint determined using
// the "location" parameter returned from the authorization endpoint.
//
// For more information, see
// https://www.zoho.com/accounts/protocol/oauth/multi-dc/client-authorization.html.
ProviderTypes.Zoho when context.GrantType is GrantTypes.AuthorizationCode
=> ((string?) context.Request?["location"])?.ToUpperInvariant() switch
{
"AU" => new Uri("https://accounts.zoho.com.au/oauth/user/info", UriKind.Absolute),
"CA" => new Uri("https://accounts.zohocloud.ca/oauth/user/info", UriKind.Absolute),
"EU" => new Uri("https://accounts.zoho.eu/oauth/user/info", UriKind.Absolute),
"IN" => new Uri("https://accounts.zoho.in/oauth/user/info", UriKind.Absolute),
"JP" => new Uri("https://accounts.zoho.jp/oauth/user/info", UriKind.Absolute),
"SA" => new Uri("https://accounts.zoho.sa/oauth/user/info", UriKind.Absolute),
_ => new Uri("https://accounts.zoho.com/oauth/user/info", UriKind.Absolute)
},
ProviderTypes.Zoho when context.GrantType is GrantTypes.RefreshToken
=> !context.Properties.TryGetValue(Zoho.Properties.Location, out string? location) ||
string.IsNullOrEmpty(location) ? throw new InvalidOperationException(SR.GetResourceString(SR.ID0451)) :
location?.ToUpperInvariant() switch
{
"AU" => new Uri("https://accounts.zoho.com.au/oauth/user/info", UriKind.Absolute),
"CA" => new Uri("https://accounts.zohocloud.ca/oauth/user/info", UriKind.Absolute),
"EU" => new Uri("https://accounts.zoho.eu/oauth/user/info", UriKind.Absolute),
"IN" => new Uri("https://accounts.zoho.in/oauth/user/info", UriKind.Absolute),
"JP" => new Uri("https://accounts.zoho.jp/oauth/user/info", UriKind.Absolute),
"SA" => new Uri("https://accounts.zoho.sa/oauth/user/info", UriKind.Absolute),
_ => new Uri("https://accounts.zoho.com/oauth/user/info", UriKind.Absolute)
},
_ => context.UserinfoEndpoint
};
@ -1195,8 +1288,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Patreon returns the email address as a custom "attributes/email" node:
ProviderTypes.Patreon => (string?) context.UserinfoResponse?["attributes"]?["email"],
// ServiceChannel returns the email address as a custom "Email" node:
ProviderTypes.ServiceChannel => (string?) context.UserinfoResponse?["Email"],
// ServiceChannel and Zoho return the email address as a custom "Email" node:
ProviderTypes.ServiceChannel or ProviderTypes.Zoho => (string?) context.UserinfoResponse?["Email"],
// Shopify returns the email address as a custom "associated_user/email" node in token responses:
ProviderTypes.Shopify => (string?) context.TokenResponse?["associated_user"]?["email"],
@ -1279,6 +1372,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Typeform returns the username as a custom "alias" node:
ProviderTypes.Typeform => (string?) context.UserinfoResponse?["alias"],
// Zoho returns the username as a custom "Display_Name" node:
ProviderTypes.Zoho => (string?) context.UserinfoResponse?["Display_Name"],
_ => context.MergedPrincipal.GetClaim(ClaimTypes.Name)
});
@ -1364,6 +1460,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// WordPress returns the user identifier as a custom "ID" node:
ProviderTypes.WordPress => (string?) context.UserinfoResponse?["ID"],
// WordPress returns the user identifier as a custom "ZUID" node:
ProviderTypes.Zoho => (string?) context.UserinfoResponse?["ZUID"],
_ => context.MergedPrincipal.GetClaim(ClaimTypes.NameIdentifier)
});
@ -1460,6 +1559,22 @@ public static partial class OpenIddictClientWebIntegrationHandlers
new Uri("https://connect.stripe.com/express/oauth/authorize", UriKind.Absolute) :
new Uri("https://connect.stripe.com/oauth/authorize", UriKind.Absolute),
// Zoho requires using a region-specific authorization endpoint.
//
// For more information, see
// https://www.zoho.com/accounts/protocol/oauth/multi-dc/client-authorization.html.
ProviderTypes.Zoho when context.Properties.TryGetValue(Zoho.Properties.Location, out string? location)
=> location?.ToUpperInvariant() switch
{
"AU" => new Uri("https://accounts.zoho.com.au/oauth/v2/auth", UriKind.Absolute),
"CA" => new Uri("https://accounts.zohocloud.ca/oauth/v2/auth", UriKind.Absolute),
"EU" => new Uri("https://accounts.zoho.eu/oauth/v2/auth", UriKind.Absolute),
"IN" => new Uri("https://accounts.zoho.in/oauth/v2/auth", UriKind.Absolute),
"JP" => new Uri("https://accounts.zoho.jp/oauth/v2/auth", UriKind.Absolute),
"SA" => new Uri("https://accounts.zoho.sa/oauth/v2/auth", UriKind.Absolute),
_ => new Uri("https://accounts.zoho.com/oauth/v2/auth", UriKind.Absolute)
},
_ => context.AuthorizationEndpoint
};
@ -1735,6 +1850,16 @@ public static partial class OpenIddictClientWebIntegrationHandlers
context.Request["language"] = settings.Language;
}
// By default, Zoho doesn't return a refresh token but
// allows sending an "access_type" parameter to retrieve one.
else if (context.Registration.ProviderType is ProviderTypes.Zoho)
{
var settings = context.Registration.GetZohoSettings();
context.Request["access_type"] = settings.AccessType;
context.Request.Prompt = settings.Prompt;
}
return default;
}
}

79
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

@ -269,11 +269,7 @@
Note: most Battle.net regions use the same issuer URI but a different domain is required for China.
-->
<Environment Issuer="https://oauth.{settings.Region switch {
string region when string.Equals(region, 'CN', StringComparison.OrdinalIgnoreCase)
=> 'battlenet.com.cn',
_ => 'battle.net' }}/" />
<Environment Issuer="https://oauth.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'battlenet.com.cn' : 'battle.net')}/" />
<Setting PropertyName="Region" ParameterName="region" Type="String" Required="false" DefaultValue="US"
Description="The preferred Battle.net region (by default, 'US')" />
@ -967,23 +963,10 @@
Note: Lark serves global users, but it is known as Feishu in China, which has a separate issuer and domain.
-->
<Environment Issuer="https://passport.{settings.Region switch {
string region when string.Equals(region, 'CN', StringComparison.OrdinalIgnoreCase)
=> 'feishu.cn',
_ => 'larksuite.com' }}/">
<Configuration AuthorizationEndpoint="https://passport.{settings.Region switch {
string region when string.Equals(region, 'CN', StringComparison.OrdinalIgnoreCase)
=> 'feishu.cn',
_ => 'larksuite.com' }}/suite/passport/oauth/authorize"
TokenEndpoint="https://passport.{settings.Region switch {
string region when string.Equals(region, 'CN', StringComparison.OrdinalIgnoreCase)
=> 'feishu.cn',
_ => 'larksuite.com' }}/suite/passport/oauth/token"
UserinfoEndpoint="https://passport.{settings.Region switch {
string region when string.Equals(region, 'CN', StringComparison.OrdinalIgnoreCase)
=> 'feishu.cn',
_ => 'larksuite.com' }}/suite/passport/oauth/userinfo">
<Environment Issuer="https://passport.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'feishu.cn' : 'larksuite.com')}/">
<Configuration AuthorizationEndpoint="https://passport.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'feishu.cn' : 'larksuite.com')}/suite/passport/oauth/authorize"
TokenEndpoint="https://passport.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'feishu.cn' : 'larksuite.com')}/suite/passport/oauth/token"
UserinfoEndpoint="https://passport.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'feishu.cn' : 'larksuite.com')}/suite/passport/oauth/userinfo">
<GrantType Value="authorization_code" />
<GrantType Value="refresh_token" />
</Configuration>
@ -2150,6 +2133,58 @@
<Environment Issuer="https://api.login.yahoo.com/" />
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ▄▄▄ ██ ▄▄▄ ██ ██ ██ ▄▄▄ ██
██▀▀▀▄▄██ ███ ██ ▄▄ ██ ███ ██
██ ▀▀▀ ██ ▀▀▀ ██ ██ ██ ▀▀▀ ██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Zoho" Id="269dc1c7-388e-4f65-9c29-c7791914532b"
Documentation="https://www.zoho.com/accounts/protocol/oauth.html">
<Environment Issuer="https://accounts.zoho.com/">
<Configuration AuthorizationEndpoint="{settings.Region?.ToUpperInvariant() switch {
'AU' => 'https://accounts.zoho.com.au/oauth/v2/auth',
'CA' => 'https://accounts.zohocloud.ca/oauth/v2/auth',
'EU' => 'https://accounts.zoho.eu/oauth/v2/auth',
'IN' => 'https://accounts.zoho.in/oauth/v2/auth',
'JP' => 'https://accounts.zoho.jp/oauth/v2/auth',
'SA' => 'https://accounts.zoho.sa/oauth/v2/auth',
_ => 'https://accounts.zoho.com/oauth/v2/auth' }}"
TokenEndpoint="{settings.Region?.ToUpperInvariant() switch {
'AU' => 'https://accounts.zoho.com.au/oauth/v2/token',
'CA' => 'https://accounts.zohocloud.ca/oauth/v2/token',
'EU' => 'https://accounts.zoho.eu/oauth/v2/token',
'IN' => 'https://accounts.zoho.in/oauth/v2/token',
'JP' => 'https://accounts.zoho.jp/oauth/v2/token',
'SA' => 'https://accounts.zoho.sa/oauth/v2/token',
_ => 'https://accounts.zoho.com/oauth/v2/token' }}">
<CodeChallengeMethod Value="S256" />
<GrantType Value="authorization_code" />
<GrantType Value="refresh_token" />
</Configuration>
<!--
Note: Zoho requires sending the "AaaServer.profile.READ" scope to be able to use the userinfo endpoint.
-->
<Scope Name="AaaServer.profile.READ" Default="true" Required="true" />
</Environment>
<Property Name="Location" DictionaryKey=".location" />
<Setting PropertyName="AccessType" ParameterName="type" Type="String" Required="false"
Description="The value used as the 'access_type' parameter (can be set to 'offline' to retrieve a refresh token)" />
<Setting PropertyName="Prompt" ParameterName="prompt" Type="String" Required="false"
Description="The value used as the 'prompt' parameter (can be set to 'consent' to display the consent form for each authorization demand)" />
<Setting PropertyName="Region" ParameterName="region" Type="String" Required="false" DefaultValue="US"
Description="The preferred Zoho region (by default, 'US')" />
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ▄▄▄ ██ ▄▄▄ ██ ▄▄▄ ██ ▄▀▄ ██

Loading…
Cancel
Save