Recently I had to deal with a scenario where I had several different service principals that were allowed to access certain endpoints on a web service. This code was already in place and was working fine. However - I had a new requirement come up where I needed only ONE service principal to be able to access certain secure endpoints, while the others could only access the "normal" endpoints.
After doing some research, I found two concepts that apply to OAuth and Azure AD application registration: scopes and roles. These concepts allow you to provide granular permissions to certain users, and then can get returned as claims in the JWT that's returned from a successful authentication. Scopes deal with when you working with an actual AD user, or a service that's accessing an endpoint on behalf of (delegated auth) a user. In my scenario, it was all backend service daemons calling the endpoints, so that wouldn't apply. However - roles will work in that case.
I found bits and pieces of examples from multiple different websites (listed below in the "Additional Resources" section), but wanted to put together a full step-by-step guide for how to set this up as a proof of concept.
For this example, we will have two application registrations / service principals. One will be the "TodoService", which will serve as the endpoint that's being called by the other service principal, the "TodoClient". Imagine that the "TodoService" is the resource that contains the sensitive/secure endpoints and it's going to be called by the "TodoClient" service.
The below Powershell script will create both of the application registrations / service principals, as well as define and create a "CallSecureEndpoints" role on the TodoService application. You will need to enter your Azure subscription name and directory domain in the "$subscriptionName" and "$directoryDomain" parameters for it to work, and you can (optionally) change the display names and identifier URI's to your own initials.
#******************************************************************************
# Script parameters
#******************************************************************************
$subscriptionName = "Your Subscription Here"
$directoryDomain = "yourazuredomain.onmicrosoft.com"
$todoClientAppDisplayName = "jdc-todo-client"
$todoServiceAppDisplayName = "jdc-todo-service"
$todoClientAppIdentifierUri = "https://$directoryDomain/jdc/todo/client"
$todoServiceAppIdentifierUri = "https://$directoryDomain/jdc/todo/service"
#******************************************************************************
# Defined functions
#******************************************************************************
function Create-AppRegistration([String]$identifierUri, [String]$displayName, [Boolean]$assignApplicationRoleToAppRegistration) {
[HashTable]$appRegistrationDetails = @{ }
try
{
# Remove existing application registration if it exists
$app = Get-AzureADApplication -Filter "identifierUris/any(uri:uri eq '$identifierUri')"
if ($app)
{
Write-Host ("Removing Application with IdentifierUri: {0}" -f $identifierUri) -ForegroundColor Green
Remove-AzureADApplication -ObjectId $($app.ObjectId)
}
# Create application registration
Write-Host ("Creating Application with IdentifierUri: {0}..." -f $identifierUri) -ForegroundColor Green
$applicationRegistration = New-AzureADApplication `
-DisplayName $displayName `
-IdentifierUris $identifierUri `
-AvailableToOtherTenants $true
# Create service principal and credentials
Write-Host "Creating service principal..."
$applicationRegistrationServicePrincipal = New-AzureADServicePrincipal -AppId $applicationRegistration.AppId
$passwordParams = @{ CustomKeyIdentifier = "AccessKey" }
$applicationRegistrationPasswordCredential = New-AzureADApplicationPasswordCredential -ObjectId $applicationRegistration.ObjectId @passwordParams
if ($assignApplicationRoleToAppRegistration) {
Write-Host "Creating and assigning application roles..."
$callSecureEndpointsAppRole = Create-AppRole -roleName "CallSecureEndpoints" -roleDescription "Applications are allowed to call sensitive and secure endpoints on the service"
$applicationRoles = $applicationRegistration.AppRoles
$applicationRoles.Add($callSecureEndpointsAppRole)
Set-AzureADApplication -ObjectId $applicationRegistration.ObjectId -AppRoles $applicationRoles
}
# fill in our values to return to the caller
$appRegistrationDetails.Uri = $identifierUri
$appRegistrationDetails.DisplayName = $displayName
$appRegistrationDetails.ApplicationId = $applicationRegistration.AppId
$appRegistrationDetails.Secret = $applicationRegistrationPasswordCredential.Value
$appRegistrationDetails.ServicePrincipalObjectId = $applicationRegistrationServicePrincipal.ObjectId
return $appRegistrationDetails
}
catch [Exception]
{
Write-Output ($_)
exit 1
}
}
Function Create-AppRole([string] $roleName, [string] $roleDescription) {
$appRole = New-Object Microsoft.Open.AzureAD.Model.AppRole
$appRole.AllowedMemberTypes = New-Object System.Collections.Generic.List[string]
$appRole.AllowedMemberTypes.Add("Application");
$appRole.DisplayName = $roleName
$appRole.Id = New-Guid
$appRole.IsEnabled = $true
$appRole.Description = $roleDescription
$appRole.Value = $roleName;
return $appRole
}
#******************************************************************************
# Script body
# Execution begins here
#******************************************************************************
Write-Host "Importing Azure Modules..."
Import-Module -Name Az
Import-Module -Name AzureAD
$ErrorActionPreference = "Stop"
Write-Host ("Script Started " + [System.Datetime]::Now.ToString()) -ForegroundColor Green
# Sign in to Azure account
Write-Host "Logging in..."
$currentContext = Get-AzContext
if ($null -eq $currentContext.Subscription)
{
$verboseMessage = Connect-AzAccount
Write-Verbose $verboseMessage
# reload context
$currentContext = Get-AzContext
}
# Select subscription
Write-Host "Selecting subscription '$subscriptionName'"
$verboseMessage = Select-AzSubscription -SubscriptionName $subscriptionName
Write-Verbose $verboseMessage
# Connect to AzureAD (needed to call any of the AD functions)
Connect-AzureAD -TenantId $currentContext.Tenant.Id -AccountId $currentContext.Account.Id
# Create the application registration for TodoService
$todoServiceApplication = Create-AppRegistration -identifierUri $todoServiceAppIdentifierUri -displayName $todoServiceAppDisplayName -assignApplicationRoleToAppRegistration $true
# Create the application registration for TodoClient
$todoClientApplication = Create-AppRegistration -identifierUri $todoClientAppIdentifierUri -displayName $todoClientAppDisplayName -assignApplicationRoleToAppRegistration $false
# Output and display the values
Write-Host "TODO SERVICE APPLICATION DETAILS:"
Write-Host "-------------------------------"
Write-Host ("DisplayName: {0}" -f $todoServiceApplication.DisplayName) -ForegroundColor Yellow
Write-Host ("Uri: {0}" -f $todoServiceApplication.Uri) -ForegroundColor Yellow
Write-Host ("AppId: {0}" -f $todoServiceApplication.ApplicationId) -ForegroundColor Yellow
Write-Host ("Secret: {0}" -f $todoServiceApplication.Secret) -ForegroundColor Yellow
Write-Host ("Service Principal ObjectId: {0}" -f $todoServiceApplication.ServicePrincipalObjectId) -ForegroundColor Yellow
Write-Host "TODO CLIENT APPLICATION DETAILS:"
Write-Host "-------------------------------"
Write-Host ("DisplayName: {0}" -f $todoClientApplication.DisplayName) -ForegroundColor Yellow
Write-Host ("Uri: {0}" -f $todoClientApplication.Uri) -ForegroundColor Yellow
Write-Host ("AppId: {0}" -f $todoClientApplication.ApplicationId) -ForegroundColor Yellow
Write-Host ("Secret: {0}" -f $todoClientApplication.Secret) -ForegroundColor Yellow
Write-Host ("Service Principal ObjectId: {0}" -f $todoClientApplication.ServicePrincipalObjectId) -ForegroundColor Yellow
# Complete
Write-Host ("Creation complete " + [System.Datetime]::Now.ToString()) -ForegroundColor Green
After successfully running the script, a bunch of values are output containing the appID, clientID, secret, and other values for both app registrations. You will want to save those off for later steps.
Now, we should be able to use our credentials for the TodoClient service to obtain an OAuth token from AzureAD. You can do this in Postman by making a POST to "https://login.microsoftonline.com/<Your-Azure-Tenant-ID>/oauth2/token". Provide the following values in the Body section:
grant_type: client_credentials
client_id: The "AppId" for your TodoClient AzureAD application
client_secret: The "Secret" for your TodoClient AzureAD application
resource: The "IdentifierUri" for your TodoService AzureAD application

You can copy out the value in "access_token" and head over to https://jwt.io/ to decode it. You should see values like this in the payload section:

Now that we know that's all setup properly, we need to actually assign the "TodoClient" service the permissions to use the "CallSecureEndpoints" role on the "TodoService" service, as well as grant Admin consent. There may be a way to do this via Powershell scripting, but it's also very easy to just do it manually in the Azure portal:
- Open the "TodoClient" application
- Go to "API permissions"
- Select "Add a permission"
- Choose "APIs my organization uses"
- Find and select the "TodoService" application from the list
- Choose "Application Permissions"
- Select the "CallSecureEndpoints" role
- Click "Add Permissions"

Now you just need to grant admin consent to the permissions:

Now, go back and send another request in Postman and get a new access_token. Go back to https://jwt.io/ and decode it. You should see it now looks slightly different:

You can now see that we get the "roles" returned in our JWT token from AzureAD, and our "CallSecureEndpoints" role is present in the claim!
It's now pretty straightforward to check for the presence of that roles claim when validating your token in your backend code:
var jwtTokenHandler = new JwtSecurityTokenHandler();
var parameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
// ENTER YOUR PARAMETERS SPECIFIC TO YOUR SCENARIO HERE
};
// replace "AuthorizationHeaderValue" with the value you got from POSTMAN in the format "Bearer access_token"
var claims = jwtTokenHandler.ValidateToken("AuthorizationHeaderValue", validationParameters, out var foundToken);
// Find the roles from the roles claim on the token
var assignedRoles = new List();
claims.FindAll("http://schemas.microsoft.com/ws/2008/06/identity/claims/role").ForEach(claim =>
{
assignedRoles.Add(claim.Value);
});
// Allow or deny actions based on if you found your role in the claim or not
And you're all done!
Additional Resources:
I used the following resources when researching this process and building out this blog post:
- https://joonasw.net/view/defining-permissions-and-roles-in-aad
- https://stackoverflow.com/questions/26497365/azure-api-management-scope-claim-null
- https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps
- https://stackoverflow.com/questions/51651889/how-to-add-app-roles-under-manifest-in-azure-active-directory-using-powershell-s
Hopefully you find this helpful - if you have any questions feel free to let me know.
Thanks,
Justin