Using OAuth for Azure Active Directory with an identity model in ASP.NET Core 2.0

Problem statement

We are developing a new enterprise-level application and want to use Azure Active Directory to enter the application so that we do not have to create another set of user credentials. However, our permission model for this application is more complex than what can be processed through groups inside AAD.

Think

The idea was that we could use Azure Active Directory OAuth 2.0 in addition to the Core Identity ASP.NET infrastructure to force users to authenticate through Azure Active Directory and then use the authentication system to handle permissions / permissions.

Problems

You can create projects out of the box using Azure OpenId authentication, and then you can easily add Microsoft account authentication (not AAD) to any project using the Identity platform. But nothing has been done to add OAuth for AAD to the authentication model.

After trying to hack into these methods to get them working the way I needed, I finally made a homebrew attempt to create my own solution from the OAuthHandler and OAuthOptions .

I encountered a lot of problems along this path, but I managed to work out most of them. Now I am to such an extent that I get the token from the endpoint, but my ClaimsIdentity does not seem valid. Then, when redirecting to ExternalLoginCallback, my SigninManager will not be able to receive external registration information.

There should almost certainly be something simple that I’m missing, but I can’t determine what it is.

Code

Startup.cs

 services.AddAuthentication() .AddAzureAd(options => { options.ClientId = Configuration["AzureAd:ClientId"]; options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize"; options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token"; options.UserInformationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/openid/userinfo"; options.Resource = Configuration["AzureAd:ClientId"]; options.ClientSecret = Configuration["AzureAd:ClientSecret"]; options.CallbackPath = Configuration["AzureAd:CallbackPath"]; }); 

Azureadextensions

 namespace Microsoft.AspNetCore.Authentication.AzureAD { public static class AzureAdExtensions { public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder) => builder.AddAzureAd(_ => { }); public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions) { return builder.AddOAuth<AzureAdOptions, AzureAdHandler>(AzureAdDefaults.AuthenticationScheme, AzureAdDefaults.DisplayName, configureOptions); } public static ChallengeResult ChallengeAzureAD(this ControllerBase controllerBase, SignInManager<ApplicationUser> signInManager, string redirectUrl) { return controllerBase.Challenge(signInManager.ConfigureExternalAuthenticationProperties(AzureAdDefaults.AuthenticationScheme, redirectUrl), AzureAdDefaults.AuthenticationScheme); } } } 

AzureADOptions and defaults

 public class AzureAdOptions : OAuthOptions { public string Instance { get; set; } public string Resource { get; set; } public string TenantId { get; set; } public AzureAdOptions() { CallbackPath = new PathString("/signin-azureAd"); AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint; TokenEndpoint = AzureAdDefaults.TokenEndpoint; UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint; Scope.Add("https://graph.windows.net/user.read"); ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "unique_name"); ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "given_name"); ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "family_name"); ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/groups", "groups"); ClaimActions.MapJsonKey("http://schemas.microsoft.com/identity/claims/objectidentifier", "oid"); ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "roles"); } } public static class AzureAdDefaults { public static readonly string DisplayName = "AzureAD"; public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize"; public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token"; public static readonly string UserInformationEndpoint = "https://login.microsoftonline.com/common/openid/userinfo"; // "https://graph.windows.net/v1.0/me"; public const string AuthenticationScheme = "AzureAD"; } 

AzureADHandler

 internal class AzureAdHandler : OAuthHandler<AzureAdOptions> { public AzureAdHandler(IOptionsMonitor<AzureAdOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); HttpResponseMessage httpResponseMessage = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted); if (!httpResponseMessage.IsSuccessStatusCode) throw new HttpRequestException(message: $"Failed to retrived Azure AD user information ({httpResponseMessage.StatusCode}) Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled."); JObject user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync()); OAuthCreatingTicketContext context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, user); context.RunClaimActions(); await Events.CreatingTicket(context); return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); } protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); dictionary.Add("grant_type", "authorization_code"); dictionary.Add("client_id", Options.ClientId); dictionary.Add("redirect_uri", redirectUri); dictionary.Add("client_secret", Options.ClientSecret); dictionary.Add(nameof(code), code); dictionary.Add("resource", Options.Resource); HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint); httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpRequestMessage.Content = new FormUrlEncodedContent(dictionary); HttpResponseMessage response = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted); if (response.IsSuccessStatusCode) return OAuthTokenResponse.Success(JObject.Parse(await response.Content.ReadAsStringAsync())); return OAuthTokenResponse.Failed(new Exception(string.Concat("OAuth token endpoint failure: ", await Display(response)))); } protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); dictionary.Add("client_id", Options.ClientId); dictionary.Add("scope", FormatScope()); dictionary.Add("response_type", "code"); dictionary.Add("redirect_uri", redirectUri); dictionary.Add("state", Options.StateDataFormat.Protect(properties)); dictionary.Add("resource", Options.Resource); return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, dictionary); } private static async Task<string> Display(HttpResponseMessage response) { StringBuilder output = new StringBuilder(); output.Append($"Status: { response.StatusCode };"); output.Append($"Headers: { response.Headers.ToString() };"); output.Append($"Body: { await response.Content.ReadAsStringAsync() };"); return output.ToString(); } } 

AccountController.cs

  [HttpGet] [AllowAnonymous] public async Task<IActionResult> SignIn() { var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account"); return this.ChallengeAzureAD(_signInManager, redirectUrl); } [HttpGet] [AllowAnonymous] public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null) { if (remoteError != null) { _logger.LogInformation($"Error from external provider: {remoteError}"); return RedirectToAction(nameof(SignedOut)); } var info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) //This always ends up true! { return RedirectToAction(nameof(SignedOut)); } } 

There you have it!

This is the code that I have, and I'm pretty sure that at the moment there is something simple that I don’t see, but I'm not sure what it is. I know that my CreateTicketAsync method is also problematic, since I didn’t get to the right endpoint of the user information (or didn’t hit it correctly), but this is another problem together, because from what I understand, the claims that concern me must be returned as part of the marker.

Any help would be greatly appreciated!

+5
source share
1 answer

I ended up solving my problem, as I ended up with several problems. I passed the wrong value for the resource field, did not correctly configure the NameIdentifer display, and then had the wrong endpoint for popping user information. Part of the user information is the largest, since it is the token that I found out what the external input element was looking for.

Updated Code

Startup.cs

 services.AddAuthentication() .AddAzureAd(options => { options.ClientId = Configuration["AzureAd:ClientId"]; options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize"; options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token"; options.ClientSecret = Configuration["AzureAd:ClientSecret"]; options.CallbackPath = Configuration["AzureAd:CallbackPath"]; }); 

AzureADOptions and defaults

 public class AzureAdOptions : OAuthOptions { public string Instance { get; set; } public string Resource { get; set; } public string TenantId { get; set; } public AzureAdOptions() { CallbackPath = new PathString("/signin-azureAd"); AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint; TokenEndpoint = AzureAdDefaults.TokenEndpoint; UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint; Resource = AzureAdDefaults.Resource; Scope.Add("user.read"); ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName"); ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname"); ClaimActions.MapJsonKey(ClaimTypes.MobilePhone, "mobilePhone"); ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName")); } } public static class AzureAdDefaults { public static readonly string DisplayName = "AzureAD"; public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize"; public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token"; public static readonly string Resource = "https://graph.microsoft.com"; public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me"; public const string AuthenticationScheme = "AzureAD"; } 
0
source

Source: https://habr.com/ru/post/1273220/


All Articles