Microsoft-identity-web: How can I call ITokenAcquisition.GetAccessTokenForUserAsync from startup.cs

Created on 16 Aug 2020  路  13Comments  路  Source: AzureAD/microsoft-identity-web

Which version of Microsoft Identity Web are you using?
Microsoft Identity Web 0.2.3-preview

Where is the issue?

  • Web app

    • [ ] Sign-in users

    • [x] Sign-in users and call web APIs

  • Web API

    • [ ] Protected web APIs (validating tokens)

    • [ ] Protected web APIs (validating scopes)

    • [ ] Protected web APIs call downstream web APIs

  • Token cache serialization

    • [ ] In-memory caches

    • [ ] Session caches

    • [x] Distributed caches

  • Other (please describe)

Is this a new or an existing app?
a. The app is in production and I have upgraded to a new version of Microsoft Identity Web.

Repro

I'm trying to upgrade one of the projects I'm working on to use the Microsoft.Identity.Web nuget package. So far really working well but I'm having trouble figuring out how to add additional claims which I was previously doing by the following:

        services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
        {
            options.Events.OnAuthorizationCodeReceived = async ctx =>
            {
                var serviceProvider = services.BuildServiceProvider();
                var distributedCache = serviceProvider.GetRequiredService<IDistributedCache>();

                var identifier = ctx.Principal.FindFirst(ObjectIdentifierType)?.Value;

                var cca = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(new ConfidentialClientApplicationOptions()
                {
                    ClientId = "ClientId",
                    RedirectUri = "RedirectUri",
                    ClientSecret = "ClientSecret"
                })
                .WithAuthority(ctx.Options.Authority)
                .Build();

                var tokenCache = new SessionTokenCache(identifier, distributedCache);
                tokenCache.Initialize(cca.UserTokenCache);
                var token = await cca.AcquireTokenByAuthorizationCode(scopes, ctx.TokenEndpointRequest.Code).ExecuteAsync();

                ctx.HandleCodeRedemption(token.AccessToken, token.IdToken);

                // get the claims
                var claimService = serviceProvider.GetRequiredService<ClaimService>();
                var response = await apiClient.GetUserAdditionalClaimsAsync(token.AccessToken);

                // add the claims
            };

Now when I try to use the ITokenAcquisition.GetAccessTokenForUserAsync() method instead of using the ConfidentialClientApplicationBuilder

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftWebApp(options =>
    {
        Configuration.Bind("AzureAD", options);
        options.Events.OnAuthorizationCodeReceived = async ctx =>
        {
            var serviceProvider = services.BuildServiceProvider();
            var tokenAcquisition = serviceProvider.GetRequiredService<ITokenAcquisition>();

            var token = await tokenAcquisition.GetAccessTokenForUserAsync("scopes");

            // get the claims
            var claimService = serviceProvider.GetRequiredService<ClaimService>();
            var response = await apiClient.GetUserAdditionalClaimsAsync(token);

            // add the claims
        };
    })
    .AddMicrosoftWebAppCallsWebApi(Configuration, new[] { "scopes" })
    .AddDistributedTokenCaches();

I get the following error:
enter image description here

Any help would be so much appreciated on how best to handle this.
Thanks!

Answered investigate question

All 13 comments

@mia01

Would the following work for you?


       services.AddMicrosoftWebApp(Configuration)
                    .AddMicrosoftWebAppCallsWebApi(Configuration, new string[] { "scopes", "you", "need" ))
                    .AddMemoryTokenCaches();

        services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            var previousEventHandler = options.Events.OnAuthorizationCodeReceived;
            options.Events.OnAuthorizationCodeReceived = async ctx =>
            {
             // Ensure the Auth code is processed and the cache populated
             await previousEventHandler(ctx);

            // Get the token acquisition service here
           var tokenAcquistion = serviceProvider.GetRequiredService<ITokenAcquisition>();
           var token = await tokenAcquisition.GetAccessTokenForUserAsync("scopes");
         }
       }

@jmprieur thanks for that snippet. Looks like it could work IIUC!
I'll give it a go...

@jmprieur I've tried this yet still getting the exact same error, unfortunately :(
I can see that the cache is populated with the token but still the call fails

@mia01 : I'm trying to understand your scenario. Why do you want to acquire a token from the Startup.cs, and not from the controller actions / Razor/Blazor pages ?

I'm also having the same issue/error and have similar use case. I want to make a call to Graph service to retrieve user membership so that I can add Group names as claims.

@sameer-kumar I see from this issue that you issue was resolved related to this?

@mia01 : I'm trying to understand your scenario. Why do you want to acquire a token from the Startup.cs, and not from the controller actions / Razor/Blazor pages ?

@jmprieur we are trying to add a bunch of custom claims to the user. We want to add this once when user is authenticated. We determine what claims the user by calling Graph Api which means we need the token at that point.

@sameer-kumar I'm interested what was the code that worked for you?

@pmaytak yes, my use case is solved.
@mia01 I just followed along this with my custom modifications specific to my use case and it works for me.

Closing as it's answered.
See also this code (and the called method) which does it: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/b07a9e06206f7274fdcadc34a50b8bebf9666fcf/5-WebApp-AuthZ/5-2-Groups/Startup.cs#L51

Feel free to reopen if you disagree

We'd like to be able to centralise the configuration of our HttpClient in Startup.cs. So instead of

services.AddHttpClient<IMyService, MyService>();

We are moving to something like this,

services.AddHttpClient(nameof(MyService), (hc) =>
            {
                string apiBaseAddress = this.configuration["MyApi:BaseAddress"];
                hc.BaseAddress = new Uri(apiBaseAddress);
                hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            });

That works fine but if we wanted to move the following in there too

var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { this.configuration["ClinicianApi:Scopes"] });
hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

We'd need access to ITokenAcquisition in Startup. There are a few references that suggest calling

var serviceProvider = services.BuildServiceProvider();
var tokenAcquisition = serviceProvider.GetRequiredService<ITokenAcquisition>()

However if you do that you get a warning when you build, "Avoid calls to BuildServiceProvider in ConfigureServices"

Therefore we would still like to know "How can I call ITokenAcquisition.GetAccessTokenForUserAsync from startup.cs"

Updating the proposed solution: @mia01 @munkii


       services.AddMicrosoftWebApp(Configuration)
                    .AddMicrosoftWebAppCallsWebApi(Configuration, new string[] { "scopes", "you", "need" ))
                    .AddMemoryTokenCaches();
       services.AddMicrosoftIdentityConsentHandler();

        services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            var previousEventHandler = options.Events.OnAuthorizationCodeReceived;
            options.Events.OnAuthorizationCodeReceived = async ctx =>
            {
             // Ensure the Auth code is processed and the cache populated
             await previousEventHandler(ctx);

            // Get the token acquisition service here
           var tokenAcquistion = serviceProvider.GetRequiredService<ITokenAcquisition>();
           var consentHandler = serviceProvider.GetRequiredService<MicrosoftIdentityConsentAndConditionalAccessHandler>(); 

           try
           { 
            var token = await tokenAcquisition.GetAccessTokenForUserAsync("scopes");
           }
          catch (Exception ex)
          {
            consentHandler .HandleException(ex);
          }
         }
       }
Was this page helpful?
0 / 5 - 0 ratings