dark-loop / functions-authorize

An ASP.NET Core based authentication and authorization middleware for HTTP triggered Azure Functions (In-Proc and Isolated)
Apache License 2.0
35 stars 4 forks source link

Authentication with MSAL Angular Access Token #69

Open Tweentyy opened 1 week ago

Tweentyy commented 1 week ago

Hello, I've set up a secure connection with Darkloop (DarkLoop.Azure.Functions.Authorization.Isolated). I have an Angular frontend and users connect with their Microsoft account (accounts have roles). Once connected I have the access token and the token id. When I make requests to the backend, the access token is added to the header. Once the request reaches the backend I get an error, I think I may have made a mistake in the configuration? I'm in .NET 8 isolated with Azure Functions v4

In the idToken I can see my user's roles [‘Admin’], in the access token they're not, which is where I can't make the link with RequireRole().

services.AddFunctionsAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtFunctionsBearer(options =>
            {
                options.Authority = "https://login.microsoftonline.com/tenantId";
                options.Audience = "audience";
            });
        services.AddFunctionsAuthorization(options =>
        {
            options.AddPolicy("OnlyAdmins", policy => policy.RequireRole(RoleConstant.GlobalAdmin));
        });

[16:21:02 INF] Failed to validate the token. [2024-09-06T14:21:02.724Z] Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10511: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey, KeyId: 'H9nj5AOSswMphg1SFx7jaV-lB9w', InternalId: 'H9nj5AOSswMphg1SFx7jaV-lB9w'. , KeyId: H9nj5AOSswMphg1SFx7jaV-lB9w [2024-09-06T14:21:02.724Z] '. [2024-09-06T14:21:02.724Z] Number of keys in TokenValidationParameters: '0'. [2024-09-06T14:21:02.724Z] Number of keys in Configuration: '12'. [2024-09-06T14:21:02.724Z] Matched key was in 'Configuration'. [2024-09-06T14:21:02.724Z] kid: 'H9nj5AOSswMphg1SFx7jaV-lB9w'. [2024-09-06T14:21:02.724Z] Exceptions caught: [2024-09-06T14:21:02.724Z] '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. [2024-09-06T14:21:02.724Z] token: '[PII of type 'Microsoft.IdentityModel.JsonWebTokens.JsonWebToken' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. See https://aka.ms/IDX10511 for details. [2024-09-06T14:21:02.724Z] at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) [2024-09-06T14:21:02.724Z] at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) [2024-09-06T14:21:02.724Z] at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateJWSAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)

Thanks :)

artmasa commented 1 week ago

Hi @Tweentyy, do you have an app registration for each application or are you using the same app registration for both? Based on your comments above I imagine you have different registrations due to the fact that the id token is the one carrying roles. Have you configured the registration for functions to return roles for users? Can you elaborate a little more on your app registration setup in EntraID. The ideal situation for functions as they are seen as backend APIs is to consume access tokens.

Tweentyy commented 1 week ago

Hello, thank you for your reply. I'm not available next week, but I'm already starting to make enquiries. I'll get back to you the following week as soon as I'm available. I didn't take care of this part

devrony commented 1 week ago

@Tweentyy - Just curious if you are using Static Web Apps in Azure to host the front-end Angular application and if you are also using the built-in Static Web Apps - Azure Function for making calls to the back-end (hence the reason for using library).

If so, what I have recently discovered is that Azure replaces the "Authorization" header bearer token from browser request (the one you see in Chrome Dev Tools or Fiddler) going to back-end with their own Proxy bearer token before it actually makes it to your Function App endpoints. See this Issue where @artmasa helped me setup my app to use a custom 'Authorization' header (X-Custom-Authorization). https://github.com/dark-loop/functions-authorize/issues/57

To confirm the bearer token is being replaced or not, I would recommend logging out the Authorization header in Function App log and compare with what is showing in Browser Dev Tools Header and see if it's being replaced.

Paste your bearer toke in this site to parse and see contents: https://jwt.ms/

