In the first part of this article we:

  • Registered an API and a client app in Azure AD
  • Created a basic ASP.NET Core API and added Azure AD authentication
  • Created a test client app that calls the API

You can find the first part here: Azure AD Authentication in ASP.NET Core APIs part 1.

This time we will look at some more topics that are important when defining APIs:

  • Custom delegated and application permissions
  • Multi-tenancy

You can find the sample app at GitHub: Joonasw.AzureAdApiSample. Note that it has been updated significantly from part 1; for example it now uses Entity Framework Core with an in-memory database provider with global query filters.

Adding custom delegated permissions

First we will look at adding more delegated permissions to our API. Remember how I mentioned in part 1 that:

Note that we require the caller to have the user_impersonation scope. This is the default delegated permission that exists in every Web app/API in Azure AD.

So there is one delegated permission that exists by default (Access App Name/user_impersonation).

Naturally we can add more. In our sample API, we want to add two new permissions:

  1. Read user's todo items (Todo.Read)
  2. Read/Write user's todo items (Todo.ReadWrite)

Since delegated permissions always apply in a user context, here we have decided that these permissions will allow the calling app access to the signed-in user's todo items. Ultimately it is up to you to decide what the permissions mean in your app. An access token acquired with delegated permissions will contain the signed-in user's info as well as the calling app's info. Your app can decide what to return based on that (return only that user's data/return other data the user has access to). The returned data can also depend on the user's roles etc.

To define the permissions, we must edit the application's Manifest. You can find the Manifest button in the App registration's blade in Azure Portal. In there we need to find "oauth2Permissions". It is a JSON array where we must add the new permissions. Note that you cannot remove the default permission there, you can just add the new ones alongside it for now.

When defining a delegated permission we need to provide some properties. Here is the first permission (Read user's todo items):

{
    "adminConsentDescription": "Allow the application to read the signed-in user's todo items.",
    "adminConsentDisplayName": "Read user's todos",
    "id": "4383b02a-d63e-4344-947e-68bb69a00ad0",
    "isEnabled": true,
    "type": "User",
    "userConsentDescription": "Allow the application to read your todo items.",
    "userConsentDisplayName": "Read your todos",
    "value": "Todo.Read"
}

Note that you must specify all of these values:

  • adminConsentDisplayName: Short name shown to an administrator doing admin consent on behalf of all users
  • adminConsentDescription: Long description for admin consent
  • id: Unique GUID/UUID for the permission, you need to generate this ([System.Guid]::NewGuid() works in PowerShell)
  • isEnabled: Set to true for new permissions (I think this makes the permission not appear in tokens anymore, it can still be required by apps)
  • type: Set to User if you want to allow regular users to grant this permission
    • Set it to Admin if you want to make it so that only administrators can grant the permission
  • userConsentDisplayName and userConsentDescription: The short name and long description shown to regular users when they grant this permission
  • value: This will be put in the access token

The two display names and descriptions are shown in different situations. The admin consent ones are shown to an administrator going through the process of granting the permissions on behalf of all users. So here the wording speaks about the signed-in user's todos.

The user consent properties are shown to a user doing regular consent, so they talk about your todos.

We have decided to allow regular users to be able to consent to this permission. So we set the type to User.

On the other hand, we want the Read/Write permission to require an admin to grant it. So we set it's type to Admin:

{
    "adminConsentDescription": "Allow the application to read and modify the signed-in user's todo items.",
    "adminConsentDisplayName": "Read and modify user's todos",
    "id": "6bd7f1a6-59d2-48e9-aaba-d4f9e861c668",
    "isEnabled": true,
    "type": "Admin",
    "userConsentDescription": "Allow the application to read and modify your todo items.",
    "userConsentDisplayName": "Read and modify your todos",
    "value": "Todo.ReadWrite"
}

This actually means no one will ever see the userConsentDescription or userConsentDisplayName, since we require admin consent.

In your APIs, it is completely up to you whether you require admin consent for delegated permissions or not. Usually it is recommended that permissions which could cause damage require admin consent. (Since users might not read the permissions the app is asking, and hopefully the admin does)

Then we just hit Save and we are done :)

Here is how the completed permissions look like for me (with all the other properties in the manifest left out):

