Custom Claims in ASP.NET Core with Azure AD

Custom claims is what you want when you have some additional properties you want to use for your application logic that is tied to the user executing the logic. This can be extra information to do custom authorization, a customer id in a SaaS solution etc. Custom claims are neat because you get them when the user signs in to your authentication provider – in this case Azure AD and is part of the token returned. This makes them tamper resistant and you don’t need extra calls to get this information. To get this up and running in Azure AD though, has got to be one of the most non-obvious things I have done in a long time. This is the end-to-end guide on how to accomplish this.

There is this concept in Azure AD called extension properties. An extension property extends an Azure AD entity type with a custom field. When an extension property has been added to an entity it can be accessed and assigned through the entity’s AdditionalData property.

An extension property is declared in scope of an App Registration in Azure AD. Using PowerShell you can create an App Registration and extend the user entity in your Azure AD with a field called SkypeName using the following commands (easiest from the Azure Portal’s Cloud Shell):

$NewAppRegistration = New-AzureADApplication -DisplayName "MyAppRegistrationWithExtensions"
New-AzureADServicePrincipal -AppId $NewAppRegistration.AppId 
$ExtensionProperty = New-AzureADApplicationExtensionProperty -ObjectId $NewAppRegistration.ObjectId -Name SkypeName -DataType "String" -TargetObjects "User"

The $ExtensionProperty will return a class with three parameters:

  • ObjectId
  • Name
  • TargetObjects

The ObjectId is a guid. The Name is a property in the format Extension_[GUID]_Name so in the example with the SkypeName it will look like Extensions_bb6ee372-e3f7-4128-90f7-eeb77d7d1c29_SkypeName. The catch with an extension property is that you need this full name to set the property’s value in Azure AD. Another important thing to note is that if you do not add a ServicePrincipal, you cannot set the extension property. The TargetObjects show what Azure AD entities it can be used on.

To set the value of the new extension property on a user object in your Azure AD, use the Graph API. On entities in Azure AD, there is a property called AdditionalData which is a dictionary of strings and objects. The following code snippet sets the extension property using the Microsoft.Graph package in C#

Prerequisite

To work with <Azure AD through the Graph API you need to get access by getting an access token with the correct access rights. For this, create a new App Registration in the Azure AD that can read and write the Azure AD without user interaction. To do this, open up Azure Active Directory in the Azure Portal and navigate to App Registrations

Click the New registration button.

Click the register button once you have given the new registration a name. On the next page copy the “Application (client) id” and keep it somewhere safe for later use. Now navigate to Certificates & secrets

Click New client secret, give it an optional name and copy the generated client secret. Keep it safe with the Application Id for use shortly. Now select API permissions.

Click Add a permission and select Microsoft Graph. You need the following permissions.

To make the permissions accessible to your App Registration click Grant admin consent for tenant name. When using delegated permissions to a user, the user consents to allowing an application to work on behalf of the user. When working with a system login (application id + client secret) we need to grant the permissions up front (now).

Next step, is to create a test user in Azure AD that can have its AdditionaData property assigned with the new extension property. Go to your Azure Active Directory in the Azure Portal. Select the Users menu and then “New user”

Fill out the user information and once created note down the username.

Set Claims using C#

  • Create a new .NET console project (dotnet new console).
  • Add Nuget Package Microsoft.Graph
  • Add Nuget Package Microsoft.IdentityModel.Clients.ActiveDirectory
  • Replace AppId and ClientSecretValue with the values generated previously. Also note that the extension_[Guid]_SkypeName extension must match the name in your system.
class Program
    {
        static async Task<string> GetAccessTokenAsync(string tenant, string applicationId, string applicationSecretKey)
        {
            const string AadInstance = "https://login.microsoftonline.com/{0}";
            const string Resource = "https://graph.microsoft.com";
            string authority = string.Format(CultureInfo.InvariantCulture, AadInstance, tenant);

            var authenticationContext = new AuthenticationContext(authority);
            var clientCredential = new ClientCredential(applicationId, applicationSecretKey);

            AuthenticationResult authResult = await authenticationContext.AcquireTokenAsync(Resource, clientCredential);

            return authResult.AccessToken;
        }

        static async Task Main(string[] args)
        {
            string accessToken = await GetAccessTokenAsync("tenantname.onmicrosoft.com", "[AppId]", "[ClientSecretValue]");

            var graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) =>
            {
                requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
                return Task.FromResult(0);
            }));

            string username = "Your Test User's Username";
            IUserRequest userRequest = graphServiceClient.Users[username].Request();
            var compatibleDataValuesDictionary = new Dictionary<string, object>();
            compatibleDataValuesDictionary["extension_GUID_SkypeName"] = "TestValue";

            var user = await userRequest.GetAsync();
            user.AdditionalData = compatibleDataValuesDictionary;
            await userRequest.UpdateAsync(user);
        }
    }

