Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade Autodesk to v2 #764

Merged
merged 6 commits into from Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -34,15 +34,15 @@ public static class AutodeskAuthenticationDefaults
/// <summary>
/// Default value for <see cref="OAuthOptions.AuthorizationEndpoint"/>.
/// </summary>
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";

/// <summary>
/// Default value for <see cref="OAuthOptions.TokenEndpoint"/>.
/// </summary>
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";

/// <summary>
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
/// </summary>
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";
}
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +51,41 @@ public partial class AutodeskAuthenticationHandler : OAuthHandler<AutodeskAuthen
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
}

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync([NotNull] OAuthCodeExchangeContext context)
{
var tokenRequestParameters = new Dictionary<string, string>
{
["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}"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, the standard way of computing the Authorization: Basic [...] header requires formURL-encoding the client identifier and client secret individually before computing their base64 representation. That said, I see in the Autodesk docs that they don't mention formURL-encoding so their implementation is probably not 100% compliant. Let's keep it this way.

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)
Expand All @@ -67,5 +103,21 @@ internal static async Task UserProfileErrorAsync(ILogger logger, HttpResponseMes
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);
}
}
Expand Up @@ -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");
}
}
Expand Up @@ -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
Expand Down
17 changes: 8 additions & 9 deletions test/AspNet.Security.OAuth.Providers.Tests/Autodesk/bundle.json
Expand Up @@ -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": {
Expand All @@ -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
}
}
]
Expand Down