Utilizing AzureAD Application Roles for added security when using Service-to-Service authentication

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 = @{ }
		# 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
			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.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 "-------------------------------"
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 "-------------------------------"
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

Authenticating with your service principal to get an OAuth access_token

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:

JWT payload without roles claim

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:

  1. Open the "TodoClient" application
  2. Go to "API permissions"
  3. Select "Add a permission"
  4. Choose "APIs my organization uses"
  5. Find and select the "TodoService" application from the list
  6. Choose "Application Permissions"
  7. Select the "CallSecureEndpoints" role
  8. Click "Add Permissions"
Adding the CallSecureEndpoints role assignment to the TodoClient service principal

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

Granting 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:

JWT payload with roles claim

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

// 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 =>

// 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:

Hopefully you find this helpful - if you have any questions feel free to let me know.


Justin Carlson

Read more posts by this author.