How to save token received in AcquireTokenAsync using Active Directory

Problem Statement

I am using .NET Core and I am trying to get a web application to speak with a web API. Both require authentication using the [Authorize] attribute for all of their classes. To be able to talk server-server between them, I need to get a validation token. I was able to do this thanks to the Microsoft tutorial .

Problem

In the tutorial, they use the AcquireTokenByAuthorizationCodeAsync call to store the token in the cache, so in other places the code can simply make AcquireTokenSilentAsync , which does not require a transition to the authority to verify the user.

This method does not look for the token cache, but stores the result in it, so you can search for it using other methods, such as AcquireTokenSilentAsync

The problem occurs when the user is already logged in. The method stored in OpenIdConnectEvents.OnAuthorizationCodeReceived is never called because there is no authorization. This method is called only when there is a new entry.

There is another event: CookieAuthenticationEvents.OnValidatePrincipal , when a user is only verified with a cookie. This works, and I can get the token, but I have to use AcquireTokenAsync , since at this moment I do not have an authorization code. According to the documentation, he

Gets the security token from authority.

This causes the AcquireTokenSilentAsync call to fail because the token is not cached. And I would prefer not always to use AcquireTokenAsync , as this always applies to proxy.

Question

How can I specify the token obtained with AcquireTokenAsync for caching so that I can use AcquireTokenSilentAsync everywhere?

Relevant Code

All this comes from the Startup.cs file in the main web application project.


Here's how event handling is done:

 app.UseCookieAuthentication(new CookieAuthenticationOptions() { Events = new CookieAuthenticationEvents() { OnValidatePrincipal = OnValidatePrincipal, } }); app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions { ClientId = ClientId, Authority = Authority, PostLogoutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"], ResponseType = OpenIdConnectResponseType.CodeIdToken, CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"], GetClaimsFromUserInfoEndpoint = false, Events = new OpenIdConnectEvents() { OnRemoteFailure = OnAuthenticationFailed, OnAuthorizationCodeReceived = OnAuthorizationCodeReceived, } }); 

And these are the following events:

 private async Task OnValidatePrincipal(CookieValidatePrincipalContext context) { string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value; ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret); AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session)); AuthenticationResult authResult = await authContext.AcquireTokenAsync(ClientResourceId, clientCred); // How to store token in authResult? } private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context) { // Acquire a Token for the Graph API and cache it using ADAL. In the TodoListController, we'll use the cache to acquire a token to the Todo List API string userObjectId = (context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value; ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret); AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session)); AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync( context.ProtocolMessage.Code, new Uri(context.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]), clientCred, GraphResourceId); // Notify the OIDC middleware that we already took care of code redemption. context.HandleCodeRedemption(); } // Handle sign-in errors differently than generic errors. private Task OnAuthenticationFailed(FailureContext context) { context.HandleResponse(); context.Response.Redirect("/Home/Error?message=" + context.Failure.Message); return Task.FromResult(0); } 

Any other code can be found in the linked tutorial or asked, and I will add it to the question.

+5
source share
1 answer

(Note: I struggled with this exact issue for several days. I followed the same Microsoft tutorial as the one related to the question and tracked various issues such as wild goose hunting, it turns out that the sample contains a whole bunch of seemingly unnecessary steps when using the latest version of Microsoft.AspNetCore.Authentication.OpenIdConnect .).

In the end, I had a breakthrough when I read this page: http://docs.identityserver.io/en/release/quickstarts/5_hybrid_and_api_access.html

The solution essentially assumes that OpenID Connect auth places various tokens ( access_token , refresh_token ) in the cookie.

First, I use the Converged Application created at https://apps.dev.microsoft.com and the Azure AD Endpoint v2.0. The application has the application secret (password / public key) and uses Allow Implicit Flow for the web platform.

