AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
682 stars 214 forks source link

Calling ITokenAcquisition.GetAccessTokenForUserAsync throws IDW10503 error #1828

Closed FelipeCostaGualberto closed 2 years ago

FelipeCostaGualberto commented 2 years ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

1.25.1

Web app

Sign-in users and call web APIs

Web API

Protected web APIs call downstream web APIs

Token cache serialization

In-memory caches

Description

I get the error IDW10503 when I try using ITokenAcquisition.GetAccessTokenForUserAsync method in my controller. I didn't find any documentation about this error. If I downgrade the version of Identity package, it works (quite) as expected.

The architecture of my solution is: -Blazor WebAssembly hosted with Asp.Net 6. -Azure OpenId Connect (Microsoft Identity) to authenticate. -JWT is the default Authenticate Scheme. -All using latest stable version.

In the real scenario, my application support both Oidc and User/Password authentication, that's why I need both schemes.

Reproduction steps

There is a repro here: https://github.com/FelipeCostaGualberto/bug-obo-token

First, create an Azure Application with scopes https://analysis.windows.net/powerbi/api/.default offline_access. Then, set the values in the appSettings.json and run ExampleObo.Server project.

With the web application provided running, click in the Login button. Enter your credentials. You'll get back to the home page. Then, click Get Token button.

Error message

When I click in the button mentioned above, I get the error: IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'Bearer' as the authentication scheme. Available authentication schemes are 'Bearer,Cookies,OpenIdConnect'. See https://aka.ms/id-web/authSchemes.

The link provided by this error message seems to not exist.

Id Web logs

No response

Relevant code snippets

There is a repro here: https://github.com/FelipeCostaGualberto/bug-obo-token

The most important files in this repo are ExampleObo.Server\Program.cs where I set up services and ExampleObo.Server\Controller\IndexController.cs where I have this method to get the desired token:

public async Task<RequestResult> GetToken()
    {
        var result = new RequestResult();

        try
        {
            var scopes = new List<string>(_configuration["AzureAd:Scopes"].Split(new char[] { ' ' }));
            var token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
            result.Message = token;
            result.Success = true;
        }
        catch (Exception ex)
        {
            result.Message = ex.Message + " --- " + ex.InnerException;
        }

        return result;
    }

Also ExampleObo.Server\Controller\AccountController.cs Is important. Notice that I'm storing user's Email, Utid and Uid to call ITokenAcquisition.GetAccessTokenForUserAsync.

Regression

1.16.0

Expected behavior

The expected behavior when clicking on Get Token is to get the token generated by ITokenAcquisition.GetAccessTokenForUserAsync in the body of the page in green color.

As you can see, I'm using both packages below:

<PackageReference Include="Microsoft.Identity.Web" Version="1.25.1" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.25.1" />

If I downgrade to 1.16.0 both packages, clear cookies (this is important!) and the login again, the Get Token will work.

Even with this workaround, I'm facing another problem: after some hours with the application running, it seems the Azure Oidc token saved in the token service expires and I begin to get the MsalUiRequiredException exception when I click on the Get Token button again. I need the token to get refreshed because there is a DAX (Power BI) running every 48h on behalf of each user that logins in our website

Additional info: the last version that it works is 1.16.0. I tried using 1.16.1 and I also get an error, but an error different from 1.25.1.

jmprieur commented 2 years ago

@FelipeCostaGualberto

For more details see: https://github.com/AzureAD/microsoft-identity-web/wiki/multiple-authentication-schemes

PS: we have added this error, because it's very important that developers use the right authentication scheme (to get security)

FelipeCostaGualberto commented 2 years ago

Hello @jmprieur , thanks for the fast answer. I feel like I'm miles away to understand what is happening. I'll try to answer your questions, maybe some statements here will be incorrect.

jmprieur commented 2 years ago

@FelipeCostaGualberto

FelipeCostaGualberto commented 2 years ago

