This is the seventh and final part of a series of blog posts related to Azure AD best practices. They are all related to a talk I gave at Tech Days Finland as well as in the Microsoft Identity Developer Community Office Hours.

For the last post, we'll discuss a very important topic: proper Azure AD token validation.

One very important check is usually not done by standard libraries, which makes it something that can be easy to miss when developing an API.

Checking permissions in the token

What do we mean by check permissions? Well, in Azure AD you can define delegated and application permissions for an API, that can in turn be required by other applications. This allows you to control what applications can do with the API.

You can check my earlier article if you want to know how to define permissions: https://joonasw.net/view/defining-permissions-and-roles-in-aad. The manifest has changed slightly since I wrote the article, but it can still be applied. You can also define delegated permissions through the Azure Portal now.

We'll go through checking permissions in practice with an ASP.NET Core application. The basic ideas can be applied to any language and framework though. You can find the sample API on GitHub:

This app has the following permissions defined:

  • Application permissions
    • Employees.Read.All
  • Delegated permissions
    • Employees.Read

When an app has required a delegated permission and received consent for it, it appears in the access token's scp claim:

{
    "scp": "Employees.Read"
}

Application permissions on the other hand arrive in the roles claim:

{
    "roles": [ "Employees.Read.All" ]
}

A big thing to note is that delegated permissions are submitted as one string, space-delimited. Application permissions are submitted as an array. In ASP.NET Core, checking for application permissions is easy because of this, as each value becomes its own claim. Checking delegated permissions is a bit awkward since they are all in one string.

To make checking for delegated permissions easier, we can use a claims transformation like this:

public class ScopeSplitClaimTransformation : IClaimsTransformation
{
    private const string ScopeClaimType = "http://schemas.microsoft.com/identity/claims/scope";

    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var scopeClaims = principal.FindAll(ScopeClaimType).ToArray();
        if (scopeClaims.Length != 1 || !scopeClaims[0].Value.Contains(' '))
        {
            // No need to split
            return Task.FromResult(principal);
        }

        var claim = scopeClaims[0];
        var scopes = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        var claims = scopes.Select(s => new Claim(ScopeClaimType, s));

        return Task.FromResult(new ClaimsPrincipal(new ClaimsIdentity(principal.Identity, claims)));
    }
}

This app only declares one delegated permission, but if a caller had more, we'd get one claim like:

{
    "scp": "Employees.Read Customers.Read"
}

This transformation splits it into two claims and adds them to the user. So then we have essentially:

{
    "scp": [
        "Employees.Read Customers.Read",
        "Employees.Read",
        "Customers.Read"
    ]
}

These will be three separate claims on the ClaimsPrincipal. It's a lot easier to do claims checks, and we can do the check similarly for both delegated and application permissions. Note that this transformation is done only in-memory and won't actually modify the token. (We can't modify the tokens since they are digitally signed and we don't have the key)

Now to prevent the attack we will discuss, we have to check that a token contains at least one valid permission. To do that, we can implement an authorization requirement:

public class AnyValidScopeRequirement : IAuthorizationRequirement
{
}

And a handler for the requirement:

public class AnyValidScopeHandler : AuthorizationHandler<AnyValidScopeRequirement>
{
    private const string ScopeClaimType = "http://schemas.microsoft.com/identity/claims/scope";

    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        AnyValidScopeRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == ScopeClaimType)
            && context.User.FindAll(ScopeClaimType)
                .Any(scope => DelegatedPermissions.All.Contains(scope.Value)))
        {
            // Caller has valid delegated permission
            context.Succeed(requirement);
        }
        else if (context.User.HasClaim(c => c.Type == ClaimTypes.Role)
            && context.User.FindAll(ClaimTypes.Role)
                .Any(role => ApplicationPermissions.All.Contains(role.Value)))
        {
            // Caller has valid app permission
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

There are two static classes DelegatedPermissions and ApplicationPermissions that contain an All property which is an array that contains all the valid values for permissions.

Then we need to register the authorization handler and claim transformation in the service collection and setup a default authorization policy:

services.AddAuthorization(o =>
{
    o.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddRequirements(new AnyValidScopeRequirement())
        .Build();
});
services.AddSingleton<IAuthorizationHandler, AnyValidScopeHandler>();
services.AddSingleton<IClaimsTransformation, ScopeSplitClaimTransformation>();

We also need to require the default authorization policy across the whole API:

services.AddMvc(o =>
{
    o.Filters.Add(new AuthorizeFilter());
});

Now all callers are required to have been granted at least one delegated or application permission. You can also define authorization policies that require specific permissions for specific endpoints.

If you don't want your API to be callable by applications without a user context, then you can remove the application permission check. Then a caller must have a valid delegated permission.

Attacking an API that does not check permissions

This attack requires knowledge of two things:

  • Id of the target tenant
    • This is usually easy to find from any client app or by finding the tenant metadata with the organization's domain name
  • Client id / App ID URI of the API
    • Can be easy to find, if the API is used from a public client
    • If the API client id / App ID URI is only ever passed to Azure AD from back-end clients, this can even be impossible. But it would be a bad idea to rely on obscurity.

Let's say we have a single-tenant API registered in Azure AD tenant A. We also have a line-of-business client app in tenant A that uses the API.

We will register an app in Azure AD tenant B and create a client secret for it.

Then we will request a token with the following parameters:

  • Token endpoint: https://login.microsoftonline.com/{tenant-a-id}/oauth2/token
  • Grant type: Client credentials
  • Client id: Id of the app we registered in tenant B
  • Client secret: Secret for the same app
  • Resource: Client id / App ID URI of the API in tenant A

But wait. We are asking Azure AD to give our app in tenant B a token for a single-tenant API in tenant A. You would not expect this to work. But it does.

We get an access token that is for all intents and purposes valid. The app in tenant B is given a token that contains claims like this:

  • Audience: id of API in tenant A
  • Issuer: Tenant A
  • No object id claim
  • Tenant id: Tenant A

The only things missing from the token are permissions and an object id. The app in tenant B cannot get permissions to the API. (Since there is no service principal for the API in tenant B) It would only get an object id in the token if it had a service principal in tenant A.

And that's why you have to check for permissions. It is not enough to check that the issuer, audience, and signature are correct and that the token is not expired.

You can see how the vulnerable API is configured here: https://github.com/juunas11/7-deadly-sins-in-azure-ad-app-development/blob/d602be4c0981d459ebcde82b155be66919109a05/CheckingScopesInApi/EmployeeApi/Startup.cs#L28-L40. There are no additional authorization checks, so the sample app is able to grab all of the data from this API using the code here: https://github.com/juunas11/7-deadly-sins-in-azure-ad-app-development/blob/d602be4c0981d459ebcde82b155be66919109a05/DataStealingApp/DataStealingApp/Controllers/HomeController.cs#L48-L72.

Summary

If you have an API protected by Azure AD, you must check token permissions in addition to all the standard token validation. An app in another tenant can with quite minimal info acquire an otherwise valid access token for your API from your tenant. The only thing they cannot have are permissions to your API in your tenant.

Check your APIs and make sure they check for permissions in tokens. If they don't, make it a priority to define permissions for the API and require them to be assigned to client apps.

Links