diff --git a/AspNet.Security.OAuth.Providers.sln b/AspNet.Security.OAuth.Providers.sln index b6d50e601..b9ec15bed 100644 --- a/AspNet.Security.OAuth.Providers.sln +++ b/AspNet.Security.OAuth.Providers.sln @@ -184,10 +184,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C2CA4B38-A docs\bitbucket.md = docs\bitbucket.md docs\digitalocean.md = docs\digitalocean.md docs\discord.md = docs\discord.md + docs\docusign.md = docs\docusign.md docs\dropbox.md = docs\dropbox.md docs\ebay.md = docs\ebay.md docs\eveonline.md = docs\eveonline.md docs\foursquare.md = docs\foursquare.md + docs\gitcode.md = docs\gitcode.md docs\gitee.md = docs\gitee.md docs\github.md = docs\github.md docs\instagram.md = docs\instagram.md @@ -219,8 +221,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C2CA4B38-A docs\workweixin.md = docs\workweixin.md docs\xumm.md = docs\xumm.md docs\zendesk.md = docs\zendesk.md - docs\docusign.md = docs\docusign.md - docs\gitcode.md = docs\gitcode.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}" @@ -316,6 +316,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.VkId" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.GitCode", "src\AspNet.Security.OAuth.GitCode\AspNet.Security.OAuth.GitCode.csproj", "{668833D5-DB6A-475F-B0FD-A03462B037B8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet.Security.OAuth.Atlassian", "src\AspNet.Security.OAuth.Atlassian\AspNet.Security.OAuth.Atlassian.csproj", "{D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -734,6 +736,10 @@ Global {668833D5-DB6A-475F-B0FD-A03462B037B8}.Debug|Any CPU.Build.0 = Debug|Any CPU {668833D5-DB6A-475F-B0FD-A03462B037B8}.Release|Any CPU.ActiveCfg = Release|Any CPU {668833D5-DB6A-475F-B0FD-A03462B037B8}.Release|Any CPU.Build.0 = Release|Any CPU + {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -848,6 +854,7 @@ Global {CD56ABE4-1CD2-4029-B556-E110A31A2CC4} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {F3E62C24-5F82-4CF5-A994-0E10D04FB495} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} {668833D5-DB6A-475F-B0FD-A03462B037B8} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} + {D2110C1B-6FE1-4D9A-81ED-93FB2AC85049} = {C1352FD3-AE8B-43EE-B45B-F6E0B3FBAC6D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7B54DE2-6407-4802-AD9C-CE54BF414C8C} diff --git a/README.md b/README.md index 279718821..d0cf81be2 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ If a provider you're looking for does not exist, consider making a PR to add one | Apple | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Apple?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Apple/ "Download AspNet.Security.OAuth.Apple from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Apple?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Apple "Download AspNet.Security.OAuth.Apple from MyGet.org") | [Documentation](https://developer.apple.com/documentation/signinwithapplerestapi "Apple developer documentation") | | ArcGIS | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.ArcGIS?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.ArcGIS/ "Download AspNet.Security.OAuth.ArcGIS from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.ArcGIS?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.ArcGIS "Download AspNet.Security.OAuth.ArcGIS from MyGet.org") | [Documentation](https://developers.arcgis.com/documentation/core-concepts/security-and-authentication/what-is-oauth-2/ "ArcGIS developer documentation") | | Asana | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Asana?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Asana/ "Download AspNet.Security.OAuth.Asana from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Asana?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Asana "Download AspNet.Security.OAuth.Asana from MyGet.org") | [Documentation](https://asana.com/developers/documentation/getting-started/auth "Asana developer documentation") | +| Atlassian | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Atlassian?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Atlassian/ "Download AspNet.Security.OAuth.Atlassian from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Atlassian?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Atlassian "Download AspNet.Security.OAuth.Atlassian from MyGet.org") | [Documentation](https://developer.atlassian.com/cloud/oauth/ "Atlassian developer documentation") | | Autodesk | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Autodesk?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Autodesk/ "Download AspNet.Security.OAuth.Autodesk from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Autodesk?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Autodesk "Download AspNet.Security.OAuth.Autodesk from MyGet.org") | [Documentation](https://forge.autodesk.com/en/docs/oauth/v2/developers_guide/overview/ "Autodesk developer documentation") | | Baidu | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Baidu?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Baidu/ "Download AspNet.Security.OAuth.Baidu from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Baidu?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Baidu "Download AspNet.Security.OAuth.Baidu from MyGet.org") | [Documentation](https://developer.baidu.com/ "Baidu developer documentation") | | Basecamp | [![NuGet](https://img.shields.io/nuget/v/AspNet.Security.OAuth.Basecamp?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/AspNet.Security.OAuth.Basecamp/ "Download AspNet.Security.OAuth.Basecamp from NuGet.org") | [![MyGet](https://img.shields.io/myget/aspnet-contrib/vpre/AspNet.Security.OAuth.Basecamp?logo=nuget&label=MyGet&color=blue)](https://www.myget.org/feed/aspnet-contrib/package/nuget/AspNet.Security.OAuth.Basecamp "Download AspNet.Security.OAuth.Basecamp from MyGet.org") | [Documentation](https://github.com/basecamp/api/blob/master/sections/authentication.md "Basecamp developer documentation") | diff --git a/src/AspNet.Security.OAuth.Atlassian/AspNet.Security.OAuth.Atlassian.csproj b/src/AspNet.Security.OAuth.Atlassian/AspNet.Security.OAuth.Atlassian.csproj new file mode 100644 index 000000000..4d7b46222 --- /dev/null +++ b/src/AspNet.Security.OAuth.Atlassian/AspNet.Security.OAuth.Atlassian.csproj @@ -0,0 +1,24 @@ + + + + 9.1.0 + $(DefaultNetCoreTargetFramework) + + + + + true + + + + ASP.NET Core security middleware enabling Atlassian authentication. + smnsht + atlassian;aspnetcore;authentication;oauth;security + + + + + + + + diff --git a/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationDefaults.cs new file mode 100644 index 000000000..0738337e3 --- /dev/null +++ b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationDefaults.cs @@ -0,0 +1,43 @@ +// 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.Atlassian; + +public class AtlassianAuthenticationDefaults +{ + /// + /// Default value for . + /// + public const string AuthenticationScheme = "Atlassian"; + + /// + /// Default value for . + /// + public static readonly string DisplayName = "Atlassian"; + + /// + /// Default value for . + /// + public static readonly string Issuer = "Atlassian"; + + /// + /// Default value for . + /// + public static readonly string CallbackPath = "/signin-atlassian"; + + /// + /// Default value for . + /// + public static readonly string AuthorizationEndpoint = "https://auth.atlassian.com/authorize"; + + /// + /// Default value for . + /// + public static readonly string TokenEndpoint = "https://auth.atlassian.com/oauth/token"; + + /// + /// Default value for . + /// + public static readonly string UserInformationEndpoint = "https://api.atlassian.com/me"; +} diff --git a/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationExtensions.cs b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationExtensions.cs new file mode 100644 index 000000000..8d16e6c46 --- /dev/null +++ b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationExtensions.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 Microsoft.Extensions.DependencyInjection; + +namespace AspNet.Security.OAuth.Atlassian; + +/// +/// Extension methods to add Atlassian authentication capabilities to an HTTP application pipeline. +/// +public static class AtlassianAuthenticationExtensions +{ + /// + /// Adds to the specified + /// , which enables Atlassian authentication capabilities. + /// + /// The authentication builder. + /// A reference to this instance after the operation has completed. + public static AuthenticationBuilder AddAtlassian([NotNull] this AuthenticationBuilder builder) + { + return builder.AddAtlassian(AtlassianAuthenticationDefaults.AuthenticationScheme, options => { }); + } + + /// + /// Adds to the specified + /// , which enables Atlassian 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 AddAtlassian( + [NotNull] this AuthenticationBuilder builder, + [NotNull] Action configuration) + { + return builder.AddAtlassian(AtlassianAuthenticationDefaults.AuthenticationScheme, configuration); + } + + /// + /// Adds to the specified + /// , which enables Atlassian authentication capabilities. + /// + /// The authentication builder. + /// The authentication scheme associated with this instance. + /// The delegate used to configure the Atlassian options. + /// The . + public static AuthenticationBuilder AddAtlassian( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [NotNull] Action configuration) + { + return builder.AddAtlassian(scheme, AtlassianAuthenticationDefaults.DisplayName, configuration); + } + + /// + /// Adds to the specified + /// , which enables Atlassian 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 Atlassian options. + /// The . + public static AuthenticationBuilder AddAtlassian( + [NotNull] this AuthenticationBuilder builder, + [NotNull] string scheme, + [CanBeNull] string caption, + [NotNull] Action configuration) + { + return builder.AddOAuth(scheme, caption, configuration); + } +} diff --git a/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationHandler.cs new file mode 100644 index 000000000..c63ba4f78 --- /dev/null +++ b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationHandler.cs @@ -0,0 +1,70 @@ +/* + * 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.Atlassian; + +public partial class AtlassianAuthenticationHandler : OAuthHandler +{ + public AtlassianAuthenticationHandler( + [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 request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + 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."); + } + + 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.Atlassian/AtlassianAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationOptions.cs new file mode 100644 index 000000000..e5212ba06 --- /dev/null +++ b/src/AspNet.Security.OAuth.Atlassian/AtlassianAuthenticationOptions.cs @@ -0,0 +1,45 @@ +/* + * 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.Linq; +using System.Security.Claims; +using static AspNet.Security.OAuth.Atlassian.AtlassianOAuthenticationConstants; + +namespace AspNet.Security.OAuth.Atlassian; + +public partial class AtlassianAuthenticationOptions : OAuthOptions +{ + public AtlassianAuthenticationOptions() + { + ClaimsIssuer = AtlassianAuthenticationDefaults.Issuer; + CallbackPath = AtlassianAuthenticationDefaults.CallbackPath; + + AuthorizationEndpoint = AtlassianAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = AtlassianAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = AtlassianAuthenticationDefaults.UserInformationEndpoint; + + AdditionalAuthorizationParameters.Add("audience", "api.atlassian.com"); + AdditionalAuthorizationParameters.Add("prompt", "consent"); + + Scope.Add("read:me"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "account_id"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + + ClaimActions.MapJsonKey(Claims.AccountType, "account_type"); + ClaimActions.MapJsonKey(Claims.Picture, "picture"); + ClaimActions.MapJsonKey(Claims.AccountStatus, "account_status"); + ClaimActions.MapJsonKey(Claims.Nickname, "nickname"); + ClaimActions.MapJsonKey(Claims.ZoneInfo, "zoneinfo"); + ClaimActions.MapJsonKey(Claims.Locale, "locale"); + + ClaimActions.MapJsonSubKey(Claims.JobTitle, "extended_profile", "job_title"); + ClaimActions.MapJsonSubKey(Claims.Organization, "extended_profile", "organization"); + ClaimActions.MapJsonSubKey(Claims.Department, "extended_profile", "department"); + ClaimActions.MapJsonSubKey(Claims.Location, "extended_profile", "location"); + } +} diff --git a/src/AspNet.Security.OAuth.Atlassian/AtlassianOAuthenticationConstants.cs b/src/AspNet.Security.OAuth.Atlassian/AtlassianOAuthenticationConstants.cs new file mode 100644 index 000000000..154c316cd --- /dev/null +++ b/src/AspNet.Security.OAuth.Atlassian/AtlassianOAuthenticationConstants.cs @@ -0,0 +1,22 @@ +// 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.Atlassian; + +public static class AtlassianOAuthenticationConstants +{ + public static class Claims + { + public static readonly string AccountType = "urn:atlassian:account_type"; + public static readonly string Picture = "urn:atlassian:picture"; + public static readonly string AccountStatus = "urn:atlassian:account_status"; + public static readonly string Nickname = "urn:atlassian:nickname"; + public static readonly string ZoneInfo = "urn:atlassian:zoneinfo"; + public static readonly string Locale = "urn:atlassian:locale"; + public static readonly string JobTitle = "urn:atlassian:job_title"; + public static readonly string Organization = "urn:atlassian:organization"; + public static readonly string Department = "urn:atlassian:department"; + public static readonly string Location = "urn:atlassian:location"; + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Atlassian/AtlassianTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Atlassian/AtlassianTests.cs new file mode 100644 index 000000000..3f62fdfb1 --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Atlassian/AtlassianTests.cs @@ -0,0 +1,68 @@ +// 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.AspNetCore.WebUtilities; + +namespace AspNet.Security.OAuth.Atlassian; + +public class AtlassianTests(ITestOutputHelper outputHelper) : OAuthTests(outputHelper) +{ + public override string DefaultScheme => AtlassianAuthenticationDefaults.AuthenticationScheme; + + protected internal override void RegisterAuthentication(AuthenticationBuilder builder) + { + builder.AddAtlassian(options => ConfigureDefaults(builder, options)); + } + + public static IEnumerable AtlassianClaimsData => new List + { + new string[] { ClaimTypes.NameIdentifier, "112233aa-bb11-cc22-33dd-445566abcabc" }, + new string[] { ClaimTypes.Email, "mia@example.com" }, + new string[] { ClaimTypes.Name, "Mia Krystof" }, + new string[] { AtlassianOAuthenticationConstants.Claims.AccountType, "atlassian" }, + new string[] { AtlassianOAuthenticationConstants.Claims.Picture, "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/112233aa-bb11-cc22-33dd-445566abcabc/1234abcd-9876-54aa-33aa-1234dfsade9487ds" }, + new string[] { AtlassianOAuthenticationConstants.Claims.AccountStatus, "active" }, + new string[] { AtlassianOAuthenticationConstants.Claims.Nickname, "mkrystof" }, + new string[] { AtlassianOAuthenticationConstants.Claims.ZoneInfo, "Australia/Sydney" }, + new string[] { AtlassianOAuthenticationConstants.Claims.Locale, "en-US" }, + new string[] { AtlassianOAuthenticationConstants.Claims.JobTitle, "Designer" }, + new string[] { AtlassianOAuthenticationConstants.Claims.Organization, "mia@example.com" }, + new string[] { AtlassianOAuthenticationConstants.Claims.Department, "Design team" }, + new string[] { AtlassianOAuthenticationConstants.Claims.Location, "Sydney" }, + }; + + [Theory] + [MemberData(nameof(AtlassianClaimsData))] + public async Task Can_Sign_In_Using_Atlassian(string claimType, string claimValue) + => await AuthenticateUserAndAssertClaimValue(claimType, claimValue); + + [Fact] + public async Task BuildChallengeUrl_Generates_Correct_Url() + { + // Arrange + var options = new AtlassianAuthenticationOptions(); + + var redirectUrl = "https://my-site.local/signin-atlassian"; + + // Act + Uri actual = await BuildChallengeUriAsync( + options, + redirectUrl, + (options, loggerFactory, encoder) => new AtlassianAuthenticationHandler(options, loggerFactory, encoder)); + + // Assert + actual.ShouldNotBeNull(); + actual.ToString().ShouldStartWith("https://auth.atlassian.com/authorize?"); + + var query = QueryHelpers.ParseQuery(actual.Query); + + query.ShouldContainKeyAndValue("audience", "api.atlassian.com"); + query.ShouldContainKeyAndValue("client_id", options.ClientId); + query.ShouldContainKeyAndValue("scope", "read:me"); + query.ShouldContainKeyAndValue("redirect_uri", redirectUrl); + query.ShouldContainKey("state"); + query.ShouldContainKeyAndValue("response_type", "code"); + query.ShouldContainKeyAndValue("prompt", "consent"); + } +} diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Atlassian/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Atlassian/bundle.json new file mode 100644 index 000000000..5c9367d7b --- /dev/null +++ b/test/AspNet.Security.OAuth.Providers.Tests/Atlassian/bundle.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", + "items": [ + { + "uri": "https://auth.atlassian.com/oauth/token", + "method": "POST", + "contentFormat": "json", + "contentJson": { + "access_token": "secret-access-token", + "token_type": "access", + "refresh_token": "secret-refresh-token", + "expires_in": "300" + } + }, + { + "uri": "https://api.atlassian.com/me", + "contentFormat": "json", + "contentJson": { + "account_type": "atlassian", + "account_id": "112233aa-bb11-cc22-33dd-445566abcabc", + "email": "mia@example.com", + "name": "Mia Krystof", + "picture": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/112233aa-bb11-cc22-33dd-445566abcabc/1234abcd-9876-54aa-33aa-1234dfsade9487ds", + "account_status": "active", + "nickname": "mkrystof", + "zoneinfo": "Australia/Sydney", + "locale": "en-US", + "extended_profile": { + "job_title": "Designer", + "organization": "mia@example.com", + "department": "Design team", + "location": "Sydney" + } + } + } + ] +}