Azure Function using "Easy Auth" for AuthN/Z is not returning role/group claims for endpoint authentication
I'm attempting to build an SSO prototype using an Azure Function web API and a react-based SPA connected to Azure AD. The goal is to use "Easy Auth" (aka Azure Function integrated authentication) for my authentication on the Azure Function (https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) with Microsoft Identity Platform as my provider.
First off, I created a React SPA using the following tutorial: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-react
This seemed to authenticate just fine and I was able to consume the sample Graph API call.
However, once I attempted to then add the Azure Function to the mix, I ran into a problem. I used the POST call for "Client-directed sign-in" (https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-customize-sign-in-out#client-directed-sign-in) to submit my AAD access token, but it failed.
I created a new button in the page that calls the following function:
export async function callExampleService(idToken, accessToken) {
const headers = new Headers();
headers.append("Content-Type", "application/json");
const options = {
method: 'POST',
headers: headers,
body: JSON.stringify({ access_token: `${accessToken}` })
};
return fetch(exampleDataServiceConfig.exampleDataServiceBase.concat(exampleDataServiceConfig.postAuth), options)
.then(response => response.json())
.catch(error => console.log(error));
}
I did find that my authentication seemed to work if I submitted the id token as the access token using the following change to the above code:
const options = {
method: 'POST',
headers: headers,
body: JSON.stringify({ access_token: `${idToken}` })
};
===
I was able to successfully authenticate on each endpoint in my test API without issue. However, I ran into an issue when integrating authorization on the endpoints.
According to the documentation (https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-user-identities#access-user-claims-in-app-code), I should use HttpRequest.HttpContext.User as my ClaimsPrincipal for identity and authorization logic in an Azure Function.
I created three app roles (Test.View, Test.Edit, and Test.Admin) within the App Registration to validate authorization. The user is only a member of Test.View.
When I added logging to the ClaimsPrincipal for debugging, the object returned the following:
ClaimsPrincipal.Identity is not null
ClaimsPrincipal.Identity.Name is empty
ClaimsPrincipal.Identity.IsAuthenticated() is true
ClaimsPrincipal.Identity.AuthenticationType is AuthenticationTypes.Federation
ClaimsPrincipal.IsInRole("Test.View") is false
ClaimsPrincipal.IsInRole("Test.Edit") is false
ClaimsPrincipal.IsInRole("Test.Admin") is false
I iterated over the claims and returned the following:
Type: stable_sid, Value: sid:<id>
Type: exp, Value: <date>
Type: http://schemas.microsoft.com/identity/claims/identityprovider, Value: aad
Type: ver, Value: 3
Type: iat, Value: <date>
Type: nbf, Value: <same date as iat>
Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier, Value: sid:<user id returned from AAD auth>
Type: iss, Value: <function url>
No claims seem to match any sort of role, as far as I can tell. I am also not sure how to find the "sid:" references in Azure AD.
The doc also mentions that the raw version of this information should be returned via the request headers, so I looked at them as well. Here are the "upper case" headers, which I believe are passed from the auth container:
X-ARR-LOG-ID | <guid>
X-ARR-SSL | 2048|256|<Microsoft cert info>
X-SITE-DEPLOYMENT-ID | <func name>
CLIENT-IP | <ip:port>
WAS-DEFAULT-HOSTNAME | <func url>
DISGUISED-HOST | <func url>
X-MS-CLIENT-PRINCIPAL-ID | sid:<user id returned from AAD auth>
X-MS-CLIENT-PRINCIPAL-IDP | aad
X-MS-CLIENT-PRINCIPAL | <JWT>
X-MS-CLIENT-AAD-ACCESS-TOKEN | <JWT ID token submitted by client>
The ClaimsPrincipal seems to be authenticated, but it doesn't seem to have a name or any roles/groups claims associated.
===
It should be noted that both the react client app and the azure function are both using the same Registered App credentials for authentication. I haven't found any details about whether that is the correct practice or that both need separate Registered Apps.
Here are a few related pieces of info to narrow down the problem:
- http://localhost:3000 is in the CORS for the function.
- http://localhost:3000 is in the Redirect URIs for the "Single Page Application" platform config of the app registration.
- https://<function-name>.azurewebsites.net/.auth/login/aad/callback is in the Redirect URIs for the "Web" platform confict of the app registration.
- <function-name> is just the placeholder for the actual name of the azure function, in case you think I actually used that for the name in the variables. ;)
- Both "Access tokens" and "ID tokens" are not checked in the "Implicit grant and hybrid flows" since we're using msal.js 2.0
- Test.Admin, Test.Edit, and Test.View are all allowed by Users/Groups and Applications in App Registration.
- Under "Expose an API" in App Registration, the only scope is "impersonate_user" for the app.
- Under "Toekn configuration" in App Registration, 2 optional claims are requested (for testing only): "groups" (ID, Access, SAML) and "preferred_username" (Access).
- The access_token and id_token from client authentication are both valid on jwt.io, but access_token is validated via 512SHA and 256SHA. Using access_token as my access token returns a 401 validation error from the Azure Function endpoint due to token validation error. The id_token works when being sent as the access token. Weird.
Since the auth is done in a black box container, I'm not sure what steps I can take to get specifics on this issue. I have the following questions:
1) Is there a way to locally test the easy auth scenario? I currently have to add debug traces to the app insights logs in order to debug this thing. I'd prefer to grab a breakpoint and dig into the issue directly.
2) What might be causing this lack of ClaimsPrincipal detail?
Any help would be appreciated.