From 2fac6800e5c848e15e4ffc00fa2016defe8c74b1 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 15 Aug 2023 15:00:55 +0100 Subject: [PATCH] Add JumpCloud provider (#797) Add a provider for JumpCloud. --- AspNet.Security.OAuth.Providers.sln | 7 ++ README.md | 2 + docs/jumpcloud.md | 23 +++++ .../AspNet.Security.OAuth.JumpCloud.csproj | 24 ++++++ .../JumpCloudAuthenticationDefaults.cs | 65 ++++++++++++++ .../JumpCloudAuthenticationExtensions.cs | 77 +++++++++++++++++ .../JumpCloudAuthenticationHandler.cs | 84 +++++++++++++++++++ .../JumpCloudAuthenticationOptions.cs | 66 +++++++++++++++ .../JumpCloudPostConfigureOptions.cs | 46 ++++++++++ .../JumpCloudAuthenticationOptionsTests.cs | 75 +++++++++++++++++ .../JumpCloudPostConfigureOptionsTests.cs | 60 +++++++++++++ .../JumpCloud/JumpCloudTests.cs | 44 ++++++++++ .../JumpCloud/bundle.json | 46 ++++++++++ 13 files changed, 619 insertions(+) create mode 100644 docs/jumpcloud.md create mode 100644 src/AspNet.Security.OAuth.JumpCloud/AspNet.Security.OAuth.JumpCloud.csproj create mode 100644 src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationDefaults.cs create mode 100644 src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationExtensions.cs create mode 100644 src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationHandler.cs create mode 100644 src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationOptions.cs create mode 100644 src/AspNet.Security.OAuth.JumpCloud/JumpCloudPostConfigureOptions.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudAuthenticationOptionsTests.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudPostConfigureOptionsTests.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudTests.cs create mode 100644 test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/bundle.json diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index f90e5fa60..f0fce9985 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -296,6 +296,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.Kook" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet.Security.OAuth.PingOne", "src\AspNet.Security.OAuth.PingOne\AspNet.Security.OAuth.PingOne.csproj", "{CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.JumpCloud", "src\AspNet.Security.OAuth.JumpCloud\AspNet.Security.OAuth.JumpCloud.csproj", "{8AF5DDBE-2631-4E71-9045-73A6356CE86B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -678,6 +680,10 @@ Global {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF}.Release|Any CPU.Build.0 = Release|Any CPU + {8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AF5DDBE-2631-4E71-9045-73A6356CE86B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -783,6 +789,7 @@ Global {E3CF7FFC-56A0-4033-87A9-BB3080CF030E} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {101681FB-569F-4941-B943-2AD380039BE0} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {CF8C4235-6AE6-404E-B572-4FF4E85AB5FF} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + {8AF5DDBE-2631-4E71-9045-73A6356CE86B} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C} diff --git a/README.md b/README.md index 6061e5e25..4bba74f60 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ We would love it if you could help contributing to this repository. **Special thanks to our contributors:** +* [Aaron Sadler](https://github.com/aaronsadleruk) * [Abhinav Nigam](https://github.com/abhinavnigam) * [Adam Reisinger](https://github.com/Res42) * [Albert Zakiev](https://github.com/serber) @@ -159,6 +160,7 @@ If a provider you're looking for does not exist, consider making a PR to add one | HubSpot | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.HubSpot?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.HubSpot/ "Download AspNet.Security.OAuth.HubSpot from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.HubSpot?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.HubSpot "Download AspNet.Security.OAuth.HubSpot from MyGet.org") | [Documentation](https://developers.hubspot.com/docs "HubSpot developer documentation") | | Imgur | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Imgur?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Imgur/ "Download AspNet.Security.OAuth.Imgur from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Imgur?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Imgur "Download AspNet.Security.OAuth.Imgur from MyGet.org") | [Documentation](https://apidocs.imgur.com/?version=latest#authorization-and-oauth "Imgur developer documentation") | | Instagram | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Instagram?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Instagram/ "Download AspNet.Security.OAuth.Instagram from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Instagram?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Instagram "Download AspNet.Security.OAuth.Instagram from MyGet.org") | [Documentation](https://www.instagram.com/developer/authentication/ "Instagram developer documentation") | +| JumpCloud | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.JumpCloud?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.JumpCloud/ "Download AspNet.Security.OAuth.JumpCloud from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.JumpCloud?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.JumpCloud "Download AspNet.Security.OAuth.JumpCloud from MyGet.org") | [Documentation](https://jumpcloud.com/support/sso-with-oidc "JumpCloud developer documentation") | | KakaoTalk | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.KakaoTalk?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.KakaoTalk/ "Download AspNet.Security.OAuth.KakaoTalk from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.KakaoTalk?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.KakaoTalk "Download AspNet.Security.OAuth.KakaoTalk from MyGet.org") | [Documentation](https://developers.kakao.com/docs/latest/en/kakaologin/common "KakaoTalk developer documentation") | | Keycloak | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Keycloak?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Keycloak/ "Download AspNet.Security.OAuth.Keycloak from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Keycloak?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Keycloak "Download AspNet.Security.OAuth.Keycloak from MyGet.org") | [Documentation](https://www.keycloak.org/docs/latest/authorization_services/#_service_overview "Keycloak developer documentation") | | Kloudless | [![NuGet](https://buildstats.info/nuget/AspNet.Security.OAuth.Kloudless?includePreReleases=false)](https://www.nuget.org/packages/AspNet.Security.OAuth.Kloudless/ "Download AspNet.Security.OAuth.Kloudless from NuGet.org") | [![MyGet](https://buildstats.info/myget/aspnet-contrib/AspNet.Security.OAuth.Kloudless?includePreReleases=true)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Kloudless "Download AspNet.Security.OAuth.Kloudless from MyGet.org") | [Documentation](https://developers.kloudless.com/docs/v1/authentication "Kloudless developer documentation") | diff --git a/docs/jumpcloud.md b/docs/jumpcloud.md new file mode 100644 index 000000000..70cff69bc --- /dev/null +++ b/docs/jumpcloud.md @@ -0,0 +1,23 @@ +# Integrating the JumpCloud Provider + +## Example + +```csharp +services.AddAuthentication(options => /* Auth configuration */) + .AddJumpCloud(options => + { + options.ClientId = "my-client-id"; + options.ClientSecret = "my-client-secret"; + options.Domain = "https://oauth.id.jumpcloud.com"; + }); +``` + +## Required Additional Settings + +| Property Name | Property Type | Description | Default Value | +|:--|:--|:--|:--| +| `Domain` | `string?` | The JumpCloud domain to use for authentication. | `null` | + +## Optional Settings + +_None._ diff --git a/src/AspNet.Security.OAuth.JumpCloud/AspNet.Security.OAuth.JumpCloud.csproj b/src/AspNet.Security.OAuth.JumpCloud/AspNet.Security.OAuth.JumpCloud.csproj new file mode 100644 index 000000000..e9893651c --- /dev/null +++ b/src/AspNet.Security.OAuth.JumpCloud/AspNet.Security.OAuth.JumpCloud.csproj @@ -0,0 +1,24 @@ + + + + 7.0.4 + $(DefaultNetCoreTargetFramework) + + + + + true + + + + ASP.NET Core security middleware enabling JumpCloud authentication. + AaronSadlerUK + jumpcloud;aspnetcore;authentication;oauth;security + + + + + + + + diff --git a/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationDefaults.cs new file mode 100644 index 000000000..d9256fd6f --- /dev/null +++ b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationDefaults.cs @@ -0,0 +1,65 @@ +/* + * 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.Globalization; + +namespace AspNet.Security.OAuth.JumpCloud; + +/// +/// Default values used by the JumpCloud authentication provider. +/// +public static class JumpCloudAuthenticationDefaults +{ + /// + /// Default value for . + /// + public const string AuthenticationScheme = "JumpCloud"; + + /// + /// Default value for . + /// + public static readonly string DisplayName = "JumpCloud"; + + /// + /// Default value for . + /// + public static readonly string Issuer = "JumpCloud"; + + /// + /// Default value for . + /// + public static readonly string CallbackPath = "/signin-jumpcloud"; + + /// + /// Default path format to use for . + /// + public static readonly string AuthorizationEndpointPathFormat = "/oauth2/auth"; + + /// + /// Default path format to use for . + /// + public static readonly string TokenEndpointPathFormat = "/oauth2/token"; + + /// + /// Default path format to use for . + /// + public static readonly string UserInformationEndpointPathFormat = "/userinfo"; + + /// + /// Default path to use for . + /// + public static readonly string AuthorizationEndpointPath = string.Format(CultureInfo.InvariantCulture, AuthorizationEndpointPathFormat); + + /// + /// Default path to use for . + /// + public static readonly string TokenEndpointPath = string.Format(CultureInfo.InvariantCulture, TokenEndpointPathFormat); + + /// + /// Default path to use for . + /// + public static readonly string UserInformationEndpointPath = string.Format(CultureInfo.InvariantCulture, UserInformationEndpointPathFormat); +} diff --git a/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationExtensions.cs new file mode 100644 index 000000000..29a56de77 --- /dev/null +++ b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationExtensions.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.JumpCloud; + +/// +/// Extension methods to add JumpCloud authentication capabilities to an HTTP application pipeline. +/// +public static class JumpCloudAuthenticationExtensions +{ + /// + /// Adds to the specified + /// , which enables JumpCloud authentication capabilities. + /// + /// The authentication builder. + /// The . + public static AuthenticationBuilder AddJumpCloud([NotNull] this AuthenticationBuilder builder) + { + return builder.AddJumpCloud(JumpCloudAuthenticationDefaults.AuthenticationScheme, options => { }); + } + + /// + /// Adds to the specified + /// , which enables JumpCloud authentication capabilities. + /// + /// The authentication builder. + /// The delegate used to configure the JumpCloud options. + /// The . + public static AuthenticationBuilder AddJumpCloud( + [NotNull] this AuthenticationBuilder builder, + [NotNull] Action configuration) + { + return builder.AddJumpCloud(JumpCloudAuthenticationDefaults.AuthenticationScheme, configuration); + } + + /// + /// Adds to the specified + /// , which enables JumpCloud authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The delegate used to configure the JumpCloud options. + /// The . + public static AuthenticationBuilder AddJumpCloud( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] Action configuration) + { + return builder.AddJumpCloud(scheme, JumpCloudAuthenticationDefaults.DisplayName, configuration); + } + + /// + /// Adds to the specified + /// , which enables JumpCloud 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 JumpCloud options. + /// The . + public static AuthenticationBuilder AddJumpCloud( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [CanBeNull] string caption, + [NotNull] Action configuration) + { + builder.Services.TryAddSingleton, JumpCloudPostConfigureOptions>(); + return builder.AddOAuth(scheme, caption, configuration); + } +} diff --git a/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationHandler.cs b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationHandler.cs new file mode 100644 index 000000000..e936eebf7 --- /dev/null +++ b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationHandler.cs @@ -0,0 +1,84 @@ +/* + * 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.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.JumpCloud; + +/// +/// Defines a handler for authentication using JumpCloud. +/// +public partial class JumpCloudAuthenticationHandler : OAuthHandler +{ + /// + /// Initializes a new instance of the class. + /// + /// The authentication options. + /// The logger to use. + /// The URL encoder to use. + /// The system clock to use. + public JumpCloudAuthenticationHandler( + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder, + [NotNull] ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + /// + protected override async Task CreateTicketAsync( + [NotNull] ClaimsIdentity identity, + [NotNull] AuthenticationProperties properties, + [NotNull] OAuthTokenResponse tokens) + { + string endpoint = Options.UserInformationEndpoint; + + using var request = new HttpRequestMessage(HttpMethod.Get, endpoint); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + + using var response = await Backchannel.SendAsync(request, 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 from JumpCloud."); + } + + 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.JumpCloud/JumpCloudAuthenticationOptions.cs b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationOptions.cs new file mode 100644 index 000000000..73fe44341 --- /dev/null +++ b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudAuthenticationOptions.cs @@ -0,0 +1,66 @@ +/* + * 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.JumpCloud; + +/// +/// Defines a set of options used by . +/// +public class JumpCloudAuthenticationOptions : OAuthOptions +{ + /// + /// Initializes a new instance of the class. + /// + public JumpCloudAuthenticationOptions() + { + ClaimsIssuer = JumpCloudAuthenticationDefaults.Issuer; + CallbackPath = JumpCloudAuthenticationDefaults.CallbackPath; + + Scope.Add("openid"); + Scope.Add("profile"); + Scope.Add("email"); + + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name"); + } + + /// + /// Gets or sets the JumpCloud domain (Org URL) to use for authentication. + /// + public string? Domain { get; set; } + + /// + public override void Validate() + { + base.Validate(); + + if (!Uri.TryCreate(AuthorizationEndpoint, UriKind.Absolute, out _)) + { + throw new ArgumentException( + $"The '{nameof(AuthorizationEndpoint)}' option must be set to a valid URI.", + nameof(AuthorizationEndpoint)); + } + + if (!Uri.TryCreate(TokenEndpoint, UriKind.Absolute, out _)) + { + throw new ArgumentException( + $"The '{nameof(TokenEndpoint)}' option must be set to a valid URI.", + nameof(TokenEndpoint)); + } + + if (!Uri.TryCreate(UserInformationEndpoint, UriKind.Absolute, out _)) + { + throw new ArgumentException( + $"The '{nameof(UserInformationEndpoint)}' option must be set to a valid URI.", + nameof(UserInformationEndpoint)); + } + } +} diff --git a/src/AspNet.Security.OAuth.JumpCloud/JumpCloudPostConfigureOptions.cs b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudPostConfigureOptions.cs new file mode 100644 index 000000000..5d926bf25 --- /dev/null +++ b/src/AspNet.Security.OAuth.JumpCloud/JumpCloudPostConfigureOptions.cs @@ -0,0 +1,46 @@ +/* + * 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.Globalization; +using Microsoft.Extensions.Options; + +namespace AspNet.Security.OAuth.JumpCloud; + +/// +/// A class used to setup defaults for all . +/// +public class JumpCloudPostConfigureOptions : IPostConfigureOptions +{ + /// + public void PostConfigure( + string? name, + [NotNull] JumpCloudAuthenticationOptions options) + { + if (string.IsNullOrWhiteSpace(options.Domain)) + { + throw new ArgumentException("No JumpCloud domain configured.", nameof(options)); + } + + options.AuthorizationEndpoint = CreateUrl(options.Domain, JumpCloudAuthenticationDefaults.AuthorizationEndpointPathFormat); + options.TokenEndpoint = CreateUrl(options.Domain, JumpCloudAuthenticationDefaults.TokenEndpointPathFormat); + options.UserInformationEndpoint = CreateUrl(options.Domain, JumpCloudAuthenticationDefaults.UserInformationEndpointPathFormat); + } + + private static string CreateUrl(string domain, string pathFormat, params object[] args) + { + var path = string.Format(CultureInfo.InvariantCulture, pathFormat, args); + + // Enforce use of HTTPS + var builder = new UriBuilder(domain) + { + Path = path, + Port = -1, + Scheme = Uri.UriSchemeHttps, + }; + + return builder.Uri.ToString(); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudAuthenticationOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudAuthenticationOptionsTests.cs new file mode 100644 index 000000000..0ca3b1693 --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudAuthenticationOptionsTests.cs @@ -0,0 +1,75 @@ +/* + * 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.JumpCloud; + +public static class JumpCloudAuthenticationOptionsTests +{ + [Fact] + public static void Validate_Throws_If_AuthorizationEndpoint_Not_Set() + { + // Arrange + var options = new JumpCloudAuthenticationOptions() + { + ClientId = "ClientId", + ClientSecret = "ClientSecret", + TokenEndpoint = "https://jumpcloud.local", + UserInformationEndpoint = "https://jumpcloud.local", + }; + + // Act and Assert + Assert.Throws("AuthorizationEndpoint", () => options.Validate()); + } + + [Fact] + public static void Validate_Throws_If_TokenEndpoint_Not_Set() + { + // Arrange + var options = new JumpCloudAuthenticationOptions() + { + AuthorizationEndpoint = "https://jumpcloud.local", + ClientId = "ClientId", + ClientSecret = "ClientSecret", + UserInformationEndpoint = "https://jumpcloud.local", + }; + + // Act and Assert + Assert.Throws("TokenEndpoint", () => options.Validate()); + } + + [Fact] + public static void Validate_Throws_If_UserInformationEndpoint_Not_Set() + { + // Arrange + var options = new JumpCloudAuthenticationOptions() + { + AuthorizationEndpoint = "https://jumpcloud.local", + ClientId = "ClientId", + ClientSecret = "ClientSecret", + TokenEndpoint = "https://jumpcloud.local", + }; + + // Act and Assert + Assert.Throws("UserInformationEndpoint", () => options.Validate()); + } + + [Fact] + public static void Validate_Does_Not_Throw_If_Uris_Are_Valid() + { + // Arrange + var options = new JumpCloudAuthenticationOptions() + { + AuthorizationEndpoint = "https://jumpcloud.local", + ClientId = "ClientId", + ClientSecret = "ClientSecret", + TokenEndpoint = "https://jumpcloud.local", + UserInformationEndpoint = "https://jumpcloud.local", + }; + + // Act (no Assert) + options.Validate(); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudPostConfigureOptionsTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudPostConfigureOptionsTests.cs new file mode 100644 index 000000000..0c7fbd1f1 --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudPostConfigureOptionsTests.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. + */ + +namespace AspNet.Security.OAuth.JumpCloud; + +public static class JumpCloudPostConfigureOptionsTests +{ + [Theory] + [InlineData("jumpcloud.local")] + [InlineData("http://jumpcloud.local")] + [InlineData("http://jumpcloud.local/")] + [InlineData("https://jumpcloud.local")] + [InlineData("https://jumpcloud.local/")] + public static void PostConfigure_Configures_Valid_Endpoints(string domain) + { + // Arrange + string name = "JumpCloud"; + var target = new JumpCloudPostConfigureOptions(); + + var options = new JumpCloudAuthenticationOptions() + { + Domain = domain, + }; + + // Act + target.PostConfigure(name, options); + + // Assert + options.AuthorizationEndpoint.ShouldStartWith("https://jumpcloud.local/oauth2/auth"); + Uri.TryCreate(options.AuthorizationEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); + + options.TokenEndpoint.ShouldStartWith("https://jumpcloud.local/oauth2/token"); + Uri.TryCreate(options.TokenEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); + + options.UserInformationEndpoint.ShouldStartWith("https://jumpcloud.local/userinfo"); + Uri.TryCreate(options.UserInformationEndpoint, UriKind.Absolute, out _).ShouldBeTrue(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public static void PostConfigure_Throws_If_Domain_Is_Invalid(string value) + { + // Arrange + string name = "JumpCloud"; + var target = new JumpCloudPostConfigureOptions(); + + var options = new JumpCloudAuthenticationOptions() + { + Domain = value, + }; + + // Act and Assert + Assert.Throws("options", () => target.PostConfigure(name, options)); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudTests.cs new file mode 100644 index 000000000..b36028ea9 --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/JumpCloudTests.cs @@ -0,0 +1,44 @@ +/* + * 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.JumpCloud; + +public class JumpCloudTests : OAuthTests +{ + public JumpCloudTests(ITestOutputHelper outputHelper) + { + OutputHelper = outputHelper; + } + + public override string DefaultScheme => JumpCloudAuthenticationDefaults.AuthenticationScheme; + + protected internal override void RegisterAuthentication(AuthenticationBuilder builder) + { + builder.AddJumpCloud(options => + { + ConfigureDefaults(builder, options); + options.Domain = "jumpcloud.local"; + }); + } + + [Theory] + [InlineData(ClaimTypes.Email, "john.doe@example.com")] + [InlineData(ClaimTypes.GivenName, "John")] + [InlineData(ClaimTypes.Name, "John Doe")] + [InlineData(ClaimTypes.NameIdentifier, "00uid4BxXw6I6TV4m0g3")] + [InlineData(ClaimTypes.Surname, "Doe")] + public async Task Can_Sign_In_Using_JumpCloud(string claimType, string claimValue) + { + // Arrange + using var server = CreateTestServer(); + + // Act + var claims = await AuthenticateUserAsync(server); + + // Assert + AssertClaim(claims, claimType, claimValue); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/bundle.json new file mode 100644 index 000000000..b6aa0e9ef --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/JumpCloud/bundle.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", + "items": [ + { + "comment": "https://jumpcloud.com/support/sso-with-oidc", + "uri": "https://jumpcloud.local/oauth2/token", + "method": "POST", + "contentFormat": "json", + "contentJson": { + "access_token": "secret-access-token", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "openid email", + "refresh_token": "secret-refresh-token", + "id_token": "secret-id-token" + } + }, + { + "comment": "https://jumpcloud.com/support/sso-with-oidc", + "uri": "https://jumpcloud.local/userinfo", + "contentFormat": "json", + "contentJson": { + "sub": "00uid4BxXw6I6TV4m0g3", + "name": "John Doe", + "nickname": "Jimmy", + "given_name": "John", + "middle_name": "James", + "family_name": "Doe", + "profile": "https://example.com/john.doe", + "zoneinfo": "America/Los_Angeles", + "locale": "en-US", + "updated_at": 1311280970, + "email": "john.doe@example.com", + "email_verified": true, + "address": { + "street_address": "123 Hollywood Blvd.", + "locality": "Los Angeles", + "region": "CA", + "postal_code": "90210", + "country": "US" + }, + "phone_number": "+1 (425) 555-1212" + } + } + ] +}