@jmprieur you said I was correctly using AddMicrosoftIdentityWebApp, and also that I just should use AddMicrosoftIdentityWebApi. Can you clarify that?

Either way, if I remove my AddJwtBearer how will I define this?

IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Jwt:SecurityKey"])),

This is important, because as you can see, Bearer is my default scheme and my IndexController is decored with [Authorize]. If I don't define what is my JWT key in the server, which key the server will use to parse the Bearer token from incoming requests?

jmprieur commented 2 years ago

You don't need to provide the issuer signing keys, @FelipeCostaGualberto. Microsoft.Identity.Web / ASP.NET Core / Microsoft.IdentityModel will discover them for you. They are part of the OAuth2.0 well known metadata.

You should not even provide them as IdentityModel ensures the resilience of the app vs outages of the IdP, which you don't get if you manage your keys yourself.

FelipeCostaGualberto commented 2 years ago

@jmprieur If I remove this code from the initialization:

.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        NameClaimType = "sub",
        RoleClaimType = "role",
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Jwt:SecurityKey"])),
        ValidateIssuer = false,
        ValidateAudience = false,
        ClockSkew = TimeSpan.Zero,
    };
})

It'll get like this:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options => { configuration.Bind("AzureAd", options); options.SaveTokens = true; })
    .EnableTokenAcquisitionToCallDownstreamApi(scopes)
    .AddInMemoryTokenCaches();

Cleared the cookies. Ran the project, clicked on Login button, all ok. But when I click on Get Token button, I get error 500 with message:

System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).
   at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
   at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

I would guess that the server doesn't know how to decode my Bearer tokens created at CreateToken in my AccountController.cs file.

What am I'm missing here? Did you run my repro?

FelipeCostaGualberto commented 2 years ago

I updated the repro GitHub project, please fetch the last version.

I have an update. I could make it run and get token successfully with this:

builder.Services
    .AddMicrosoftIdentityWebAppAuthentication(configuration)
    .EnableTokenAcquisitionToCallDownstreamApi(scopes)
    .AddInMemoryTokenCaches();

But I have a custom claim and a button Get Custom Claim that when clicked, was expected to get its value. Only the Microsoft Identity's claims are available in my controller and now my Bearer authentication header makes no difference when calling the controllers.

How can I get my custom claims using a token created by my server?

Thanks!

FelipeCostaGualberto commented 2 years ago

I think I figured out after a lot of trial and error. I found it quite complicated and was almost quitting and implementing my own ITokenAcquisition.

This is how I registered my services:

// This must be first
builder.Services
    .AddMicrosoftIdentityWebAppAuthentication(configuration)
    .EnableTokenAcquisitionToCallDownstreamApi(scopes)
    .AddInMemoryTokenCaches();

// Then, register JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            NameClaimType = "sub",
            RoleClaimType = "role",
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Jwt:SecurityKey"])),
            ValidateIssuer = false,
            ValidateAudience = false,
            ClockSkew = TimeSpan.Zero,
        };
    });

In you controller, if you use your ITokenAcquisition like this:

var token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);

, you'll get the error IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'Bearer' as the authentication scheme. Available authentication schemes are 'Cookies,OpenIdConnect,Bearer'. See https://aka.ms/id-web/authSchemes

The correct way to make the call above is:

var token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes: scopes, authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme);

With that, this problem is solved.

@jmprieur If my solution is alright, we can close this topic. Is this solution ok? Also, there is something to fix in the documentation: the link https://aka.ms/id-web/authSchemes shown in the error IDW10503 doesn't seem to exist.

jmprieur commented 2 years ago

@FelipeCostaGualberto yes, your solution is right. I had not understood that your web app part was creating a token (not AAD). This makes sense. Since this is the web app, the authentication scheme should be OpenIdConnect indeed.

Thanks for the heads-up for the broken aka.ms link. I just fixed it: https://aka.ms/id-web/authSchemes I also added more info about the error and how to fix it.

Thanks again, and sorry for the pain!