dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.54k stars 10.05k forks source link

Blazor WASM MSAL - Usable Azure App Proxy token not available to Blazor Server when consenting to multiple resources #52495

Closed Coruscate5 closed 8 months ago

Coruscate5 commented 11 months ago

Is there an existing issue for this?

Describe the bug

I have created a sample application as described here: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-7.0

The secured Hosted WebAPI now needs to use additional scopes to call OBO or consented permissions that are delegated on WebAPI2 (Azure Application Proxy, https://myproxy/user_impersonation), also in Azure.

The problem: The received token on the Hosted API always contains the wrong audience - we cannot use acquireTokenSilent on the token sent by WASM because it requires an additional consent for the additional scope with a different audience (WebAPI2).

This pattern is confirmed to work in Mobile applications (Xamarin) with custom Redirect URIs configured on the proxy. We have tried alternatives, client confidential applications, to authenticate on the back-end directly to the proxy, but the Entra pre-auth fails because it expects user interaction (MsalUiRequired).

We can see that the audience in the controller (WeatherForecastController), when secured by [Authorize], contains only the JWT scope and audience for the Server's App Registration, without the additional scopes required for Bearer authentication to the proxy object. Attempting to secure a new controller with authorization tied to the secondary WebAPI2 audience fails, likely because the server application is bound to a single ID:


builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))

There appears to be little/no documentation on this pattern, though I'd imagine it should be a common use case (Azure webapp calls for on-prem data from application proxy).

Expected Behavior

Hosted WebAPI should be able to receive the secondary token and retrieve additional API resource scopes silently. From what I understand, updates were made to WASM MSAL to obtain multiple tokens in sequence, but the binding context on front-end/back-end for Blazor under the Authorized component context only ever contains the DefaultScope.

Steps To Reproduce

  1. Follow steps in https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-7.0
  2. Attempt to use received token on Controller to authenticate to an Azure Application Proxy - always redirects to an additional sign-in (the user is already signed in on WASM)

Exceptions (if any)

No response

.NET Version

8.0.0

Anything else?

We could in theory call the Application Proxy directly from WASM, though of course CORS fails and this seems like a bizarre pattern. We simply would like to use tokens from WASM to call additional Azure resources from Blazor.Server.

Blazor WASM attempts to get multiple resource consent by doing the following - I can't tell if the additional scope is actually being acquired though:

options.ProviderOptions.DefaultAccessTokenScopes.Add("api://BlazorServerAppID/API.Access");
options.ProviderOptions.AdditionalScopesToConsent.Add("https://myproxy/user_impersonation");
Coruscate5 commented 11 months ago

After several days of trying different methods - the current workaround is to have ITokenAcquisition request a new user token on the controller using the App Proxy scope in a way I've never seen before:

GUID/user_impersonation

The expected Scopes all fail, e.g. api:// and https://, because apparently what Azure STS wants is the aud to be set to the AppID directly of the App Proxy App Registration. I'm unsure how this could have been figured out without extreme trial and error.

In any other configuration, the Proxy does not accept the token and redirects to the sign-in page

halter73 commented 8 months ago

We're glad you eventually found a workaround @Coruscate5.

We simply would like to use tokens from WASM to call additional Azure resources from Blazor.Server.

This is likely why you were running into issues. The guide at https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-7.0 demonstrates how to set up the client app and server app with different registrations and Client IDs. The WASM app is a Single-page application (SPA) and the guide tells you to register it as such. This causes the token to have a shorter lifetime than a token that is only accessible on the server.

Cross-site scripting (XSS) attacks or compromised JS packages can steal the refresh token and use it remotely until it expires or is revoked. Application developers are responsible for reducing their application's risk to cross-site scripting. In order to minimize the risk of stolen refresh tokens, SPAs are issued tokens valid for 24 hours only. After 24 hours, the app must acquire a new authorization code via a top-level frame visit to the login page.

Security implications of refresh tokens in the browser | Microsoft Learn

DO NOT send access tokens that were issued to the middle tier to any other party. Access tokens issued to the middle tier are intended for use only by that middle tier.

Security risks of relaying access tokens from a middle-tier resource to a client (instead of the client getting the access tokens themselves) include:

Increased risk of token interception over compromised SSL/TLS channels. Inability to satisfy token binding and Conditional Access scenarios requiring claim step-up (for example, MFA, Sign-in Frequency). Incompatibility with admin-configured device-based policies (for example, MDM, location-based policies).

Microsoft identity platform and OAuth2.0 On-Behalf-Of flow - Microsoft identity platform | Microsoft Learn

Coruscate5 commented 8 months ago

I attempted to resolve this through MSFT support to no avail - the core issue here is Proxy configuration, not the OBO flow (after investigation). You can reproduce this by creating any App Proxy and attempting to retrieve a valid token using the App URI that is automatically created (https://somesite/user_impersonation) and use it for a proxy call - it will always redirect programmatically back to the sign-in page (even using ConfidentialClient flow).

If using ConfidentialClient, the only scope that ends up working is GUID\.default (since the .default scope modifier is required for confidential flows)

The problem appears to be with STS acceptance of App Proxy's URI as a Scope. It does not accept the custom auto-created URI, but instead works using this undocumented fix.

Likely this is either a bug in the Azure App Proxy, or something that skipped documentation - the Support Team ended up wanting me to create a full sample of the reproduction which I couldn't commit to (and since this is Azure Proxy, that wouldn't even work on a remote build). Is there a more proper place to put this? I don't think Azure STS/App Proxy has a dedicated git repo.