{"id":2055,"date":"2024-10-30T09:55:50","date_gmt":"2024-10-30T16:55:50","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/identity\/?p=2055"},"modified":"2024-10-30T09:55:50","modified_gmt":"2024-10-30T16:55:50","slug":"introducing-the-authentication-events-nuget-library","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/identity\/introducing-the-authentication-events-nuget-library\/","title":{"rendered":"Integrate data from external sources into Microsoft Entra tokens using the Authentication Events library"},"content":{"rendered":"<p>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\u2019s event-based custom authentication extension framework and a <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-claims-provider-overview\">Custom Claims Provider<\/a>\u2014now generally available\u2014you 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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/identity\/wp-content\/uploads\/sites\/74\/2024\/10\/Fetch-and-add-custom-claims-into-authentication-tokens.png\" alt=\"fetch and add custom claims into authentication tokens\" \/><\/p>\n<p>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\u2019ve released a NuGet library that allows efficient integration of custom user data without the headache of writing boilerplate code\u2014ensuring a secure, performant implementation.<\/p>\n<p>In this blog post, we give you an overview of how to get started.<\/p>\n<h2>Bringing external data into authentication tokens<\/h2>\n<p>Let\u2019s say you\u2019re 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\u2019s important to ensure you can easily pull this custom data into your application and provide it as custom claims in the token.<\/p>\n<h2>Introducing the Authentication Events NuGet library<\/h2>\n<p>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\u2019s <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-extension-overview\">custom extension<\/a> objects\u2014allowing you to focus on building custom business logic.<\/p>\n<p>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.<\/p>\n<h2>Advantages of using the NuGet library<\/h2>\n<ul>\n<li><strong>Secure, built-in token validation<\/strong> \n<ul>\n<li>Secure implementation of token validation using the Microsoft Authentication Library (MSAL), to make sure requests coming into the Azure Functions are legitimate. <\/li>\n<\/ul>\n<\/li>\n<li><strong>Reduced developer overhead<\/strong> \n<ul>\n<li>The library manages serialization, deserialization, and defining data models for requests and responses\u2014saving time and effort. <\/li>\n<\/ul>\n<\/li>\n<li><strong>Easy integration with Azure Functions<\/strong> \n<ul>\n<li>Easily integrate with Azure Functions to streamline your setup. <\/li>\n<\/ul>\n<\/li>\n<li><strong>Detailed error handling<\/strong> \n<ul>\n<li>Clear, detailed error messages aid in debugging and logging. <\/li>\n<\/ul>\n<\/li>\n<li><strong>Comprehensive data modelling<\/strong> \n<ul>\n<li>Provides a comprehensive data model for Custom Extension Events. Handles the serialization of requests and responses. <\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>Start using the NuGet library<\/h2>\n<p>To use the library, create an Azure Function in Visual Studio or Visual Studio Code using the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents\/\">Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents<\/a> NuGet package.<\/p>\n<p>For detailed step-by-step instructions, please refer to the full docs guide on <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-extension-tokenissuancestart-setup?tabs=visual-studio%2Cazure-portal&amp;pivots=nuget-library\">creating a REST API for a token issuance event in Azure Functions<\/a>.<\/p>\n<p>For the purposes of this blog post, the code samples below outline how to map claims in an Azure Function <em>with<\/em> and <em>without<\/em> the NuGet as part of the response.<\/p>\n<blockquote>\n<p><strong>Note:<\/strong> 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.<\/p>\n<\/blockquote>\n<h3>Sample of mapping claims using NuGet<\/h3>\n<p>The below is a code sample for adding a string and string array as two new claims to the response with the NuGet<\/p>\n<pre><code class=\"language-csharp\">\nusing System;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\n\nusing Microsoft.Azure.WebJobs;\nusing Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents;\nusing Microsoft.Azure.WebJobs.Extensions.AuthenticationEvents.TokenIssuanceStart;\nusing Microsoft.Extensions.Logging;\n\nnamespace AuthEventsTrigger\n{\n    public static class AuthEventsTrigger\n    {\n        \/\/ The WebJobsAuthenticationEventsTriggerAttribute attribute can be used to specify audience app ID, \n        \/\/ authority URL and authorized party app id. \n        \/\/ This is an alternative route to setting up Authorization values instead of Environment variables or EzAuth\n        [FunctionName(\"onTokenIssuanceStart\")]\n        public static WebJobsAuthenticationEventResponse Run(\n            [WebJobsAuthenticationEventsTrigger(\n                    AudienceAppId = \"Enter custom authentication extension app ID here\",\n                    AuthorityUrl = \"Enter authority URI here\",\n                    AuthorizedPartyAppId = \"Enter the Authorized Party App Id here\")]\n            WebJobsTokenIssuanceStartRequest request, ILogger log)\n        {\n            try\n            {\n                \/\/ Checks if the request is successful and did the token validation pass \n                if (request.RequestStatus == WebJobsAuthenticationEventsRequestStatusType.Successful)\n                {\n                    var claimValue = GetMyCustomClaimFromADFS(\n                        GetAccessToken(),\n                        request.Data.TenantId.ToString(),\n                        request.Data.AuthenticationContext.User.UserPrincipalName,\n                        log);\n\n                    \/\/ Create a list of claims to be added to the token \n                    var claims = new[]\n                    {\n                        \/\/ Set the claim value from the source and add it to the list of claims \n                        new WebJobsAuthenticationEventsTokenClaim(\"mappedClaimName\", claimValue),\n                        new WebJobsAuthenticationEventsTokenClaim(\"customRole\", \"Writer\", \"Editor\")\n                    };\n\n                    \/\/ Add new claims to the response \n                    request.Response.Actions.Add(new WebJobsProvideClaimsForToken(claims));\n                }\n                else\n                {\n                    \/\/ If the request fails, such as in token validation, output the failed request status, \n                    \/\/ such as in token validation or response validation. \n                    log.LogInformation(request.StatusMessage);\n                }\n                return request.Completed();\n            }\n            catch (Exception ex)\n            {\n                return request.Failed(ex);\n            }\n        }\n\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Filler function to return value for the custom claim\n        \/\/\/ &lt;\/summary&gt;\n        private static string GetMyCustomClaimFromADFS(string tenantId, string upn, ILogger log)\n        {\n            try\n            {\n                if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))\n                {\n                    throw new ArgumentNullException();\n                }\n                throw new NotImplementedException();\n            }\n            catch (NotImplementedException)\n            {\n                return \"DefaultValue\";\n            }\n        }\n    }\n}<\/code><\/pre>\n<h4>Response<\/h4>\n<p>200<\/p>\n<pre><code class=\"language-json\">{\n    \"data\": {\n        \"@odata.type\": \"microsoft.graph.onTokenIssuanceStartResponseData\",\n        \"actions\": [\n            {\n                \"@odata.type\": \"microsoft.graph.tokenIssuanceStart.provideClaimsForToken\",\n                \"claims\": {\n                    \"customClaimValue\": \"DefaultValue\",\n                    \"customRoles\": [\n                        \"Writer\",\n                        \"Editor\"\n                    ]\n                }\n            }\n        ]\n    }\n}<\/code><\/pre>\n<h3>Sample of mapping claims without the NuGet<\/h3>\n<p>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.<\/p>\n<pre><code class=\"language-csharp\">using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing System.Threading.Tasks;\nusing System.Web.Http;\n\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Azure.WebJobs;\nusing Microsoft.Azure.WebJobs.Extensions.Http;\nusing Microsoft.Extensions.Logging;\n\nusing Newtonsoft.Json;\n\nnamespace MyCustomClaimsProvider\n{\n    public static class Function1\n    {\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Main function to read in the request, and set the claims.\n        \/\/\/ &lt;\/summary&gt;\n        [FunctionName(\"Function1\")]\n        public static async Task&lt;IActionResult&gt; Run(\n            [HttpTrigger(AuthorizationLevel.Function, \"post\", Route = null)] HttpRequest req,\n            ILogger log)\n        {\n            try\n            {\n                \/\/ No built in Token Validation.\n                \/\/ We can not assert the token was verified by EazyAuth either without adding additional code.\n                \/\/ No request validation.\n                log.LogInformation(\"C# HTTP trigger function processed a request.\");\n                string requestBody = await new StreamReader(req.Body).ReadToEndAsync();\n\n                \/\/ Dynamic object to read the request body, and get the user's UPN and tenant ID\n                dynamic data = JsonConvert.DeserializeObject(requestBody);\n\n                \/\/ Read the upn and tenantId from the Microsoft Entra request body\n                string upn = data?.data.authenticationContext.user.userPrincipalName;\n                string tenantId = data?.data.tenantId;\n\n                var claimValue = GetMyCustomClaimFromADFS(GetAccessToken(), tenantId, upn, log);\n\n                \/\/ Claims to return to Microsoft Entra\n                ResponseContent r = new();\n                r.Data.Actions[0].Claims.CustomClaimValue = claimValue;\n                r.Data.Actions[0].Claims.CustomRoles.Add(\"Writer\");\n                r.Data.Actions[0].Claims.CustomRoles.Add(\"Editor\");\n\n                \/\/ No response validation.\n                return new OkObjectResult(r);\n            }\n            catch (Exception ex)\n            {\n                log.LogError(ex.Message);\n                return new InternalServerErrorResult();\n            }\n        }\n\n        \/\/\/ &lt;summary&gt;\n        \/\/\/ Filler function to return value for the custom claim\n        \/\/\/ &lt;\/summary&gt;\n        private static string GetMyCustomClaimFromADFS(string tenantId, string upn, ILogger log)\n        {\n            try\n            {\n                if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))\n                {\n                    throw new ArgumentNullException();\n                }\n                throw new NotImplementedException();\n            }\n            catch (NotImplementedException)\n            {\n                return \"DefaultValue\";\n            }\n        }\n    }\n\n    public class ResponseContent\n    {\n        [JsonProperty(\"data\")]\n        public Data Data { get; set; }\n        public ResponseContent()\n        {\n            Data = new Data();\n        }\n    }\n\n    public class Data\n    {\n        [JsonProperty(\"@odata.type\")]\n        public string Odatatype { get; set; }\n        public List&lt;Action&gt; Actions { get; set; }\n        public Data()\n        {\n            Odatatype = \"microsoft.graph.onTokenIssuanceStartResponseData\";\n            Actions = new List&lt;Action&gt;\n            {\n                new()\n            };\n        }\n    }\n\n    public class Action\n    {\n        [JsonProperty(\"@odata.type\")]\n        public string Odatatype { get; set; }\n        public Claims Claims { get; set; }\n        public Action()\n        {\n            Odatatype = \"microsoft.graph.tokenIssuanceStart.provideClaimsForToken\";\n            Claims = new Claims();\n        }\n    }\n\n    public class Claims\n    {\n        public string CustomClaimValue { get; set; }\n\n        public List&lt;string&gt; CustomRoles { get; set; }\n\n        public Claims()\n        {\n            CustomRoles = new List&lt;string&gt;();\n        }\n    }\n}<\/code><\/pre>\n<h4>Response with valid request<\/h4>\n<p><code>200<\/code><\/p>\n<pre><code class=\"language-json\">{\n    \"data\": {\n        \"@odata.type\": \"microsoft.graph.onTokenIssuanceStartResponseData\",\n        \"actions\": [\n            {\n                \"@odata.type\": \"microsoft.graph.tokenIssuanceStart.provideClaimsForToken\",\n                \"claims\": {\n                    \"customClaimValue\": \"DefaultValue\",\n                    \"customRoles\": [\n                        \"Writer\",\n                        \"Editor\"\n                    ]\n                }\n            }\n        ]\n    }\n}<\/code><\/pre>\n<h4>Response with invalid request<\/h4>\n<p><code>500<\/code><\/p>\n<h2>What\u2019s next<\/h2>\n<p>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\u2019ll be adding support for the isolated worker process soon.<\/p>\n<p>We welcome feedback to help us determine where we should focus our energy. Please reach out to us using <a href=\"https:\/\/github.com\/Azure\/azure-sdk-for-net\/issues\/new\/choose\">Github Issues<\/a>, adding the \u2018Entra\u2019 tag.<\/p>\n<h2>Stay connected and informed<\/h2>\n<p>To learn more or test out features in the Microsoft Entra portfolio, visit our\u202f<a href=\"https:\/\/aka.ms\/dev\/ms-entra\">developer center<\/a>. Make sure you subscribe to the\u202f<a href=\"https:\/\/aka.ms\/devblog\/ms-entra\">Identity developer blog<\/a>\u202ffor more insights and to keep up with the latest on all things Identity. And, follow us on\u202f<a href=\"https:\/\/www.youtube.com\/@MicrosoftSecurity\/playlists\">YouTube<\/a>\u202ffor video overviews, tutorials, and deep dives.\u202f<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Streamline the integration of custom claims into authentication tokens with the new Authentication Events NuGet library. This blog post introduces a NuGet library that simplifies the implementation of custom claims providers, reducing the need for extensive code and minimizing validation errors. Learn how to efficiently fetch and add custom data, like entitlements or profile attributes, from external systems, such as CRMs or legacy databases, while ensuring secure, scalable solutions for your applications.<\/p>\n","protected":false},"author":174217,"featured_media":2068,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[40,32],"tags":[38,44,20,16,50,4],"class_list":["post-2055","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-insights","category-news","tag-authentication","tag-authorization","tag-devex","tag-entra","tag-identity","tag-security"],"acf":[],"blog_post_summary":"<p>Streamline the integration of custom claims into authentication tokens with the new Authentication Events NuGet library. This blog post introduces a NuGet library that simplifies the implementation of custom claims providers, reducing the need for extensive code and minimizing validation errors. Learn how to efficiently fetch and add custom data, like entitlements or profile attributes, from external systems, such as CRMs or legacy databases, while ensuring secure, scalable solutions for your applications.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/posts\/2055","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/users\/174217"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/comments?post=2055"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/posts\/2055\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/media\/2068"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/media?parent=2055"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/categories?post=2055"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/identity\/wp-json\/wp\/v2\/tags?post=2055"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}