From ba948120044147de112884f1676f45cb0f36df3b Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:52:18 +0300 Subject: [PATCH 01/11] Implement Zoho OAuth provider --- AspNet.Security.OAuth.Providers.sln | 8 ++ README.md | 1 + docs/zoho.md | 23 ++++++ .../AspNet.Security.OAuth.Zoho.csproj | 24 ++++++ .../ZohoAuthenticationDefaults.cs | 48 ++++++++++++ .../ZohoAuthenticationExtensions.cs | 77 +++++++++++++++++++ .../ZohoAuthenticationHandler.cs | 72 +++++++++++++++++ .../ZohoAuthenticationOptions.cs | 34 ++++++++ .../ZohoAuthenticationPostConfigureOptions.cs | 60 +++++++++++++++ .../ZohoAuthenticationRegion.cs | 21 +++++ ...AuthenticationPostConfigureOptionsTests.cs | 63 +++++++++++++++ .../Zoho/ZohoTests.cs | 24 ++++++ .../Zoho/bundle.json | 26 +++++++ 13 files changed, 481 insertions(+) create mode 100644 docs/zoho.md create mode 100644 src/AspNet.Security.OAuth.Zoho/AspNet.Security.OAuth.Zoho.csproj create mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs create mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs create mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs create mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs create mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs create mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index 2d794948e..848748c70 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -220,6 +220,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C2CA4B38-A docs\xumm.md = docs\xumm.md docs\zendesk.md = docs\zendesk.md docs\docusign.md = docs\docusign.md + docs\zoho.md = docs\zoho.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Basecamp", "src\AspNet.Security.OAuth.Basecamp\AspNet.Security.OAuth.Basecamp.csproj", "{42306484-B2BF-4B52-B950-E0CDFA58B02A}" @@ -309,6 +310,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Piped EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Docusign", "src\AspNet.Security.OAuth.Docusign\AspNet.Security.OAuth.Docusign.csproj", "{4E96BD06-04CD-4014-BA42-10D2CDB820D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Zoho", "src\AspNet.Security.OAuth.Zoho\AspNet.Security.OAuth.Zoho.csproj", "{CD56ABE4-1CD2-4029-B556-E110A31A2CC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -715,6 +718,10 @@ Global {4E96BD06-04CD-4014-BA42-10D2CDB820D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E96BD06-04CD-4014-BA42-10D2CDB820D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E96BD06-04CD-4014-BA42-10D2CDB820D6}.Release|Any CPU.Build.0 = Release|Any CPU + {CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD56ABE4-1CD2-4029-B556-E110A31A2CC4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -826,6 +833,7 @@ Global {83C37AC5-51FB-47CD-8CBE-77AA114FF6F3} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {55975423-C9C0-4C47-AD00-0F012F30AD3C} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {4E96BD06-04CD-4014-BA42-10D2CDB820D6} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + {CD56ABE4-1CD2-4029-B556-E110A31A2CC4} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C} diff --git a/README.md b/README.md index d6e35ffe4..75d07ffec 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,7 @@ If a provider you're looking for does not exist, consider making a PR to add one | Yandex | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Yandex?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Yandex/ "Download AspNet.Security.OAuth.Yandex from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Yandex?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Yandex "Download AspNet.Security.OAuth.Yandex from MyGet.org") | [Documentation](https://tech.yandex.com/oauth/ "Yandex developer documentation") | | Zalo | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Zalo?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zalo/ "Download AspNet.Security.OAuth.Zalo from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Zalo?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zalo "Download AspNet.Security.OAuth.Zalo from MyGet.org") | [Documentation](https://developers.zalo.me/docs/api/social-api-4 "Zalo developer documentation") | | Zendesk | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Zendesk?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zendesk/ "Download AspNet.Security.OAuth.Zendesk from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Zendesk?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zendesk "Download AspNet.Security.OAuth.Zendesk from MyGet.org") | [Documentation](https://support.zendesk.com/hc/en-us/articles/203663836#topic_ar1_mfs_qk "Zendesk developer documentation") | +| Zoho | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Zoho?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zoho/ "Download AspNet.Security.OAuth.Zoho from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Zoho?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zoho "Download AspNet.Security.OAuth.Zoho from MyGet.org") | [Documentation](https://www.zoho.com/accounts/protocol/oauth.html "Zoho developer documentation") | | Zoom | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Zoom?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Zoom/ "Download AspNet.Security.OAuth.Zoom from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Zoom?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Zoom "Download AspNet.Security.OAuth.Zoom from MyGet.org") | [Documentation](https://developers.zoom.us/docs/integrations/ "Zoom developer documentation") | + + true + + + + ASP.NET Core security middleware enabling Zoho authentication. + Denys Goncharenko + aspnetcore;authentication;oauth;zoho;security + + + + + + + + diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs new file mode 100644 index 000000000..3f28c9e24 --- /dev/null +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Zoho; + +/// +/// Default values used by the Zoho authentication middleware. +/// +public static class ZohoAuthenticationDefaults +{ + /// + /// Default value for . + /// + public const string AuthenticationScheme = "Zoho"; + + /// + /// Default value for . + /// + public static readonly string DisplayName = "Zoho"; + + /// + /// Default value for . + /// + public static readonly string Issuer = "Zoho"; + + /// + /// Default value for . + /// + public static readonly string CallbackPath = "/signin-zoho"; + + /// + /// Default value for . + /// + public static readonly string AuthorizationPath = "/oauth/v2/auth"; + + /// + /// Default value for . + /// + public static readonly string TokenPath = "/oauth/v2/token"; + + /// + /// Default value for . + /// + public static readonly string UserInformationPath = "/oauth/user/info"; +} diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs new file mode 100644 index 000000000..d2e5558e0 --- /dev/null +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.Zoho; + +/// +/// Extension methods to add Zoho authentication capabilities to an HTTP application pipeline. +/// +public static class ZohoAuthenticationExtensions +{ + /// + /// Adds to the specified + /// , which enables Zoho authentication capabilities. + /// + /// The authentication builder. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddZoho([NotNull] this AuthenticationBuilder builder) + { + return builder.AddZoho(ZohoAuthenticationDefaults.AuthenticationScheme, options => { }); + } + + /// + /// Adds to the specified + /// , which enables Zoho authentication capabilities. + /// + /// The authentication builder. + /// The delegate used to configure the OpenID 2.0 options. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddZoho( + [NotNull] this AuthenticationBuilder builder, + [NotNull] Action configuration) + { + return builder.AddZoho(ZohoAuthenticationDefaults.AuthenticationScheme, configuration); + } + + /// + /// Adds to the specified + /// , which enables Zoho authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The delegate used to configure the Zoho options. + /// The . + public static AuthenticationBuilder AddZoho( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] Action configuration) + { + return builder.AddZoho(scheme, ZohoAuthenticationDefaults.DisplayName, configuration); + } + + /// + /// Adds to the specified + /// , which enables Zoho authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The optional display name associated with this instance. + /// The delegate used to configure the Zoho options. + /// The . + public static AuthenticationBuilder AddZoho( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [CanBeNull] string caption, + [NotNull] Action configuration) + { + builder.Services.TryAddSingleton, ZohoAuthenticationPostConfigureOptions>(); + return builder.AddOAuth(scheme, caption, configuration); + } +} diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs new file mode 100644 index 000000000..684824d00 --- /dev/null +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.Zoho; + +public partial class ZohoAuthenticationHandler : OAuthHandler +{ + public ZohoAuthenticationHandler( + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override async Task CreateTicketAsync( + [NotNull] ClaimsIdentity identity, + [NotNull] AuthenticationProperties properties, + [NotNull] OAuthTokenResponse tokens) + { + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + requestMessage.Version = Backchannel.DefaultRequestVersion; + + using var response = await Backchannel.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + await Log.UserProfileErrorAsync(Logger, response, Context.RequestAborted); + throw new HttpRequestException("An error occurred while retrieving the user profile."); + } + + using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted)); + + var principal = new ClaimsPrincipal(identity); + var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement); + context.RunClaimActions(); + + await Events.CreatingTicket(context); + return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); + } + + private static partial class Log + { + internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) + { + UserProfileError( + logger, + response.StatusCode, + response.Headers.ToString(), + await response.Content.ReadAsStringAsync(cancellationToken)); + } + + [LoggerMessage(1, LogLevel.Error, "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] + private static partial void UserProfileError( + ILogger logger, + System.Net.HttpStatusCode status, + string headers, + string body); + } +} diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs new file mode 100644 index 000000000..08cc34b27 --- /dev/null +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using System.Security.Claims; + +namespace AspNet.Security.OAuth.Zoho; + +/// +/// Defines a set of options used by . +/// +public class ZohoAuthenticationOptions : OAuthOptions +{ + /// + /// Gets or sets a value that determines whether development or production endpoints are used. + /// The default value of this property is . + /// + public ZohoAuthenticationRegion Region { get; set; } + + public ZohoAuthenticationOptions() + { + ClaimsIssuer = ZohoAuthenticationDefaults.Issuer; + CallbackPath = ZohoAuthenticationDefaults.CallbackPath; + Region = ZohoAuthenticationRegion.Global; + + Scope.Add("AaaServer.profile.READ"); + + ClaimActions.MapCustomJson(ClaimTypes.NameIdentifier, user => user.GetString("ZUID")); + ClaimActions.MapCustomJson(ClaimTypes.Name, user => user.GetString("Display_Name")); + ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.GetString("Email")); + } +} diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs new file mode 100644 index 000000000..416b367fb --- /dev/null +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.Zoho; + +/// +/// Used to configure instances. +/// +public sealed class ZohoAuthenticationPostConfigureOptions : IPostConfigureOptions +{ + /// + public void PostConfigure( + string? name, + [NotNull] ZohoAuthenticationOptions options) + { + ConfigureEndpoints(options); + } + + private static void ConfigureEndpoints(ZohoAuthenticationOptions options) + { + var domain = GetDomain(options.Region); + + options.AuthorizationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.AuthorizationPath); + options.TokenEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.TokenPath); + options.UserInformationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.UserInformationPath); + } + + private static string CreateUrl(string domain, string path) + { + // Enforce use of HTTPS + var builder = new UriBuilder(domain) + { + Path = path, + Port = -1, + Scheme = Uri.UriSchemeHttps, + }; + + return builder.Uri.ToString(); + } + + private static string GetDomain(ZohoAuthenticationRegion region) + { + return region switch + { + ZohoAuthenticationRegion.Global => "accounts.zoho.com", + ZohoAuthenticationRegion.Europe => "accounts.zoho.eu", + ZohoAuthenticationRegion.India => "accounts.zoho.in", + ZohoAuthenticationRegion.Australia => "accounts.zoho.com.au", + ZohoAuthenticationRegion.Japan => "accounts.zoho.jp", + ZohoAuthenticationRegion.Canada => "accounts.zohocloud.ca", + ZohoAuthenticationRegion.SaudiArabia => "accounts.zoho.sa", + _ => throw new InvalidOperationException($"The {nameof(ZohoAuthenticationRegion)} is not supported."), + }; + } +} diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs new file mode 100644 index 000000000..6a29931d9 --- /dev/null +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs @@ -0,0 +1,21 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Zoho; + +/// +/// Used to determine which region to use. +/// +public enum ZohoAuthenticationRegion +{ + Global = 0, + Europe, + India, + Australia, + Japan, + Canada, + SaudiArabia +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs new file mode 100644 index 000000000..351a2284b --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs @@ -0,0 +1,63 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Zoho; + +public static class ZohoAuthenticationPostConfigureOptionsTests +{ + [Theory] + [InlineData(ZohoAuthenticationRegion.Global, "accounts.zoho.com")] + [InlineData(ZohoAuthenticationRegion.Europe, "accounts.zoho.eu")] + [InlineData(ZohoAuthenticationRegion.India, "accounts.zoho.in")] + [InlineData(ZohoAuthenticationRegion.Australia, "accounts.zoho.com.au")] + [InlineData(ZohoAuthenticationRegion.Japan, "accounts.zoho.jp")] + [InlineData(ZohoAuthenticationRegion.Canada, "accounts.zohocloud.ca")] + [InlineData(ZohoAuthenticationRegion.SaudiArabia, "accounts.zoho.sa")] + public static void PostConfigure_Configures_Valid_Authentication_Region(ZohoAuthenticationRegion region, string domain) + { + // Arrange + const string name = "Zoho"; + var target = new ZohoAuthenticationPostConfigureOptions(); + + var options = new ZohoAuthenticationOptions + { + Region = region + }; + + // Act + target.PostConfigure(name, options); + + // Assert + options.AuthorizationEndpoint.ShouldBeEquivalentTo( + $"https://{domain}{ZohoAuthenticationDefaults.AuthorizationPath}"); + Uri.TryCreate(options.AuthorizationEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); + + options.TokenEndpoint.ShouldBeEquivalentTo( + $"https://{domain}{ZohoAuthenticationDefaults.TokenPath}"); + Uri.TryCreate(options.TokenEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); + + options.UserInformationEndpoint.ShouldBeEquivalentTo( + $"https://{domain}{ZohoAuthenticationDefaults.UserInformationPath}"); + Uri.TryCreate(options.UserInformationEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); + } + + [Fact] + public static void PostConfigure_Invalid_Authentication_Region_ThrowsException() + { + // Arrange + const string name = "Zoho"; + var target = new ZohoAuthenticationPostConfigureOptions(); + + var options = new ZohoAuthenticationOptions + { + Region = (ZohoAuthenticationRegion)10 + }; + + // Act + Action act = () => target.PostConfigure(name, options); + act.ShouldThrow(); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs new file mode 100644 index 000000000..3e6b066b8 --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers + * for more information concerning the license and the contributors participating to this project. + */ + +namespace AspNet.Security.OAuth.Zoho; + +public class ZohoTests(ITestOutputHelper outputHelper) : OAuthTests(outputHelper) +{ + public override string DefaultScheme => ZohoAuthenticationDefaults.AuthenticationScheme; + + protected internal override void RegisterAuthentication(AuthenticationBuilder builder) + { + builder.AddZoho(options => ConfigureDefaults(builder, options)); + } + + [Theory] + [InlineData(ClaimTypes.NameIdentifier, "1234567890")] + [InlineData(ClaimTypes.Name, "User Name")] + [InlineData(ClaimTypes.Email, "testuser@example.com")] + public async Task Can_Sign_In_Using_Zoho(string claimType, string claimValue) + => await AuthenticateUserAndAssertClaimValue(claimType, claimValue); +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json new file mode 100644 index 000000000..7bf95559c --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", + "items": [ + { + "uri": "https://accounts.zoho.com/oauth/v2/token", + "method": "POST", + "contentFormat": "json", + "contentJson": { + "access_token": "secret-access-token", + "token_type": "Bearer", + "refresh_token": "secret-refresh-token", + "expires_in": 3600 + } + }, + { + "uri": "https://accounts.zoho.com/oauth/user/info", + "method": "GET", + "contentFormat": "json", + "contentJson": { + "ZUID": "1234567890", + "Email": "testuser@example.com", + "Display_Name": "User Name" + } + } + ] +} From 16541c41a71c54571e2e1750b9396fd2186f03aa Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:09:44 +0300 Subject: [PATCH 02/11] Fix orders for regions and change post-configuration behavior --- .../ZohoAuthenticationOptions.cs | 1 - .../ZohoAuthenticationPostConfigureOptions.cs | 18 +++++++++++++++--- ...oAuthenticationPostConfigureOptionsTests.cs | 6 +++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs index 08cc34b27..bfb1619d7 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs @@ -23,7 +23,6 @@ public ZohoAuthenticationOptions() { ClaimsIssuer = ZohoAuthenticationDefaults.Issuer; CallbackPath = ZohoAuthenticationDefaults.CallbackPath; - Region = ZohoAuthenticationRegion.Global; Scope.Add("AaaServer.profile.READ"); diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs index 416b367fb..e99659e02 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs @@ -23,6 +23,11 @@ public void PostConfigure( private static void ConfigureEndpoints(ZohoAuthenticationOptions options) { + if (AreEndpointsInitialized(options)) + { + return; + } + var domain = GetDomain(options.Region); options.AuthorizationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.AuthorizationPath); @@ -47,14 +52,21 @@ private static string GetDomain(ZohoAuthenticationRegion region) { return region switch { - ZohoAuthenticationRegion.Global => "accounts.zoho.com", + ZohoAuthenticationRegion.Australia => "accounts.zoho.com.au", + ZohoAuthenticationRegion.Canada => "accounts.zohocloud.ca", ZohoAuthenticationRegion.Europe => "accounts.zoho.eu", + ZohoAuthenticationRegion.Global => "accounts.zoho.com", ZohoAuthenticationRegion.India => "accounts.zoho.in", - ZohoAuthenticationRegion.Australia => "accounts.zoho.com.au", ZohoAuthenticationRegion.Japan => "accounts.zoho.jp", - ZohoAuthenticationRegion.Canada => "accounts.zohocloud.ca", ZohoAuthenticationRegion.SaudiArabia => "accounts.zoho.sa", _ => throw new InvalidOperationException($"The {nameof(ZohoAuthenticationRegion)} is not supported."), }; } + + private static bool AreEndpointsInitialized(ZohoAuthenticationOptions options) + { + return !string.IsNullOrEmpty(options.AuthorizationEndpoint) && + !string.IsNullOrEmpty(options.TokenEndpoint) && + !string.IsNullOrEmpty(options.UserInformationEndpoint); + } } diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs index 351a2284b..2960a14f7 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs @@ -9,12 +9,12 @@ namespace AspNet.Security.OAuth.Zoho; public static class ZohoAuthenticationPostConfigureOptionsTests { [Theory] - [InlineData(ZohoAuthenticationRegion.Global, "accounts.zoho.com")] + [InlineData(ZohoAuthenticationRegion.Australia, "accounts.zoho.com.au")] + [InlineData(ZohoAuthenticationRegion.Canada, "accounts.zohocloud.ca")] [InlineData(ZohoAuthenticationRegion.Europe, "accounts.zoho.eu")] + [InlineData(ZohoAuthenticationRegion.Global, "accounts.zoho.com")] [InlineData(ZohoAuthenticationRegion.India, "accounts.zoho.in")] - [InlineData(ZohoAuthenticationRegion.Australia, "accounts.zoho.com.au")] [InlineData(ZohoAuthenticationRegion.Japan, "accounts.zoho.jp")] - [InlineData(ZohoAuthenticationRegion.Canada, "accounts.zohocloud.ca")] [InlineData(ZohoAuthenticationRegion.SaudiArabia, "accounts.zoho.sa")] public static void PostConfigure_Configures_Valid_Authentication_Region(ZohoAuthenticationRegion region, string domain) { From eff828d328138acdc680e4a71095fb2a71a49188 Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:10:53 +0300 Subject: [PATCH 03/11] fix zoho documentation. --- docs/zoho.md | 1 - src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zoho.md b/docs/zoho.md index 3f6aa3bb0..73ec272c3 100644 --- a/docs/zoho.md +++ b/docs/zoho.md @@ -8,7 +8,6 @@ services.AddAuthentication(options => /* Auth configuration */) { options.ClientId = "my-client-id"; options.ClientSecret = "my-client-secret"; - options.Region = ZohoAuthenticationRegion.Global; }); ``` diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs index bfb1619d7..08cc34b27 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs @@ -23,6 +23,7 @@ public ZohoAuthenticationOptions() { ClaimsIssuer = ZohoAuthenticationDefaults.Issuer; CallbackPath = ZohoAuthenticationDefaults.CallbackPath; + Region = ZohoAuthenticationRegion.Global; Scope.Add("AaaServer.profile.READ"); From efa0da5ce8f613e66618b5324544674f3081fd0e Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:30:31 +0300 Subject: [PATCH 04/11] rework zoho region param to be optional --- docs/zoho.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/zoho.md b/docs/zoho.md index 73ec272c3..e341cfd78 100644 --- a/docs/zoho.md +++ b/docs/zoho.md @@ -13,10 +13,10 @@ services.AddAuthentication(options => /* Auth configuration */) ## Required Additional Settings -| Property Name | Property Type | Description | Default Value | -|:--------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------|:----------------------------------| -| `Region` | [`ZohoAuthenticationRegion`](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/blob/dev/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs "ZohoAuthenticationRegion enumeration") | The target online region for Zoho authentication. | `ZohoAuthenticationRegion.Global` | +_None._ ## Optional Settings -_None._ +| Property Name | Property Type | Description | Default Value | +|:--------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------|:----------------------------------| +| `Region` | [`ZohoAuthenticationRegion`](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/blob/dev/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs "ZohoAuthenticationRegion enumeration") | The target online region for Zoho authentication. | `ZohoAuthenticationRegion.Global` | From 5ae027ce0468958672aac4bc9d01c3567a77d4ab Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:53:26 +0300 Subject: [PATCH 05/11] update endpoints only when null or empty. --- .../ZohoAuthenticationPostConfigureOptions.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs index e99659e02..45340aa57 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs @@ -23,16 +23,22 @@ public void PostConfigure( private static void ConfigureEndpoints(ZohoAuthenticationOptions options) { - if (AreEndpointsInitialized(options)) + var domain = GetDomain(options.Region); + + if (string.IsNullOrEmpty(options.AuthorizationEndpoint)) { - return; + options.AuthorizationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.AuthorizationPath); } - var domain = GetDomain(options.Region); + if (string.IsNullOrEmpty(options.TokenEndpoint)) + { + options.TokenEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.TokenPath); + } - options.AuthorizationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.AuthorizationPath); - options.TokenEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.TokenPath); - options.UserInformationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.UserInformationPath); + if (string.IsNullOrEmpty(options.UserInformationEndpoint)) + { + options.UserInformationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.UserInformationPath); + } } private static string CreateUrl(string domain, string path) @@ -62,11 +68,4 @@ private static string GetDomain(ZohoAuthenticationRegion region) _ => throw new InvalidOperationException($"The {nameof(ZohoAuthenticationRegion)} is not supported."), }; } - - private static bool AreEndpointsInitialized(ZohoAuthenticationOptions options) - { - return !string.IsNullOrEmpty(options.AuthorizationEndpoint) && - !string.IsNullOrEmpty(options.TokenEndpoint) && - !string.IsNullOrEmpty(options.UserInformationEndpoint); - } } From f82dcd5b4636149f85da57459088538f0155851c Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:50:04 +0300 Subject: [PATCH 06/11] rework hard coded domains to dynamic --- AspNet.Security.OAuth.Providers.sln | 1 - docs/zoho.md | 22 ------ .../ZohoAuthenticationDefaults.cs | 12 +++- .../ZohoAuthenticationExtensions.cs | 3 - .../ZohoAuthenticationHandler.cs | 40 +++++++++++ .../ZohoAuthenticationOptions.cs | 9 +-- .../ZohoAuthenticationPostConfigureOptions.cs | 71 ------------------- .../ZohoAuthenticationRegion.cs | 21 ------ ...AuthenticationPostConfigureOptionsTests.cs | 63 ---------------- 9 files changed, 53 insertions(+), 189 deletions(-) delete mode 100644 docs/zoho.md delete mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs delete mode 100644 src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs delete mode 100644 test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index 848748c70..d81fac018 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -220,7 +220,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C2CA4B38-A docs\xumm.md = docs\xumm.md docs\zendesk.md = docs\zendesk.md docs\docusign.md = docs\docusign.md - docs\zoho.md = docs\zoho.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Basecamp", "src\AspNet.Security.OAuth.Basecamp\AspNet.Security.OAuth.Basecamp.csproj", "{42306484-B2BF-4B52-B950-E0CDFA58B02A}" diff --git a/docs/zoho.md b/docs/zoho.md deleted file mode 100644 index e341cfd78..000000000 --- a/docs/zoho.md +++ /dev/null @@ -1,22 +0,0 @@ -# Integrating the Zoho Provider - -## Example - -```csharp -services.AddAuthentication(options => /* Auth configuration */) - .AddZoho(options => - { - options.ClientId = "my-client-id"; - options.ClientSecret = "my-client-secret"; - }); -``` - -## Required Additional Settings - -_None._ - -## Optional Settings - -| Property Name | Property Type | Description | Default Value | -|:--------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------|:----------------------------------| -| `Region` | [`ZohoAuthenticationRegion`](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/blob/dev/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs "ZohoAuthenticationRegion enumeration") | The target online region for Zoho authentication. | `ZohoAuthenticationRegion.Global` | diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs index 3f28c9e24..4cdf34b0d 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs @@ -34,15 +34,25 @@ public static class ZohoAuthenticationDefaults /// /// Default value for . /// - public static readonly string AuthorizationPath = "/oauth/v2/auth"; + public static readonly string AuthorizeEndpoint = "https://accounts.zoho.com/oauth/v2/auth"; /// /// Default value for . /// public static readonly string TokenPath = "/oauth/v2/token"; + /// + /// Default value for . + /// + public static readonly string TokenEndpoint = "https://accounts.zoho.com/oauth/v2/token"; + /// /// Default value for . /// public static readonly string UserInformationPath = "/oauth/user/info"; + + /// + /// Default value for Zoho Server Info. + /// + public static readonly string ServerInfoEndpoint = "https://accounts.zoho.com/oauth/serverinfo"; } diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs index d2e5558e0..5b8574942 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationExtensions.cs @@ -5,8 +5,6 @@ */ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; namespace AspNet.Security.OAuth.Zoho; @@ -71,7 +69,6 @@ public static AuthenticationBuilder AddZoho( [CanBeNull] string caption, [NotNull] Action configuration) { - builder.Services.TryAddSingleton, ZohoAuthenticationPostConfigureOptions>(); return builder.AddOAuth(scheme, caption, configuration); } } diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs index 684824d00..cf8651441 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs @@ -51,6 +51,30 @@ protected override async Task CreateTicketAsync( return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); } + protected override async Task HandleRemoteAuthenticateAsync() + { + var location = Context.Request.Query["location"]; + + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, ZohoAuthenticationDefaults.ServerInfoEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + requestMessage.Version = Backchannel.DefaultRequestVersion; + + using var response = await Backchannel.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + await Log.ServerInfoErrorAsync(Logger, response, Context.RequestAborted); + throw new HttpRequestException("An error occurred while retrieving the server info."); + } + + using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted)); + + var domain = payload.RootElement.GetProperty("locations").GetProperty(location.ToString()); + Options.UserInformationEndpoint = $"{domain}{ZohoAuthenticationDefaults.UserInformationPath}"; + Options.TokenEndpoint = $"{domain}{ZohoAuthenticationDefaults.TokenPath}"; + + return await base.HandleRemoteAuthenticateAsync(); + } + private static partial class Log { internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) @@ -62,11 +86,27 @@ internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMes await response.Content.ReadAsStringAsync(cancellationToken)); } + internal static async Task ServerInfoErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) + { + ServerInfoErrorAsync( + logger, + response.StatusCode, + response.Headers.ToString(), + await response.Content.ReadAsStringAsync(cancellationToken)); + } + [LoggerMessage(1, LogLevel.Error, "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] private static partial void UserProfileError( ILogger logger, System.Net.HttpStatusCode status, string headers, string body); + + [LoggerMessage(2, LogLevel.Error, "An error occurred while retrieving the server info: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] + private static partial void ServerInfoErrorAsync( + ILogger logger, + System.Net.HttpStatusCode status, + string headers, + string body); } } diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs index 08cc34b27..1e533fec3 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs @@ -13,17 +13,12 @@ namespace AspNet.Security.OAuth.Zoho; /// public class ZohoAuthenticationOptions : OAuthOptions { - /// - /// Gets or sets a value that determines whether development or production endpoints are used. - /// The default value of this property is . - /// - public ZohoAuthenticationRegion Region { get; set; } - public ZohoAuthenticationOptions() { ClaimsIssuer = ZohoAuthenticationDefaults.Issuer; CallbackPath = ZohoAuthenticationDefaults.CallbackPath; - Region = ZohoAuthenticationRegion.Global; + AuthorizationEndpoint = ZohoAuthenticationDefaults.AuthorizeEndpoint; + TokenEndpoint = ZohoAuthenticationDefaults.TokenEndpoint; Scope.Add("AaaServer.profile.READ"); diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs deleted file mode 100644 index 45340aa57..000000000 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationPostConfigureOptions.cs +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers - * for more information concerning the license and the contributors participating to this project. - */ - -using Microsoft.Extensions.Options; - -namespace AspNet.Security.OAuth.Zoho; - -/// -/// Used to configure instances. -/// -public sealed class ZohoAuthenticationPostConfigureOptions : IPostConfigureOptions -{ - /// - public void PostConfigure( - string? name, - [NotNull] ZohoAuthenticationOptions options) - { - ConfigureEndpoints(options); - } - - private static void ConfigureEndpoints(ZohoAuthenticationOptions options) - { - var domain = GetDomain(options.Region); - - if (string.IsNullOrEmpty(options.AuthorizationEndpoint)) - { - options.AuthorizationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.AuthorizationPath); - } - - if (string.IsNullOrEmpty(options.TokenEndpoint)) - { - options.TokenEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.TokenPath); - } - - if (string.IsNullOrEmpty(options.UserInformationEndpoint)) - { - options.UserInformationEndpoint = CreateUrl(domain, ZohoAuthenticationDefaults.UserInformationPath); - } - } - - private static string CreateUrl(string domain, string path) - { - // Enforce use of HTTPS - var builder = new UriBuilder(domain) - { - Path = path, - Port = -1, - Scheme = Uri.UriSchemeHttps, - }; - - return builder.Uri.ToString(); - } - - private static string GetDomain(ZohoAuthenticationRegion region) - { - return region switch - { - ZohoAuthenticationRegion.Australia => "accounts.zoho.com.au", - ZohoAuthenticationRegion.Canada => "accounts.zohocloud.ca", - ZohoAuthenticationRegion.Europe => "accounts.zoho.eu", - ZohoAuthenticationRegion.Global => "accounts.zoho.com", - ZohoAuthenticationRegion.India => "accounts.zoho.in", - ZohoAuthenticationRegion.Japan => "accounts.zoho.jp", - ZohoAuthenticationRegion.SaudiArabia => "accounts.zoho.sa", - _ => throw new InvalidOperationException($"The {nameof(ZohoAuthenticationRegion)} is not supported."), - }; - } -} diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs deleted file mode 100644 index 6a29931d9..000000000 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationRegion.cs +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers - * for more information concerning the license and the contributors participating to this project. - */ - -namespace AspNet.Security.OAuth.Zoho; - -/// -/// Used to determine which region to use. -/// -public enum ZohoAuthenticationRegion -{ - Global = 0, - Europe, - India, - Australia, - Japan, - Canada, - SaudiArabia -} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs deleted file mode 100644 index 2960a14f7..000000000 --- a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoAuthenticationPostConfigureOptionsTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers - * for more information concerning the license and the contributors participating to this project. - */ - -namespace AspNet.Security.OAuth.Zoho; - -public static class ZohoAuthenticationPostConfigureOptionsTests -{ - [Theory] - [InlineData(ZohoAuthenticationRegion.Australia, "accounts.zoho.com.au")] - [InlineData(ZohoAuthenticationRegion.Canada, "accounts.zohocloud.ca")] - [InlineData(ZohoAuthenticationRegion.Europe, "accounts.zoho.eu")] - [InlineData(ZohoAuthenticationRegion.Global, "accounts.zoho.com")] - [InlineData(ZohoAuthenticationRegion.India, "accounts.zoho.in")] - [InlineData(ZohoAuthenticationRegion.Japan, "accounts.zoho.jp")] - [InlineData(ZohoAuthenticationRegion.SaudiArabia, "accounts.zoho.sa")] - public static void PostConfigure_Configures_Valid_Authentication_Region(ZohoAuthenticationRegion region, string domain) - { - // Arrange - const string name = "Zoho"; - var target = new ZohoAuthenticationPostConfigureOptions(); - - var options = new ZohoAuthenticationOptions - { - Region = region - }; - - // Act - target.PostConfigure(name, options); - - // Assert - options.AuthorizationEndpoint.ShouldBeEquivalentTo( - $"https://{domain}{ZohoAuthenticationDefaults.AuthorizationPath}"); - Uri.TryCreate(options.AuthorizationEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); - - options.TokenEndpoint.ShouldBeEquivalentTo( - $"https://{domain}{ZohoAuthenticationDefaults.TokenPath}"); - Uri.TryCreate(options.TokenEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); - - options.UserInformationEndpoint.ShouldBeEquivalentTo( - $"https://{domain}{ZohoAuthenticationDefaults.UserInformationPath}"); - Uri.TryCreate(options.UserInformationEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); - } - - [Fact] - public static void PostConfigure_Invalid_Authentication_Region_ThrowsException() - { - // Arrange - const string name = "Zoho"; - var target = new ZohoAuthenticationPostConfigureOptions(); - - var options = new ZohoAuthenticationOptions - { - Region = (ZohoAuthenticationRegion)10 - }; - - // Act - Action act = () => target.PostConfigure(name, options); - act.ShouldThrow(); - } -} From 3fd3bd9a8f265f41cd24611caf0c025c90ffc890 Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Tue, 23 Jul 2024 19:40:39 +0300 Subject: [PATCH 07/11] fix tests --- .../Zoho/ZohoTests.cs | 13 +++++++++++-- .../Zoho/bundle.json | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs index 3e6b066b8..6c985e61e 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/ZohoTests.cs @@ -6,13 +6,22 @@ namespace AspNet.Security.OAuth.Zoho; -public class ZohoTests(ITestOutputHelper outputHelper) : OAuthTests(outputHelper) +public class ZohoTests : OAuthTests { + public ZohoTests(ITestOutputHelper outputHelper) + : base(outputHelper) + { + LoopbackRedirectHandler.LoopbackParameters.Add("location", "us"); + } + public override string DefaultScheme => ZohoAuthenticationDefaults.AuthenticationScheme; protected internal override void RegisterAuthentication(AuthenticationBuilder builder) { - builder.AddZoho(options => ConfigureDefaults(builder, options)); + builder.AddZoho(options => + { + ConfigureDefaults(builder, options); + }); } [Theory] diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json index 7bf95559c..68718dd85 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json @@ -21,6 +21,24 @@ "Email": "testuser@example.com", "Display_Name": "User Name" } + }, + { + "uri": "https://accounts.zoho.com/oauth/serverinfo", + "method": "GET", + "contentFormat": "json", + "contentJson": { + "result": "success", + "locations": { + "eu": "https://accounts.zoho.eu", + "au": "https://accounts.zoho.com.au", + "in": "https://accounts.zoho.in", + "jp": "https://accounts.zoho.jp", + "uk": "https://accounts.zoho.uk", + "us": "https://accounts.zoho.com", + "ca": "https://accounts.zohocloud.ca", + "sa": "https://accounts.zoho.sa" + } + } } ] } From 736e488e00251b645cac1b71c083b64c4fb7cc92 Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:30:46 +0300 Subject: [PATCH 08/11] fix remote authentication process --- .../ZohoAuthenticationHandler.cs | 84 +++++++++++++++---- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs index cf8651441..241b9fd19 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs @@ -4,6 +4,7 @@ * for more information concerning the license and the contributors participating to this project. */ +using System.Net; using System.Net.Http.Headers; using System.Net.Mime; using System.Security.Claims; @@ -29,7 +30,8 @@ protected override async Task CreateTicketAsync( [NotNull] AuthenticationProperties properties, [NotNull] OAuthTokenResponse tokens) { - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + var userInformationEndpoint = CreateEndpoint(ZohoAuthenticationDefaults.UserInformationPath); + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInformationEndpoint); requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); requestMessage.Version = Backchannel.DefaultRequestVersion; @@ -51,28 +53,64 @@ protected override async Task CreateTicketAsync( return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); } - protected override async Task HandleRemoteAuthenticateAsync() + protected override async Task ExchangeCodeAsync(OAuthCodeExchangeContext context) { - var location = Context.Request.Query["location"]; + var nameValueCollection = new Dictionary + { + ["client_id"] = Options.ClientId, + ["client_secret"] = Options.ClientSecret, + ["code"] = context.Code, + ["redirect_uri"] = context.RedirectUri, + ["grant_type"] = "authorization_code" + }; + + if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier)) + { + nameValueCollection.Add(OAuthConstants.CodeVerifierKey, codeVerifier!); + context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey); + } - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, ZohoAuthenticationDefaults.ServerInfoEndpoint); - requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - requestMessage.Version = Backchannel.DefaultRequestVersion; + var tokenEndpoint = CreateEndpoint(ZohoAuthenticationDefaults.TokenPath); + using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + request.Content = new FormUrlEncodedContent(nameValueCollection); + request.Version = Backchannel.DefaultRequestVersion; - using var response = await Backchannel.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); + using var response = await Backchannel.SendAsync(request, Context.RequestAborted); if (!response.IsSuccessStatusCode) { - await Log.ServerInfoErrorAsync(Logger, response, Context.RequestAborted); - throw new HttpRequestException("An error occurred while retrieving the server info."); + await Log.ExchangeCodeErrorAsync(Logger, response, Context.RequestAborted); + return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); } - using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted)); + var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted)); - var domain = payload.RootElement.GetProperty("locations").GetProperty(location.ToString()); - Options.UserInformationEndpoint = $"{domain}{ZohoAuthenticationDefaults.UserInformationPath}"; - Options.TokenEndpoint = $"{domain}{ZohoAuthenticationDefaults.TokenPath}"; + return OAuthTokenResponse.Success(payload); + } + + private string CreateEndpoint(string path) + { + var location = Context.Request.Query["location"]; - return await base.HandleRemoteAuthenticateAsync(); + var domain = GetDomain(location.ToString()); + + return $"{domain}{path}"; + } + + private static string GetDomain(string location) + { + return location switch + { + "au" => "https://accounts.zoho.com.au", + "ca" => "https://accounts.zohocloud.ca", + "eu" => "https://accounts.zoho.eu", + "us" => "https://accounts.zoho.com", + "in" => "https://accounts.zoho.in", + "jp" => "https://accounts.zoho.jp", + "sa" => "https://accounts.zoho.sa", + "uk" => "https://accounts.zoho.uk", + _ => "https://accounts.zoho.com" + }; } private static partial class Log @@ -95,6 +133,15 @@ internal static async Task ServerInfoErrorAsync(ILogger logger, HttpResponseMess await response.Content.ReadAsStringAsync(cancellationToken)); } + internal static async Task ExchangeCodeErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) + { + ExchangeCodeError( + logger, + response.StatusCode, + response.Headers.ToString(), + await response.Content.ReadAsStringAsync(cancellationToken)); + } + [LoggerMessage(1, LogLevel.Error, "An error occurred while retrieving the user profile: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] private static partial void UserProfileError( ILogger logger, @@ -105,7 +152,14 @@ private static partial void UserProfileError( [LoggerMessage(2, LogLevel.Error, "An error occurred while retrieving the server info: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] private static partial void ServerInfoErrorAsync( ILogger logger, - System.Net.HttpStatusCode status, + HttpStatusCode status, + string headers, + string body); + + [LoggerMessage(3, LogLevel.Error, "An error occurred while retrieving an access token: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] + private static partial void ExchangeCodeError( + ILogger logger, + HttpStatusCode status, string headers, string body); } From 760469aeeed9ea5bd6f92f5d85db829599356487 Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:39:56 +0300 Subject: [PATCH 09/11] fix code smells, add summary for CreateEndpoint --- .../ZohoAuthenticationDefaults.cs | 7 +------ .../ZohoAuthenticationHandler.cs | 18 ++++++++++-------- .../ZohoAuthenticationOptions.cs | 2 +- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs index 4cdf34b0d..ad48a07b7 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationDefaults.cs @@ -34,7 +34,7 @@ public static class ZohoAuthenticationDefaults /// /// Default value for . /// - public static readonly string AuthorizeEndpoint = "https://accounts.zoho.com/oauth/v2/auth"; + public static readonly string AuthorizationEndpoint = "https://accounts.zoho.com/oauth/v2/auth"; /// /// Default value for . @@ -50,9 +50,4 @@ public static class ZohoAuthenticationDefaults /// Default value for . /// public static readonly string UserInformationPath = "/oauth/user/info"; - - /// - /// Default value for Zoho Server Info. - /// - public static readonly string ServerInfoEndpoint = "https://accounts.zoho.com/oauth/serverinfo"; } diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs index 241b9fd19..edbb4348f 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs @@ -88,18 +88,18 @@ protected override async Task ExchangeCodeAsync(OAuthCodeExc return OAuthTokenResponse.Success(payload); } + /// + /// Creates the endpoint for the Zoho API using the location parameter. + /// If the location parameter doesn't match any of the supported locations, the default location (US) is used. + /// We don't use the accounts-server parameter due to security reasons. + /// + /// The request path. + /// The API endpoint for the Zoho API. private string CreateEndpoint(string path) { var location = Context.Request.Query["location"]; - var domain = GetDomain(location.ToString()); - - return $"{domain}{path}"; - } - - private static string GetDomain(string location) - { - return location switch + var domain = location.ToString().ToLowerInvariant() switch { "au" => "https://accounts.zoho.com.au", "ca" => "https://accounts.zohocloud.ca", @@ -111,6 +111,8 @@ private static string GetDomain(string location) "uk" => "https://accounts.zoho.uk", _ => "https://accounts.zoho.com" }; + + return $"{domain}{path}"; } private static partial class Log diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs index 1e533fec3..7feb8631e 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationOptions.cs @@ -17,7 +17,7 @@ public ZohoAuthenticationOptions() { ClaimsIssuer = ZohoAuthenticationDefaults.Issuer; CallbackPath = ZohoAuthenticationDefaults.CallbackPath; - AuthorizationEndpoint = ZohoAuthenticationDefaults.AuthorizeEndpoint; + AuthorizationEndpoint = ZohoAuthenticationDefaults.AuthorizationEndpoint; TokenEndpoint = ZohoAuthenticationDefaults.TokenEndpoint; Scope.Add("AaaServer.profile.READ"); From 8ccb7f55a66966599630fd359c0b5f5f7f1b21f2 Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:52:48 +0300 Subject: [PATCH 10/11] use UriBuilder instead of interpolation --- .../ZohoAuthenticationHandler.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs index edbb4348f..469e67b5e 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs @@ -112,7 +112,14 @@ private string CreateEndpoint(string path) _ => "https://accounts.zoho.com" }; - return $"{domain}{path}"; + var builder = new UriBuilder(domain) + { + Path = path, + Port = -1, + Scheme = Uri.UriSchemeHttps, + }; + + return builder.Uri.ToString(); } private static partial class Log From bc050864662ca1e675e5c94f6fbec50f8164ed89 Mon Sep 17 00:00:00 2001 From: denis-goncharenko <52198869+denis-goncharenko@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:08:00 +0300 Subject: [PATCH 11/11] remove unused config, fix summary --- .../ZohoAuthenticationHandler.cs | 2 +- .../Zoho/bundle.json | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs index 469e67b5e..96df44898 100644 --- a/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Zoho/ZohoAuthenticationHandler.cs @@ -91,7 +91,7 @@ protected override async Task ExchangeCodeAsync(OAuthCodeExc /// /// Creates the endpoint for the Zoho API using the location parameter. /// If the location parameter doesn't match any of the supported locations, the default location (US) is used. - /// We don't use the accounts-server parameter due to security reasons. + /// We don't use the accounts-server parameter for security reasons. /// /// The request path. /// The API endpoint for the Zoho API. diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json index 68718dd85..7bf95559c 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json +++ b/test/AspNet.Security.OAuth.Providers.Tests/Zoho/bundle.json @@ -21,24 +21,6 @@ "Email": "testuser@example.com", "Display_Name": "User Name" } - }, - { - "uri": "https://accounts.zoho.com/oauth/serverinfo", - "method": "GET", - "contentFormat": "json", - "contentJson": { - "result": "success", - "locations": { - "eu": "https://accounts.zoho.eu", - "au": "https://accounts.zoho.com.au", - "in": "https://accounts.zoho.in", - "jp": "https://accounts.zoho.jp", - "uk": "https://accounts.zoho.uk", - "us": "https://accounts.zoho.com", - "ca": "https://accounts.zohocloud.ca", - "sa": "https://accounts.zoho.sa" - } - } } ] }