diff --git a/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationDefaults.cs b/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationDefaults.cs index 5b9905ca9..7ff621b82 100644 --- a/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationDefaults.cs +++ b/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationDefaults.cs @@ -34,15 +34,15 @@ public static class AutodeskAuthenticationDefaults /// /// Default value for . /// - public static readonly string AuthorizationEndpoint = "https://developer.api.autodesk.com/authentication/v1/authorize"; + public static readonly string AuthorizationEndpoint = "https://developer.api.autodesk.com/authentication/v2/authorize"; /// /// Default value for . /// - public static readonly string TokenEndpoint = "https://developer.api.autodesk.com/authentication/v1/gettoken"; + public static readonly string TokenEndpoint = "https://developer.api.autodesk.com/authentication/v2/token"; /// /// Default value for . /// - public static readonly string UserInformationEndpoint = "https://developer.api.autodesk.com/userprofile/v1/users/@me"; + public static readonly string UserInformationEndpoint = "https://api.userprofile.autodesk.com/userinfo"; } diff --git a/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationHandler.cs index fb2db8a76..6eb2b2b1f 100644 --- a/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationHandler.cs @@ -6,6 +6,7 @@ using System.Net.Http.Headers; using System.Security.Claims; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -50,6 +51,41 @@ protected override async Task CreateTicketAsync( return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name); } + protected override async Task ExchangeCodeAsync([NotNull] OAuthCodeExchangeContext context) + { + var tokenRequestParameters = new Dictionary + { + ["redirect_uri"] = context.RedirectUri, + ["code"] = context.Code, + ["grant_type"] = "authorization_code" + }; + + // PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see BuildChallengeUrl + if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier)) + { + tokenRequestParameters[OAuthConstants.CodeVerifierKey] = codeVerifier!; + context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey); + } + + using var requestContent = new FormUrlEncodedContent(tokenRequestParameters!); + using var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes( + $"{Options.ClientId}:{Options.ClientSecret}")); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + requestMessage.Content = requestContent; + requestMessage.Version = Backchannel.DefaultRequestVersion; + using var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); + if (!response.IsSuccessStatusCode) + { + await Log.ExchangeCodeAsync(Logger, response, Context.RequestAborted); + return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token.")); + } + + var body = await response.Content.ReadAsStringAsync(Context.RequestAborted); + return OAuthTokenResponse.Success(JsonDocument.Parse(body)); + } + private static partial class Log { internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) @@ -67,5 +103,21 @@ private static partial void UserProfileError( System.Net.HttpStatusCode status, string headers, string body); + + internal static async Task ExchangeCodeAsync(ILogger logger, HttpResponseMessage response, CancellationToken cancellationToken) + { + ExchangeCodeAsync( + logger, + response.StatusCode, + response.Headers.ToString(), + await response.Content.ReadAsStringAsync(cancellationToken)); + } + + [LoggerMessage(2, LogLevel.Error, "An error occurred while retrieving an access token: the remote server returned a {Status} response with the following payload: {Headers} {Body}.")] + static partial void ExchangeCodeAsync( + ILogger logger, + System.Net.HttpStatusCode status, + string headers, + string body); } } diff --git a/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationOptions.cs index 0895504a9..c323d6b88 100644 --- a/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationOptions.cs +++ b/src/AspNet.Security.OAuth.Autodesk/AutodeskAuthenticationOptions.cs @@ -25,12 +25,11 @@ public AutodeskAuthenticationOptions() Scope.Add("user-profile:read"); - ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "userId"); - ClaimActions.MapJsonKey(ClaimTypes.Name, "userName"); - ClaimActions.MapJsonKey(ClaimTypes.GivenName, "firstName"); - ClaimActions.MapJsonKey(ClaimTypes.Surname, "lastName"); - ClaimActions.MapJsonKey(ClaimTypes.Email, "emailId"); - ClaimActions.MapJsonKey(Claims.EmailVerified, "emailVerified"); - ClaimActions.MapJsonKey(Claims.TwoFactorEnabled, "2FaEnabled"); + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "preferred_username"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(Claims.EmailVerified, "email_verified"); } } diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/AutodeskTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/AutodeskTests.cs index 3df743306..3c46a75ff 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/AutodeskTests.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/AutodeskTests.cs @@ -27,7 +27,6 @@ protected internal override void RegisterAuthentication(AuthenticationBuilder bu [InlineData(ClaimTypes.GivenName, "John")] [InlineData(ClaimTypes.Surname, "Smith")] [InlineData("urn:autodesk:emailverified", "True")] - [InlineData("urn:autodesk:twofactorenabled", "False")] public async Task Can_Sign_In_Using_Autodesk(string claimType, string claimValue) { // Arrange diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/bundle.json index 7d92e1d2d..182cdf53b 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/bundle.json +++ b/test/AspNet.Security.OAuth.Providers.Tests/Autodesk/bundle.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/justeat/httpclient-interception/master/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", "items": [ { - "uri": "https://developer.api.autodesk.com/authentication/v1/gettoken", + "uri": "https://developer.api.autodesk.com/authentication/v2/token", "method": "POST", "contentFormat": "json", "contentJson": { @@ -13,16 +13,15 @@ } }, { - "uri": "https://developer.api.autodesk.com/userprofile/v1/users/@me", + "uri": "https://api.userprofile.autodesk.com/userinfo", "contentFormat": "json", "contentJson": { - "userId": "my-id", - "userName": "John Smith", - "firstName": "John", - "lastName": "Smith", - "emailId": "john@john-smith.local", - "emailVerified": true, - "2FaEnabled": false + "sub": "my-id", + "preferred_username": "John Smith", + "given_name": "John", + "family_name": "Smith", + "email": "john@john-smith.local", + "email_verified": true } } ]