"oauth2Permissions": [
{
    "adminConsentDescription": "Allow the application to access Todo API on behalf of the signed-in user.",
    "adminConsentDisplayName": "Access Todo API",
    "id": "1c2dd5e6-6988-4ab8-b5e6-6260ef26e872",
    "isEnabled": true,
    "type": "User",
    "userConsentDescription": "Allow the application to access Todo API on your behalf.",
    "userConsentDisplayName": "Access Todo API",
    "value": "user_impersonation"
},
{
    "adminConsentDescription": "Allow the application to read the signed-in user's todo items.",
    "adminConsentDisplayName": "Read user's todos",
    "id": "4383b02a-d63e-4344-947e-68bb69a00ad0",
    "isEnabled": true,
    "type": "User",
    "userConsentDescription": "Allow the application to read your todo items.",
    "userConsentDisplayName": "Read your todos",
    "value": "Todo.Read"
},
{
    "adminConsentDescription": "Allow the application to read and modify the signed-in user's todo items.",
    "adminConsentDisplayName": "Read and modify user's todos",
    "id": "6bd7f1a6-59d2-48e9-aaba-d4f9e861c668",
    "isEnabled": true,
    "type": "Admin",
    "userConsentDescription": "Allow the application to read and modify your todo items.",
    "userConsentDisplayName": "Read and modify your todos",
    "value": "Todo.ReadWrite"
}
]

You can now go update the required permissions for the test client app that was made in part 1. Note that if you want to get rid of the default permission, you can't delete it directly. You will have to disable it first. This is done by setting "isEnabled": false. Then after saving, you can delete it completely from the array and save again to finally delete it.

Now if you made a new client app and only required the newer permissions, you'd get a 403 Forbidden back from the API. Since the API requires the default permission, it won't accept the request. The current client will still work even if its require permissions are changed. It still has the default permission, even if it no longer required it.

To configure the API to accept our new delegated permissions, we will need two new authorization policies:

services.AddAuthorization(o =>
{
    o.AddPolicy(Policies.Default, policy =>
    {
        policy.RequireAuthenticatedUser();
    });
    o.AddPolicy(Policies.ReadTodoItems, policy =>
    {
        policy.RequirePermissions(new[] { Scopes.TodosRead, Scopes.TodosReadWrite });
    });
    o.AddPolicy(Policies.WriteTodoItems, policy =>
    {
        policy.RequirePermissions(new[] { Scopes.TodosReadWrite });
    });
});

RequirePermissions is a small extension method I made for requiring delegated/application permissions. You can see it's source code here. Here we utilize ASP.NET Core's authorization system to its fullest by adding a custom requirement and a custom requirement handler. In addition there is a claims transformation class that splits the delegated permissions into individual claims. You see, when an app has multiple delegated permissions, those values are put into a single claim. For example:

{
    "scp": "Todo.Read Todo.ReadWrite"
}

It's quite annoying having to do string.Split(" ") every time, so this makes it a lot easier. After the transformation, the user will get an additional claim for each scope. The original is also kept. With it you can check if the caller has a delegated permission in the normal way: User.HasClaim(Constants.ScopeClaimType, "Todo.Read"). You can see the transformation here. Without it the check would be something like this:

User.FindFirstValue(Constants.ScopeClaimType)?.Split(" ").Contains("Todo.Read") ?? false;

Then of course we need to apply the authorization policies:

[HttpGet]
[Authorize(Policies.ReadTodoItems)] //This is a read operation, so apply the read policy
public async Task<IActionResult> Get()
{
    return Ok(await _db.TodoItems.AsNoTracking().ToListAsync());
}

Here we get all of the items from the EF context and return them. The sample uses a global query filter to filter out other users' items if the API is called on behalf of a user. You can see the setup for it in TodoContext and how the context is built with TodoContextFactory. The factory is setup in Startup such that it is used whenever new DB contexts are required from DI. If the current call is made on behalf of a user, it grabs that user's object id, and gives it to the DB context, which it then uses in a global query filter:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Setup the default query filter that filters out other users' items if a user id is specified to this context instance
    modelBuilder.Entity<TodoItem>()
        .HasQueryFilter(i => i.UserId == _userId || _userId == null);
}

In this case we have already prepared a bit to accept calls that are not made on behalf of a user, i.e. app-only calls.

Note that it is very important that every request gets a fresh DbContext instance. You would not want another request to have another user's id in the filter. EF Core's new context pooling feature may not be usable if you use this pattern.

In your API you can use either the object id (oid claim) or name identifier (sub claim) to identify users. Never use the username/user principal name, as it can change. Object id and name identifier cannot be changed, and thus work well for identifying the user. The difference between the two is that the object id will be the same across apps and is used as the id for the user in e.g. the Microsoft Graph API. The name identifier can be different between apps, but it is guaranteed to be the same on every login for the same user within one app.

