diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 36a25a3f..49068021 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1686,6 +1686,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
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.
+
+ 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.
+
The security token is missing.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs
index 3f7cab62..c74166b8 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs
@@ -17,7 +17,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
{
public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create([
/*
- * Token response extraction:
+ * Device authorization response extraction:
*/
MapNonStandardResponseParameters.Descriptor
]);
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
index 8efe5f8f..19844e26 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
+++ b/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);
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
index 5c85d8fe..43d4a578 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
+++ b/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;
}
}
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
index fd7cb4cd..d4fc43c1 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
+++ b/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.
-->
-
+
@@ -967,23 +963,10 @@
Note: Lark serves global users, but it is known as Feishu in China, which has a separate issuer and domain.
-->
-
-
-
+
+
@@ -2150,6 +2133,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+