NOTE: If this is the issue, I used an custom HttpInterceptor in Angular to automatically clone the Authorization header in browser to my own custom. Then use the Program.cs configuration from my Issue posted above to map over this custom header to be the one the Authorize attribute uses.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CustomAuthInterceptor implements HttpInterceptor {
  constructor(private router: Router) {

  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    // IMPORTANT: If this app is deployed as Static Web App and we
    // want server-side resources to have access to the real bearer token
    // for the authenticated user we need to store the authorization bearer token in
    // our own custom request header because a proxy may be placed in between
    // this client app and the back-end Function App API (depending on setup) and the 
    // proxy injects/overrides the current authorization header with own bearer token.
    const authHeader = req.headers.get('Authorization');
    if (authHeader) {

      // Clone the request and set the custom header
      const customReq = req.clone({
        setHeaders: {
          'X-Custom-Authorization': authHeader,
        },
      });
    }

    // If no authorization header is found, pass the original request
    return next.handle(req);
  }
}

Also, wanted to make sure that based on your code sample posted. Have you tried this authority instead?

https://login.microsoftonline.com/common/v2.0 <-- if your App Registration is setup for v2 tokens

or

https://login.microsoftonline.com/common

I just went through this setup because my app is setup a Multitenant in the App Registration (I use /organizations though because I'm not supporting MS Accounts). Here is my latest Issue I just got working where I needed to support multiple tenants. https://github.com/dark-loop/functions-authorize/issues/68

Tweentyy commented 10 hours ago

@Tweentyy - Just curious if you are using Static Web Apps in Azure to host the front-end Angular application and if you are also using the built-in Static Web Apps - Azure Function for making calls to the back-end (hence the reason for using library).

If so, what I have recently discovered is that Azure replaces the "Authorization" header bearer token from browser request (the one you see in Chrome Dev Tools or Fiddler) going to back-end with their own Proxy bearer token before it actually makes it to your Function App endpoints. See this Issue where @artmasa helped me setup my app to use a custom 'Authorization' header (X-Custom-Authorization). #57

To confirm the bearer token is being replaced or not, I would recommend logging out the Authorization header in Function App log and compare with what is showing in Browser Dev Tools Header and see if it's being replaced.

Paste your bearer toke in this site to parse and see contents: https://jwt.ms/

NOTE: If this is the issue, I used an custom HttpInterceptor in Angular to automatically clone the Authorization header in browser to my own custom. Then use the Program.cs configuration from my Issue posted above to map over this custom header to be the one the Authorize attribute uses.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CustomAuthInterceptor implements HttpInterceptor {
  constructor(private router: Router) {

  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    // IMPORTANT: If this app is deployed as Static Web App and we
    // want server-side resources to have access to the real bearer token
    // for the authenticated user we need to store the authorization bearer token in
    // our own custom request header because a proxy may be placed in between
    // this client app and the back-end Function App API (depending on setup) and the 
    // proxy injects/overrides the current authorization header with own bearer token.
    const authHeader = req.headers.get('Authorization');
    if (authHeader) {

      // Clone the request and set the custom header
      const customReq = req.clone({
        setHeaders: {
          'X-Custom-Authorization': authHeader,
        },
      });
    }

    // If no authorization header is found, pass the original request
    return next.handle(req);
  }
}

Also, wanted to make sure that based on your code sample posted. Have you tried this authority instead?

https://login.microsoftonline.com/common/v2.0 <-- if your App Registration is setup for v2 tokens

or

https://login.microsoftonline.com/common

I just went through this setup because my app is setup a Multitenant in the App Registration (I use /organizations though because I'm not supporting MS Accounts). Here is my latest Issue I just got working where I needed to support multiple tenants. #68

Hello,

I see that the access token sent is different and a proxy seems to be applied. My access token still doesn't contain the roles in the claims, I think I've got a scope or permission problem instead.

Tweentyy commented 8 hours ago

Hi @Tweentyy, do you have an app registration for each application or are you using the same app registration for both? Based on your comments above I imagine you have different registrations due to the fact that the id token is the one carrying roles. Have you configured the registration for functions to return roles for users? Can you elaborate a little more on your app registration setup in EntraID. The ideal situation for functions as they are seen as backend APIs is to consume access tokens.

Hello,

From what I can see, I have an App Registrations that manages all the roles. For example, I have the "Test.Admin" role in App roles. It is used as SSO. On the App Registrations side, which manages the frontend, there is no special configuration. Lastly, Azure Functions seem to be a simple Resource with no specific configuration required.

On my side, on the frontend with the MSAL Angular module, the user logs in on the SSO side (I get the roles from the ID token but not the access token). In the Azure Functions code, I make the connection with the SSO as shown above in the code extract I read in the Microsoft documentation that the Apps had the roles in the ID token whereas the APIs had them in the Access Token.

When I send the token, I send an access token of type "App", so it doesn't contain the roles because they are in the ID token.