After updating the policies used on the endpoints, you should be able to successfully run a client app using the new permissions.

Quick note about roles

Maybe you would want to add different roles for your users in your front-end. Now if that front-end is a "traditional" back-end Web app, it's relatively simple since you can define user roles on the front-end app in Azure AD and just use those.

Though in that case the API will not check user rights at all so only do this if you can trust the front-end app. The reason is that if the roles are not defined on the API, they will never be in tokens it receives.

However if your front-end app is a public client (i.e. an app which runs on a user device), you can't assign roles for users on it. Well okay, I've heard you can hack them in via the MS Graph API, but what would be the point? The program is running on the user's device. A malicious program/user could remove your role checks anyway. That's probably the reason you can't assign users to roles on public clients.

But often you do want to filter UI elements at least so that users won't see things they have no rights to do anyway. My suggestion is this:

  1. Define the user roles on the back-end API
  2. Decode the API access token in your front-end and check user roles from there
  3. Enforce authorization based on the roles in the API

This way access control is always enforced within the API, and the front-end has a chance to hide unnecessary UI elements. You might also want to do this in the case your front-end is a confidential client, since you can enforce role-based access on the API-level.

Another approach I've seen with Single Page Applications is that you use a single App registration in Azure AD for both the back-end and front-end. In this case you use the Id token to authenticate calls to the API, and it will always contain the roles.

In the sample app it is possible to define user roles required with delegated permissions. So if you have some administrative endpoints, you can require the user to have the Admin role on the API in addition to requiring some delegated permissions assigned to the app. This makes it so that even if some other user logs in to the front-end app and makes the call, even though the right delegated permission is there, the user does not have the Admin role. So the call fails, as it should.

You can see how roles are defined in my older article: Defining permission scopes and roles offered by an app in Azure AD.

Adding application permissions

Now let's say we want apps to be able to call the API in the background as themselves? For example, we want to create a background process that can archive old todo items automatically.

For this, we will add two appRoles in the Manifest:

{
  "appRoles": [
    {
      "allowedMemberTypes": [
        "Application"
      ],
      "displayName": "Read and write all users' todo items",
      "id": "5e792f77-ad94-41dd-9583-5431f97b1238",
      "isEnabled": true,
      "description": "Allow the app to read and write every user's todo items",
      "value": "Todo.ReadWrite.All"
    },
    {
      "allowedMemberTypes": [
        "Application"
      ],
      "displayName": "Read all users' todo items",
      "id": "b941cee8-254c-4a7d-8f3f-6559eea2f2b7",
      "isEnabled": true,
      "description": "Allow the app to read every user's todo items",
      "value": "Todo.Read.All"
    }
  ]
}

Couple important points on these:

  • Allowed member types must contain Application, this makes it an app permission
  • Again you need to generate an id
  • There is only a single display name and description, since app permissions can only be granted by admins

After saving the manifest, you will be able to assign these app permissions. For testing, I made another test client app. This time its type has to be Web app/API, since Native apps cannot use app permissions. You also must add a Key for the app, you can either generate a client secret/password (which is what I did), or you can add the public key of a key pair. The app will use this to authenticate itself and get access tokens.

The API of course will need to be modified as well. For example, we would define a policy like this:

o.AddPolicy(Policies.WriteTodoItems, policy =>
{
    policy.RequirePermissions(
        delegated: new[] { Scopes.TodosReadWrite },
        application: new[] { AppRoles.TodosReadWrite });
});

The permission requirement handler is quite simple:

protected override Task HandleRequirementAsync(
    AuthorizationHandlerContext context,
    PermissionRequirement requirement)
{
    if (requirement.DelegatedPermissions.Any(p =>
        context.User.HasClaim(Constants.ScopeClaimType, p)))
    {
        //Caller has one of the allowed delegated permissions
        context.Succeed(requirement);
    }
    else if(requirement.ApplicationPermissions.Any(p =>
        context.User.HasClaim(ClaimTypes.Role, p)))
    {
        //Caller has one of the allowed application permissions
        context.Succeed(requirement);
    }

    return Task.CompletedTask;
}

And now since in an app-only call we need to return all todo items, the query filter has a case for the user id being null:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Setup the default query filter that filters out other users' items if a user id is specified to this context instance
    modelBuilder.Entity<TodoItem>()
        .HasQueryFilter(i => i.UserId == _userId || _userId == null);
}