So now, there is an App Registration which owns the SkypeName extension. So far, so good, but what you really want, is to be able to use this claim from any application with their own App Registrations in your Azure AD tenant. Why? Because most often the custom claims are synchronized from an external data source and they should only know about a single App Registration not all of the possible applications that will use them.

Custom Clams Drawing
Propagate one set of custom claims consistently across multiple app registrations.

To use the SkypeName claim in another App Registration – in this example let it be a meeting planner application called MeetingPlannerApp that needs the user’s SkypeName, the claims needs to be mapped to this App Registration. So we want to map the SkypeName from the MyAppRegistrationWithExtensions App Registration that owns the Azure AD extension property to MeetingPlannerApp App Registration.
Since it can be hard to remember what exact name an extension property ended up having because of the extension_[guid] prefix, the solution below takes the simple name – e.g. SkypeName – of the property and translates it to the complete one. The PowerShell script assumes you are logged in with sufficient rights to Azure AD using Connect-AzureAD from the Azure AD PowerShell module.

$SchemaExtensionApp = (Get-AzureADApplication -Filter "DisplayName eq 'MyAppRegistrationWithExtensions'")

function GetExtensionProperty {
  param( [string]$extensionName )
  return @{ 
    "JwtClaimType" = $extensionName 
    "ExtensionId"  = (Get-AzureADApplicationExtensionProperty -ObjectId $SchemaExtensionApp.ObjectId).Where( { $_.Name.endsWith($extensionName)})[0].Name
  }
}

$SkypeNameExtension = GetExtensionProperty("SkypeName")

$ClaimsMappingPolicy = [ordered]@{
  "ClaimsMappingPolicy" = [ordered]@{
    "Version"              = 1
    "IncludeBasicClaimSet" = $true
    "ClaimsSchema"         = @(
      [ordered]@{
        "Source"       = "User"
        "ExtensionID"  = $SkypeNameExtension.ExtensionId
        "JwtClaimType" = $SkypeNameExtension.JwtClaimType
      }
    )
  }
}

$SecurityPrincipal = (Get-AzureADServicePrincipal -Filter "DisplayName eq 'MeetingPlannerApp'") 

$PolicyDefinition = $ClaimsMappingPolicy | ConvertTo-Json -Depth 99 -Compress
$Policy = New-AzureADPolicy -Type "ClaimsMappingPolicy" -DisplayName "MyPolicyName" -Definition $PolicyDefinition
Add-AzureADServicePrincipalPolicy -Id $SecurityPrincipal.ObjectId -RefObjectId $Policy.Id

The PowerShell script creates a function that can return the full extension property name along with its JwtClaimType based on just the friendly extension name. It then creates an array of claims, creates a ClaimsMappingPolicy and applies it to the MeetingPlannerApp App Registration.

Logging in to Azure AD now from the meeting planner using the MeetingPlannerApp App Registration for authentication, will give an error though. Most likely AADSTS50146 which means that the App Registration has not been set up to allow claims mapping. To allow this, head over to the Azure Portal (https://portal.azure.com) and go to Azure Active Directory, then App Registrations. Open up your App Registration (in our example it would be MeetingPlannerApp. Click manifest and you can edit the App Registration’s manifest in json. Locate AcceptMappedClaims: null and change the null to true. Save the manifest.

Using the App Registration now for logging in will give you the SkypeName as a custom claim in the token. To inspect the value, use your browser’s development tools to extract the token and head over to jwt.ms or jwt.io to inspect the token.

If you are developing an ASP.NET Core application, you can try out the claims easily by creating a new WebApi app with authentication set to SingleOrg

dotnet new webapi -au SingleOrg -o MyWebApi

In appsettings.json fill out the App Registration details from the MeetingPlannerApp App Registration. In the WeatherForecastController Get() method add the following to read the claims:

var claimsPrincipal = ClaimsPrincipal.Current;
var claims = claimsPrincipal.Claims;

If you are working in .NET Core 3.0 or later, add a reference to the Nuget package System.Security.Claims for this to work.

Summary

Using the steps in this article, you should be able to create an App Registration and use that to extend your Azure AD user entity with new extension properties. Then map those properties to other App Registrations, and use them as (custom) claims.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.