Recently, we had the need to deny External (also referred to as Guest) users to consume some endpoints of our Atlas API. You should be aware that if your Tenant has enabled external users, an external user can get a valid token and call your AAD secured API. This might or might not be what you want.
In our Atlas API, most of our endpoints allow external users calls, as after all, with Delegated permissions, any “on behalf of” call to other APIs, like MS Graph or SharePoint, will only allow you to do whatever you can do in there. However, we have some endpoints where we do not want to allow External users calls.
This is how we did it.
What type of token is it?
To find out if the token is coming from an external user, first we need to know if the token is a Delegated one or an Application permissions call.
There are 2 ways to do this:
- You can check if there is a “scp” claim in the Token. If so, this is a Delegated permissions token.
- Microsoft has documented another way here: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-verification-scope-app-roles?tabs=aspnetcore#accepting-app-only-tokens-if-the-web-api-should-be-called-only-by-daemon-apps
Basically, if there is a “oid” and “sub” claims in the token, and both values are equal, then it is an App call:
We are using the second option, as it appears to be the officially documented approach. However, if you go that route, you should be aware that the “sub” claim is mapped to the .net claim http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier.
In the Microsoft.Identity.Web library, there is an interesting method to return a claim from the ClaimsPrincipal. It allows you to pass multiple claim names, and it will return the value of the first claim found. The method is private, but you can copy the code:
We have created a ClaimsPrincipal extension method to check if it is an App call:
Is the token coming from an external user?
Now that we know if it is an Application or Delegated permissions call, let´s see how we can find out if the user is an external user (assuming for this example that it is a Delegated permissions token).
One of the easiest and most apparent options that comes to mind is doing an “on behalf of” call to MS Graph /me endpoint, and checking the “userType” field (Member / Guest). However, this requires another call to Graph, impacting performance (we are doing this when we want to know if a specific user, not the current one, is an external user). Ideally, we should have something in the current token telling us if the user is an external token… and luckily, it seems we have it.
According to the official documentation about the claims included in an Azure AD Access Token here: https://learn.microsoft.com/en-us/azure/active-directory/develop/access-token-claims-reference#payload-claims, we can see this about the “idp” claim:
In other words, if the “idp” claim value, is different than the “iss” claim value, it means the user is an external user.
However, as per our experience, there are a couple of gotchas here:
- The “idp” claim is not always present, but in that case, it just means the user is NOT an external one.
- When present, that claim is mapped to the .net Claim http://schemas.microsoft.com/identity/claims/identityprovider
Again, we can create a ClaimsPrincipal extension method to check this:
Now that we have these two extensions, we can check for external users in our endpoints, and deny calls from them:
Create a policy for this type of user
That´s cool, but we can go a bit further and make use of the Authorization framework built in .NET.
Let´s create a custom AuthorizationHandler. First, we need to create a Requirement:
Now, let´s add some code to the Handler:
Next, we need to register the Handler in our Dependency Injection container and configure a new Policy to deny external users:
In the code above, besides registering our custom handler, we are creating a couple of policies:
- The first one ensures any user is authenticated.
- The second one uses our custom handler that will deny external users, and ensure any user is authenticated.
This is to deal with the two scenario requirements: 1) any endpoint, must require that the user, is at least authenticated; and 2) some specific endpoint, besides user authenticated, will require that the user is not an external one.
As a fallback policy (the policy that will be used if no other one is required) we assign the “requireAuthUserPolicy”.
Finally, to apply the deny external users policy, we do the following:
And that´s all! You now have an elegant way to authorize your endpoints to non-external users only.
Hope it helps!