If the call is app-only, we set user id to null and will return all items (since the right side of the OR is always true).

The way the sample checks if the call is app-only looks like this:

public static bool IsAppOnlyCall(this ClaimsPrincipal user)
{
    // If caller has a scope claim in the token,
    // it's a delegated call, so an app-only call is one
    // without the claim
    return !user.HasClaim(c => c.Type == Constants.ScopeClaimType);
}

Basically we check if the token contains a scope claim, which tokens acquired with a user context always have. Originally I thought that I could check if an object id is there, but app-only callers also have an object id claim. In their case it's the id of the service principal.

Thanks to the query filter, we do not need to worry too much about a caller getting data they are not supposed to get. If a delegated caller tries to get a todo with an id and that item does not belong to that user, it will not be found and the API will return a 404.

If you are very paranoid, you could use Resource-based authorization to double-check the caller is allowed to access each item being returned.

With these changes, the API should be able to successfully authenticate and authorize clients using application permissions. You can check the sample console app in the repository that uses the Client Credentials Grant Flow: Joonasw.AzureAdApiSample.ConsoleBackgroundJob.

Multi-tenant authentication

So multi-tenancy is what allows other organizations to start using your apps. You can read an introduction to it from the documentation if its concept is not clear to you.

From the developer's point of view, a few things need to be done:

  • Use the common Azure AD authority
  • Disable issuer validation
    • Or setup a validator that checks the valid format
  • Be very careful when handling data, so that a user in tenant A does not see data from tenant B
    • There are various approaches to multi-tenancy, some more hard-core than others
  • Specify your client applications as known client applications in the API

So the first thing to do is change the authority you are using to:

{
    "Authentication": {
        "Authority": "https://login.microsoftonline.com/common/"
    }
}

The common endpoint allows a user to log in with an account in Azure AD tenant. Which is what we want here.

Next, issuer validation. We will have to disable it, since the common endpoint says this is the valid issuer in its metadata:

{
    "issuer": "https://sts.windows.net/{tenantid}/"
}

You can see it for yourself at https://login.microsoftonline.com/common/.well-known/openid-configuration. That value creates a problem since the JWT Bearer authentication handler will try to do an Equals() check on that and the issuer claim in the token. Normally we disable issuer validation, since token signatures as well as the audience are still checked. If you want, you can make a custom issuer validator that checks the issuer claim conforms to that format.

Then, in your API you will most likely need to do some level of tenant isolation. I will leave it up to you to decide how soft/hard you want to make the isolation.

You can easily find out which tenant the user is from by checking the tid claim. It contains the tenant id. You'll find it with the claim type http://schemas.microsoft.com/identity/claims/tenantid in a ClaimsPrincipal though.

So you can use something like this to get it:

public static string GetTenantId(this ClaimsPrincipal user)
{
    return user.FindFirstValue("http://schemas.microsoft.com/identity/claims/tenantid");
}

You could again use a global query filter in EF Core to isolate data between tenants if you store all the tenants' data in one table. This time the filter would need to be applied every time, no matter if the call is app-only or delegated. Keep in mind though that the database is only one thing that needs tenant isolation. You also need to think about caches, file storage etc.

Be sure to add your client applications as known clients of the API. This is done by adding their application ids to the API's manifest in the knownClientApplications property:

{
    "knownClientApplications":[
        "id-of-client-1",
        "id-of-client-2"
    ]
}

Why? So that when another tenant starts using your app, they will be able to consent to the client and the API at the same time. This is pretty important and not doing this will actually prevent other tenants from starting to use your app. The consent screen would give an error along the lines of "Application A requires access to an application which is not registered in this tenant". So either the other tenant people would have to first consent to the API and then the client, or you can take the easy way and specify them as known clients :)

One last note is that the other tenant's administrators can assign whatever permissions they want in your API to their apps. This is exactly the same as Graph API. Though of course, the permissions will only apply within their tenant's scope, as long as you do data isolation properly.

Summary

Well, this article turned out longer than expected :) Hopefully it is useful to you.

Using custom permissions in your APIs will enable you to give granular permissions to client apps. ASP.NET Core's authorization primitives make it relatively simple to add authorization checks. For those scenarios where an API is part of a SaaS application, multi-tenancy in Azure AD allows those organizations to decide the level of access applications have to the various endpoints on your API.

Thanks a lot for reading!

Links: