kmcconnell
kmcconnell

Reputation: 351

How to exchange authorization code for access_token using Azure AD Authentication and OpenIdConnect in .NET Core 2.1?

I'm new to multi-tenant apps and have been googling for a couple days on how to get the access_token natively in .NET Core 2.1. So far everything I've found is 2.0 or earlier and none of the methods posted even exist in 2.1.

I've create a Microsoft Graph helper that takes a string access_token and will fetch the user details. I'm simply trying to invoke the helper with an access_token after receiving the authorization code (OnAuthorizationCodeReceived event).

I feel like this should be a one-liner or short snippet at most, and I just can't seem to find a solution.

Here's my Azure AD extension where I'm wanting this to occur:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Authentication
{
    public static class AzureAdAuthenticationBuilderExtensions
    {        
        public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
            => builder.AddAzureAd(_ => { });

        public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
            builder.AddOpenIdConnect();
            return builder;
        }

        private class ConfigureAzureOptions: IConfigureNamedOptions<OpenIdConnectOptions>
        {
            private readonly AzureAdOptions _azureOptions;

            public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
            {
                _azureOptions = azureOptions.Value;
            }

            public void Configure(string name, OpenIdConnectOptions options)
            {
                options.ClientId = _azureOptions.ClientId;
                options.Authority = $"{_azureOptions.Instance}";
                options.UseTokenLifetime = true;
                options.CallbackPath = _azureOptions.CallbackPath;
                options.RequireHttpsMetadata = true;
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.TokenValidationParameters.ValidateIssuer = true;
                options.TokenValidationParameters.IssuerValidator = ValidateIssuer;
                options.Events.OnAuthenticationFailed = AuthenticationFailed;
                options.Events.OnAuthorizationCodeReceived = AuthorizationCodeReceived;
            }

            // TODO check tenant against database for authorized tenants
            private string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
            {
                if (false)
                {
                    //throw new SecurityTokenInvalidIssuerException();

                    // how do i get my db context here if there's no context in the scope?
                    // var db = context.HttpContext.RequestServices.GetRequiredService<RdmsContext>(); <-- something like this
                }

                // allowed
                return issuer;
            }

            private static Task AuthenticationFailed(
                AuthenticationFailedContext context)
            {
                context.HandleResponse();

                string message = Uri.EscapeUriString(context.Exception.Message);
                context.Response.Redirect($"/Home/Error?message={message}");
                return Task.CompletedTask;
            }

            private static async Task AuthorizationCodeReceived(
                AuthorizationCodeReceivedContext context)
            {
                string authorizationCode = context.ProtocolMessage.Code;
                string idToken = context.ProtocolMessage.IdToken;

                // ProtocolMessage has AccessToken property, but it's null.

                // Exchange authorization code for access_token here
                string accessToken = ...

                var userDetails = MyProject.Helpers.Graph
                    .GetUserDetailsAsync(accessToken);

                context.HandleCodeRedemption(accessToken, idToken);
            }

            public void Configure(OpenIdConnectOptions options)
            {
                Configure(Options.DefaultName, options);
            }
        }
    }
}

Upvotes: 1

Views: 1873

Answers (2)

Isaac Ojeda
Isaac Ojeda

Reputation: 325

Thank you so much, I was wondering how to get access_token for microsoft graph but i'm using Microsoft.AspNetCore.Authentication.AzureAD.UI (which abstracts all the OpenId configuration) and i think that options.UseTokenLifetime = true and options.SaveTokens = true; helped to access to context.ProtocolMessage.AccessToken

Thank you again

Upvotes: 0

kmcconnell
kmcconnell

Reputation: 351

After lots of trial and error, I finally figured out the missing pieces.

First, I changed options.ResponseType to OpenIdConnectResponseType.IdTokenToken which as I understand it returns an IdToken as well as a Token (access token). This requires supplying a resource for which the access token will be used.

So, I've also added options.Resource with a value of "https://graph.microsoft.com".

I also removed options.GetClaimsFromUserInfoEndpoint = true;

I also had to update the application's manifest in Azure to change oauth2AllowImplicitFlow to true.

Finally I replaced the OnAuthorizationCodeRecevied event with OnTokenValidated as the point from which I call my Microsoft Graph helper.

This combination of changes resulted in successfully receiving the access token which I could then feed to my Microsoft Graph helper and get what I need.

The final Configure method now looks like this:

public void Configure(string name, OpenIdConnectOptions options)
{
    options.ClientId = _azureOptions.ClientId;
    options.Resource = "https://graph.microsoft.com";
    options.Authority = $"{_azureOptions.Instance}";
    options.UseTokenLifetime = true;
    options.CallbackPath = _azureOptions.CallbackPath;
    options.RequireHttpsMetadata = true;
    options.ResponseType = OpenIdConnectResponseType.IdTokenToken;
    options.SaveTokens = true;
    options.TokenValidationParameters.ValidateIssuer = true;
    options.TokenValidationParameters.IssuerValidator = ValidateIssuer;
    options.Events.OnAuthenticationFailed = AuthenticationFailed;
    options.Events.OnTokenValidated = TokenValidatedAsync;
}

And TokenValidatedAsync now has the access token found in TokenValidatedContext.ProtocolMessage.AccessToken:

private static async Task TokenValidatedAsync(
    TokenValidatedContext context)
{
    string accessToken = context.ProtocolMessage.AccessToken;
    Graph.User userDetails = await MyProject.Helpers.Graph
        .GetUserDetailsAsync(accessToken);
}

From here I can do what I need with the Microsoft Graph user details.

I couldn't find any working example of this so I'll leave this here for future reference.

Upvotes: 3

Related Questions