(For some reason, the v2.0 endpoint doesn't seem to work with Azure AD applications. I'm not sure why, and I'm not sure if that really matters.)

Corresponding lines from the Startup.Configure method:

  // Configure the OWIN pipeline to use cookie auth. app.UseCookieAuthentication(new CookieAuthenticationOptions()); // Configure the OWIN pipeline to use OpenID Connect auth. var openIdConnectOptions = new OpenIdConnectOptions { ClientId = "{Your-ClientId}", ClientSecret = "{Your-ClientSecret}", Authority = "http://login.microsoftonline.com/{Your-TenantId}/v2.0", ResponseType = OpenIdConnectResponseType.CodeIdToken, TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", }, GetClaimsFromUserInfoEndpoint = true, SaveTokens = true, }; openIdConnectOptions.Scope.Add("offline_access"); app.UseOpenIdConnectAuthentication(openIdConnectOptions); 

And this! No OpenIdConnectOptions.Event callbacks. There are no AcquireTokenAsync or AcquireTokenSilentAsync calls. No TokenCache . None of this seems necessary.

The magic seems to happen as part of OpenIdConnectOptions.SaveTokens = true

Here is an example when I use an access token to send email on behalf of a user using my Office365 account.

I have a WebAPI controller action that gets their access token using HttpContext.Authentication.GetTokenAsync("access_token") :

  [HttpGet] public async Task<IActionResult> Get() { var graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async requestMessage => { var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token"); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken); })); var message = new Message { Subject = "Hello", Body = new ItemBody { Content = "World", ContentType = BodyType.Text, }, ToRecipients = new[] { new Recipient { EmailAddress = new EmailAddress { Address = " email@address.com ", Name = "Somebody", } } }, }; var request = graphClient.Me.SendMail(message, true); await request.Request().PostAsync(); return Ok(); } 

Side Note # 1

At some point, you may also need to hold refresh_token if the access_token expires:

 HttpContext.Authentication.GetTokenAsync("refresh_token") 

Side Note # 2

My OpenIdConnectOptions includes a few more things that I have missed here, for example:

  openIdConnectOptions.Scope.Add("email"); openIdConnectOptions.Scope.Add("Mail.Send"); 

I used them to work with the Microsoft.Graph API to send email on behalf of the current user.

(These delegated permissions for Microsoft Graph are also configured in the application).


Update - how to "quietly" update Azure access token

So far, this answer explains how to use the access cache key, but not what needs to be done when the token expires (usually after 1 hour).

Possible options:

  • Force the user to log in again. (Not silent)
  • Send a request to Azure AD using refresh_token to get a new access_token (no sound).

How to update an access token using endpoint version 2.0

After even more searching, I found some of the answer in this SO question:

How to handle expired token in asp.net core using update token using OpenId Connect

It seems that the Microsoft OpenIdConnect libraries are not updating the access token for you. Unfortunately, the answer in the above question does not contain important information about the exact how to update the token; presumably because it depends on specific details about Azure AD that OpenIdConnect doesn't care about.

The accepted answer to the above question involves sending a request directly to the Azure AD Token REST API instead of using one of the Azure AD libraries.

The relevant documentation is here (Note: this applies to the combination of v1.0 and v2.0)

Here's a proxy based on API docs:

 public class AzureAdRefreshTokenProxy { private const string HostUrl = "https://login.microsoftonline.com/"; private const string TokenUrl = $"{Your-Tenant-Id}/oauth2/v2.0/token"; private const string ContentType = "application/x-www-form-urlencoded"; // "HttpClient is intended to be instantiated once and re-used throughout the life of an application." // - MSDN Docs: // https://msdn.microsoft.com/en-us/library/system.net.http.httpclient(v=vs.110).aspx private static readonly HttpClient Http = new HttpClient {BaseAddress = new Uri(HostUrl)}; public async Task<AzureAdTokenResponse> RefreshAccessTokenAsync(string refreshToken) { var body = $"client_id={Your-Client-Id}" + $"&refresh_token={refreshToken}" + "&grant_type=refresh_token" + $"&client_secret={Your-Client-Secret}"; var content = new StringContent(body, Encoding.UTF8, ContentType); using (var response = await Http.PostAsync(TokenUrl, content)) { var responseContent = await response.Content.ReadAsStringAsync(); return response.IsSuccessStatusCode ? JsonConvert.DeserializeObject<AzureAdTokenResponse>(responseContent) : throw new AzureAdTokenApiException( JsonConvert.DeserializeObject<AzureAdErrorResponse>(responseContent)); } } } 

The AzureAdTokenResponse and AzureAdErrorResponse classes used by JsonConvert :

 [JsonObject(MemberSerialization = MemberSerialization.OptIn)] public class AzureAdTokenResponse { [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "token_type", Required = Required.Default)] public string TokenType { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_in", Required = Required.Default)] public int ExpiresIn { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "expires_on", Required = Required.Default)] public string ExpiresOn { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "resource", Required = Required.Default)] public string Resource { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "access_token", Required = Required.Default)] public string AccessToken { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "refresh_token", Required = Required.Default)] public string RefreshToken { get; set; } } [JsonObject(MemberSerialization = MemberSerialization.OptIn)] public class AzureAdErrorResponse { [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error", Required = Required.Default)] public string Error { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_description", Required = Required.Default)] public string ErrorDescription { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "error_codes", Required = Required.Default)] public int[] ErrorCodes { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "timestamp", Required = Required.Default)] public string Timestamp { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "trace_id", Required = Required.Default)] public string TraceId { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "correlation_id", Required = Required.Default)] public string CorrelationId { get; set; } } public class AzureAdTokenApiException : Exception { public AzureAdErrorResponse Error { get; } public AzureAdTokenApiException(AzureAdErrorResponse error) : base($"{error.Error} {error.ErrorDescription}") { Error = error; } } 

Finally, my modifications to Startup.cs to update access_token (Based on the answer I linked above)

  // Configure the OWIN pipeline to use cookie auth. app.UseCookieAuthentication(new CookieAuthenticationOptions { Events = new CookieAuthenticationEvents { OnValidatePrincipal = OnValidatePrincipal }, }); 

The OnValidatePrincipal handler in Startup.cs (again from the related answer above):

  private async Task OnValidatePrincipal(CookieValidatePrincipalContext context) { if (context.Properties.Items.ContainsKey(".Token.expires_at")) { if (!DateTime.TryParse(context.Properties.Items[".Token.expires_at"], out var expiresAt)) { expiresAt = DateTime.Now; } if (expiresAt < DateTime.Now.AddMinutes(-5)) { var refreshToken = context.Properties.Items[".Token.refresh_token"]; var refreshTokenService = new AzureAdRefreshTokenService(); var response = await refreshTokenService.RefreshAccessTokenAsync(refreshToken); context.Properties.Items[".Token.access_token"] = response.AccessToken; context.Properties.Items[".Token.refresh_token"] = response.RefreshToken; context.Properties.Items[".Token.expires_at"] = DateTime.Now.AddSeconds(response.ExpiresIn).ToString(CultureInfo.InvariantCulture); context.ShouldRenew = true; } } } 

Finally, a solution with OpenIdConnect using the Azure AD API v2.0.

Interestingly, v2.0 does not seem to request a resource for inclusion in the API request; the documentation assumes this is necessary, but the API itself simply replies that the resource not supported. This is probably good. Presumably this means that the access token works for all resources (it certainly works with the Microsoft Graph API)

+3
source

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


All Articles