Modern applications often require business-specific data such as entitlements or profile attributes. This data is typically stored in external systems, like CRMs or legacy on-prem datastores. And, without a streamlined solution, conveying custom data, known as custom claims, to applications can become complex. Using Microsoft Entra’s event-based custom authentication extension framework and a Custom Claims Provider—now generally available—you can fetch external data via an API call and add it as custom claims into authentication tokens issued by Microsoft Entra ID or Microsoft Entra External ID.
Implementing an API can require writing a significant amount of code and introduces the potential for error in things like token validation. To simplify the process of implementing a custom claims provider, we’ve released a NuGet library that allows efficient integration of custom user data without the headache of writing boilerplate code—ensuring a secure, performant implementation.
In this blog post, we give you an overview of how to get started.
Bringing external data into authentication tokens
Let’s say you’re working with a legacy datastore that stores customer information. You need specific pieces of customer data included in authentication tokens to complete a process for each user. It’s important to ensure you can easily pull this custom data into your application and provide it as custom claims in the token.
Introducing the Authentication Events NuGet library
A custom claims provider allows your application to fetch key pieces of user data from external sources and include them in authentication tokens. The NuGet library simplifies the implementation of custom claims providers in an Azure Function. It handles token validation, request and response validation, and includes a built-in data model of Microsoft Entra’s custom extension objects—allowing you to focus on building custom business logic.
Using the NuGet library, you can easily set up a custom claims provider with minimal coding on your part. This approach saves you time but also ensures that your system is secure, scalable, and easy to maintain.
Advantages of using the NuGet library
- Secure, built-in token validation
- Secure implementation of token validation using the Microsoft Authentication Library (MSAL), to make sure requests coming into the Azure Functions are legitimate.
- Reduced developer overhead
- The library manages serialization, deserialization, and defining data models for requests and responses—saving time and effort.
- Easy integration with Azure Functions
- Easily integrate with Azure Functions to streamline your setup.
- Detailed error handling
- Clear, detailed error messages aid in debugging and logging.
- Comprehensive data modelling
- Provides a comprehensive data model for Custom Extension Events. Handles the serialization of requests and responses.
Start using the NuGet library
To use the library, create an Azure Function in Visual Studio or Visual Studio Code using the Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents NuGet package.
For detailed step-by-step instructions, please refer to the full docs guide on creating a REST API for a token issuance event in Azure Functions.
For the purposes of this blog post, the code samples below outline how to map claims in an Azure Function with and without the NuGet as part of the response.
Note: the sample without the NuGet does not do token validation, but the one in the NuGet offers all the benefits listed above and is much more intuitive to write and understand.
Sample of mapping claims using NuGet
The below is a code sample for adding a string and string array as two new claims to the response with the NuGet
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents;
using Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart;
using Microsoft.Extensions.Logging;
namespace AuthEventsTrigger
{
public static class AuthEventsTrigger
{
// The WebJobsAuthenticationEventsTriggerAttribute attribute can be used to specify audience app ID,
// authority URL and authorized party app id.
// This is an alternative route to setting up Authorization values instead of Environment variables or EzAuth
[FunctionName("onTokenIssuanceStart")]
public static WebJobsAuthenticationEventResponse Run(
[WebJobsAuthenticationEventsTrigger(
AudienceAppId = "Enter custom authentication extension app ID here",
AuthorityUrl = "Enter authority URI here",
AuthorizedPartyAppId = "Enter the Authorized Party App Id here")]
WebJobsTokenIssuanceStartRequest request, ILogger log)
{
try
{
// Checks if the request is successful and did the token validation pass
if (request.RequestStatus == WebJobsAuthenticationEventsRequestStatusType.Successful)
{
var claimValue = GetMyCustomClaimFromADFS(
GetAccessToken(),
request.Data.TenantId.ToString(),
request.Data.AuthenticationContext.User.UserPrincipalName,
log);
// Create a list of claims to be added to the token
var claims = new[]
{
// Set the claim value from the source and add it to the list of claims
new WebJobsAuthenticationEventsTokenClaim("mappedClaimName", claimValue),
new WebJobsAuthenticationEventsTokenClaim("customRole", "Writer", "Editor")
};
// Add new claims to the response
request.Response.Actions.Add(new WebJobsProvideClaimsForToken(claims));
}
else
{
// If the request fails, such as in token validation, output the failed request status,
// such as in token validation or response validation.
log.LogInformation(request.StatusMessage);
}
return request.Completed();
}
catch (Exception ex)
{
return request.Failed(ex);
}
}
/// <summary>
/// Filler function to return value for the custom claim
/// </summary>
private static string GetMyCustomClaimFromADFS(string tenantId, string upn, ILogger log)
{
try
{
if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
{
throw new ArgumentNullException();
}
throw new NotImplementedException();
}
catch (NotImplementedException)
{
return "DefaultValue";
}
}
}
}
Response
200
{
"data": {
"@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData",
"actions": [
{
"@odata.type": "microsoft.graph.tokenIssuanceStart.provideClaimsForToken",
"claims": {
"customClaimValue": "DefaultValue",
"customRoles": [
"Writer",
"Editor"
]
}
}
]
}
}
Sample of mapping claims without the NuGet
The below is a code sample for adding a string and string array as two new claims to the response without the NuGet. There is no token validation, request or response validation in the below code. If using the code below in production, we strongly recommend adding token validation at a minimum.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace MyCustomClaimsProvider
{
public static class Function1
{
/// <summary>
/// Main function to read in the request, and set the claims.
/// </summary>
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
ILogger log)
{
try
{
// No built in Token Validation.
// We can not assert the token was verified by EazyAuth either without adding additional code.
// No request validation.
log.LogInformation("C# HTTP trigger function processed a request.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
// Dynamic object to read the request body, and get the user's UPN and tenant ID
dynamic data = JsonConvert.DeserializeObject(requestBody);
// Read the upn and tenantId from the Microsoft Entra request body
string upn = data?.data.authenticationContext.user.userPrincipalName;
string tenantId = data?.data.tenantId;
var claimValue = GetMyCustomClaimFromADFS(GetAccessToken(), tenantId, upn, log);
// Claims to return to Microsoft Entra
ResponseContent r = new();
r.Data.Actions[0].Claims.CustomClaimValue = claimValue;
r.Data.Actions[0].Claims.CustomRoles.Add("Writer");
r.Data.Actions[0].Claims.CustomRoles.Add("Editor");
// No response validation.
return new OkObjectResult(r);
}
catch (Exception ex)
{
log.LogError(ex.Message);
return new InternalServerErrorResult();
}
}
/// <summary>
/// Filler function to return value for the custom claim
/// </summary>
private static string GetMyCustomClaimFromADFS(string tenantId, string upn, ILogger log)
{
try
{
if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
{
throw new ArgumentNullException();
}
throw new NotImplementedException();
}
catch (NotImplementedException)
{
return "DefaultValue";
}
}
}
public class ResponseContent
{
[JsonProperty("data")]
public Data Data { get; set; }
public ResponseContent()
{
Data = new Data();
}
}
public class Data
{
[JsonProperty("@odata.type")]
public string Odatatype { get; set; }
public List<Action> Actions { get; set; }
public Data()
{
Odatatype = "microsoft.graph.onTokenIssuanceStartResponseData";
Actions = new List<Action>
{
new()
};
}
}
public class Action
{
[JsonProperty("@odata.type")]
public string Odatatype { get; set; }
public Claims Claims { get; set; }
public Action()
{
Odatatype = "microsoft.graph.tokenIssuanceStart.provideClaimsForToken";
Claims = new Claims();
}
}
public class Claims
{
public string CustomClaimValue { get; set; }
public List<string> CustomRoles { get; set; }
public Claims()
{
CustomRoles = new List<string>();
}
}
}
Response with valid request
200
{
"data": {
"@odata.type": "microsoft.graph.onTokenIssuanceStartResponseData",
"actions": [
{
"@odata.type": "microsoft.graph.tokenIssuanceStart.provideClaimsForToken",
"claims": {
"customClaimValue": "DefaultValue",
"customRoles": [
"Writer",
"Editor"
]
}
}
]
}
}
Response with invalid request
500
What’s next
Currently, the NuGet library supports writing a custom claims provider in C#, running as an Azure Function. We are evaluating other platforms and languages, such as Python, JavaScript, TypeScript, Go, and Java. Additionally, we are looking at the possibility of offering data models as a separate library. For developers working with the latest NuGet libraries on Azure, we’ll be adding support for the isolated worker process soon.
We welcome feedback to help us determine where we should focus our energy. Please reach out to us using Github Issues, adding the ‘Entra’ tag.
Stay connected and informed
To learn more or test out features in the Microsoft Entra portfolio, visit our developer center. Make sure you subscribe to the Identity developer blog for more insights and to keep up with the latest on all things Identity. And, follow us on YouTube for video overviews, tutorials, and deep dives. 
0 comments
Be the first to start the discussion.