{"id":7989,"date":"2026-09-16T16:10:16","date_gmt":"2026-09-16T14:10:16","guid":{"rendered":"https:\/\/blog.redbaronofazure.com\/?p=7989"},"modified":"2026-09-16T17:32:30","modified_gmt":"2026-09-16T15:32:30","slug":"custom-authentication-extensions-for-entra-external-id","status":"publish","type":"post","link":"https:\/\/blog.redbaronofazure.com\/?p=7989","title":{"rendered":"Custom Authentication Extensions for Entra External ID"},"content":{"rendered":"\n<p>In both Entra Workforce tenants and Entra External tenants, you can add your own <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-extension-tokenissuancestart-setup\" target=\"_blank\" rel=\"noreferrer noopener\">custom authentication extensions<\/a>. In a workforce tenant, you should think twice before adding custom code to your authentication scenarios as you most likely don&#8217;t need it. In external tenants, there is a likelihood that you will need it. What you can do differs between the tenant types:<\/p>\n\n\n\n<ul><li><strong>Workforce<\/strong> &#8211; Token Issuance, Account Recovery with Verified ID<\/li><li><strong>External<\/strong> &#8211; Token Issuance, Attribute Collection Start &amp; Submit, EmailOtpSend, and Password Migration <\/li><\/ul>\n\n\n\n<p> In detail, the different types can help you with:<\/p>\n\n\n\n<ul><li><strong>Token Issuance<\/strong> &#8211; add custom claims to the issued id and access tokens from the source of your choice (backend system, etc).<\/li><li><strong>Account Recovery with Verified ID<\/strong> &#8211; special case where you do account recovery when a user is completely locked out and you need a hook to compare the claims in the presented Verified ID, issued by an identity proofing partner, with some backend HR system, etc.<\/li><li><strong>Attribute Collection Start &amp; Submit<\/strong> &#8211; When a customer signs up to your CIAM application and you want to prefill and validate claims captures during signup. In the Start extension, you can fetch values from your backend order systems (perhaps the customer already exists) or derive values like geo-location based on ip address. In the Submit extension, you get a chance to validate and clean up entered data.<\/li><li><strong>EmailOtpSend<\/strong> &#8211; If you want to roll your own sending of OTP codes to end users via email or SMS. <\/li><li><strong>Password Migration<\/strong> &#8211; It is a classic problem when you migrate users in CIAM systems that you don&#8217;t know the passwords of users in the system you migrate from. This extension can be used to reach out to the system being retired and validate the password during a migration period. Once the other system says <em>OK<\/em>, Entra will set the password and know it from then on.  <\/li><\/ul>\n\n\n\n<p>In this post, I will show you how to create and configure a Token Issuance extension. In a later post, I will show you how to extend that with Password Migration.<\/p>\n\n\n\n<h2>Configuring a Custom Authentication Extension<\/h2>\n\n\n\n<p>In your Entra External tenant, you need to do three things:<\/p>\n\n\n\n<ol><li>Register an application with permission <em>CustomAuthExtension.Receive.Payload<\/em> that will be used for creating an access token to call you custom API. The AppID will show up as the &#8216;aud&#8217; claim. <\/li><li>Define your custom extension so Entra knows how to call you<\/li><li>Configure custom claims provider on the service principal for the app that end users will login to.<\/li><\/ol>\n\n\n\n<h3><strong>Step 1 &#8211;  <span style=\"font-size: revert; color: initial;\">Register an application with permission <\/span><em style=\"font-size: revert; color: initial;\">CustomAuthExtension.Receive.Payload<\/em> <\/strong><\/h3>\n\n\n\n<p>There need not be a lot of detail for the application, because you only use it to give Entra permission to call your API. Just give the app a name (like CIAM-custom-auth-extension-app) and add the permission. Microsoft Entra ID uses OAuth 2.0 client credentials grant flow to secure the call to your API endpoint and in that process it uses this permission for scope. <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"641\" height=\"134\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-151513.png\" alt=\"\" class=\"wp-image-7991\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-151513.png 641w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-151513-300x63.png 300w\" sizes=\"(max-width: 641px) 100vw, 641px\" \/><\/figure>\n\n\n\n<p>The quirky thing to get right is to set the IdentifierUris to the correct value. It must have an entry with the value of<\/p>\n\n\n\n<p>api:\/\/&lt;your-FQDN-where-you-host-your-API&gt;\/&lt;appID&gt;. This is to make sure the Entra knows that it is calling a legitimate endpoint authorized by you. In my dev environment, it looks like below. The guid is just the AppID of your app. <\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"571\" height=\"64\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-151625.png\" alt=\"\" class=\"wp-image-7992\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-151625.png 571w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-151625-300x34.png 300w\" sizes=\"(max-width: 571px) 100vw, 571px\" \/><figcaption>When you use the Entra Admin Center and try to update the endpoint, maybe because it changes during dev\/debug cycles, it will display errors if the new endpoint you are trying to add doesn&#8217;t match in the identifierUris array. A tip is to first update the identifierUris array in the app manifest and then change the API endpoint in UI.<\/figcaption><\/figure>\n\n\n\n<h3><strong>Step 2- Define your custom extension<\/strong><\/h3>\n\n\n\n<ol><li>Entra Admin Center &gt; External Identities &gt; Custom authentication extensions &gt; + Create a custom extension<\/li><li>Select <em>TokenIssuanceStart<\/em> and click Next<\/li><\/ol>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"590\" height=\"455\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152521.png\" alt=\"\" class=\"wp-image-7993\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152521.png 590w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152521-300x231.png 300w\" sizes=\"(max-width: 590px) 100vw, 590px\" \/><figcaption>In the next screen you can either pick the pre-registered app with the correct permissions, or let the wizard create one for you. If this is your first time trying this feature out, creating a new one is the easiest option.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"683\" height=\"468\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152659.png\" alt=\"\" class=\"wp-image-7994\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152659.png 683w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152659-300x206.png 300w\" sizes=\"(max-width: 683px) 100vw, 683px\" \/><\/figure>\n\n\n\n<p>The third screen is where you define what claims are returned from your extension. Here you should add them just as you return them from your API &#8211; no name decoration needed. If your API returns more claims than what you registered, Entra will simply ignore them. After you have created your extension you can add or remove claims as you like if your API starts to return more claims in the next release.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"441\" height=\"345\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152912.png\" alt=\"\" class=\"wp-image-7995\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152912.png 441w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-152912-300x235.png 300w\" sizes=\"(max-width: 441px) 100vw, 441px\" \/><\/figure>\n\n\n\n<h3><strong>Step 3 &#8211; <span style=\"font-size: revert; color: initial;\">Configure custom claims provider on the service principal<\/span> <\/strong><\/h3>\n\n\n\n<p>In order to create the binding between the custom extension and the app that end users use to login to, we need to add the extension as a custom claims provider to your app&#8217;s service principal.<\/p>\n\n\n\n<ol><li>Entra Admin Center > Enterprise apps > Select your app > Singel sign-on<\/li><li>Click on Edit for <em>Attributes &amp; Claims<\/em><\/li><li>Open the <em>Advanced settings<\/em> section and click Edit next to <em>Custom claims provider<\/em><\/li><li>Select your custom extension in the dropdown list and click Save<\/li><\/ol>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"662\" height=\"356\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-153549.png\" alt=\"\" class=\"wp-image-7996\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-153549.png 662w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-153549-300x161.png 300w\" sizes=\"(max-width: 662px) 100vw, 662px\" \/><figcaption>In order for your application to accept the claims your custom extension provides, you need to configure it i the app manifest of the app that end users login to (now the extension app). Open the manifest and look for attribute <em>acceptMappedClaims<\/em> and set it to <em>true<\/em>.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"277\" height=\"40\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/Screenshot-2026-09-16-153936.png\" alt=\"\" class=\"wp-image-7997\"\/><\/figure>\n\n\n\n<h2>How Entra is calling your API<\/h2>\n\n\n\n<p>Microsoft <a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-extension-tokenissuancestart-setup\" target=\"_blank\" rel=\"noreferrer noopener\">documentation<\/a> provides sample code for an Azure Function that uses Managed Identity and pre-auth before hitting your code. If you host your on API in an aspnet app, you need to be aware of how to authenticate the incoming call.<\/p>\n\n\n\n<p>The access token your app will receive will be issued by your tenant (see <em>iss<\/em> and <em>tid<\/em> claims). The <em>aud<\/em> claim will be the appId of the app you registered with the <em>CustomAuthExtension.Receive.Payload<\/em> permission. This is how you can be sure that this is an authorized caller. The claims <em>azp<\/em> and <em>oid<\/em> is Entra&#8217;s multi-tenant app for making the call.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n  \"aud\": \"a3126a46-def7-49e2-8e96-2e46c6252503\",  \/\/ appId of the registered custom extension app\n  \"iss\": \"https:\/\/90a979a4-58af-4784-b3c3-9a36fb1d6436.ciamlogin.com\/90a979a4-58af-4784-b3c3-9a36fb1d6436\/v2.0\",\n  \"azp\": \"99045fe1-7639-4a75-9d4a-577b6ca3810f\",  \/\/ appID for SP \"Azure Active Directory Authentication Extensions\"\n  \"oid\": \"34d97ed7-a7b1-4f58-a9a7-85770c2d5112\",  \/\/ objectID for -\"-\n  \"sub\": \"34d97ed7-a7b1-4f58-a9a7-85770c2d5112\",\n  \"tid\": \"90a979a4-58af-4784-b3c3-9a36fb1d6436\",\n}\n<\/code><\/pre>\n\n\n\n<p>If your extension supports multiple client apps where the behaviour varies between the apps, the access token will not be your friend. Instead in the request JSON there will be something called the <em>authenticationContext<\/em> that have the details you are looking for.  In the <em>clientServicePrincipal<\/em>, you have the details of the app that the end user is signing in to.   <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">\"authenticationContext\": {\n  \"correlationId\": \"a2b1f0dd-19f6-43a9-be18-3ba36ad419a0\",\n  \"client\": {\n    \"ip\": \"321.418.945.255\",\n    \"locale\": \"en-gb\",\n    \"market\": \"en-gb\"\n  },\n  \"protocol\": \"OAUTH2.0\",\n  \"clientServicePrincipal\": {\n    \"id\": \"a70c1626-70cf-4657-9e4e-bbd8e8908d4e\",\n    \"appId\": \"4678df57-ae15-4440-af00-0bb82fbc003d\",\n    \"appDisplayName\": \"ciam-test-app\",\n    \"displayName\": \"ciam-test-app\"\n  },<\/code><\/pre>\n\n\n\n<p>Who the user is currently logging in is passed in the <em>user<\/em> section of the request. The <em>userPrincipalName<\/em> is in the Entra External decorated format, but the <em>mail<\/em> attribute is more suitable for doing lookups in external systems.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">\"user\": {\n  \"id\": \"111111-222-333-4444-5555555555\",\n  \"userPrincipalName\": \"111111-222-333-4444-5555555555@foobar.onmicrosoft.com\",\n  \"userType\": \"Member\",\n  \"createdDateTime\": \"2026-09-15T13:08:10Z\",\n  \"displayName\": \"John Doe\",\n  \"mail\": \"johndoe@hotmail.com\"\n}\n<\/code><\/pre>\n\n\n\n<h2>What you return to Entra<\/h2>\n\n\n\n<p>If you return a 400 Bad Request or any other unsuccessful HTTP status code, the tokens will not be issued and the login flow will be unsuccessful. If you return a 200 OK, Entra expects a JSON response like the following. Your app simly adds your custom claims in the claims section.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n  \"data\": {\n    \"@odata.type\": \"microsoft.graph.onTokenIssuanceStartResponseData\",\n    \"actions\": [\n      {\n        \"@odata.type\": \"microsoft.graph.tokenIssuanceStart.provideClaimsForToken\",\n        \"claims\": {\n          \"dateOfBirth\": \"1980-01-01\",\n          \"customRoles\": [\n            \"Moderator\",\n            \"Writer\",\n            \"Reader\"\n          ],\n          \"memberSince\": \"2026-09-15\"\n        }\n      }\n    ]\n  }\n} \n<\/code><\/pre>\n\n\n\n<p>There is another &#8220;gotcha&#8221; and that is that Entra expects you to return the response within 2 seconds. If you are debugging and hanging on a breakpoint, you will see the second call coming in after the first 2 seconds and then 2 seconds more, the login flow in the browser will fail.<\/p>\n\n\n\n<h2>Cleanup after testing<\/h2>\n\n\n\n<p>If you want to cleanup after testing, you need to delete the resources in the following order:<\/p>\n\n\n\n<ol><li>Enterprise apps &gt; Remove the Custom Claims Provider on the Enterprise app (otherwise, it will be &#8220;in-use&#8221;)<\/li><li>External identities &gt; Remove the Custom Authentication Extension<\/li><li>App registrations &gt; Remove the app with the <em>CustomAuthExtension.Receive.Payload<\/em> permission<\/li><\/ol>\n","protected":false},"excerpt":{"rendered":"<p>In both Entra Workforce tenants and Entra External tenants, you can add your own custom authentication extensions. In a workforce tenant, you should think twice before adding custom code to your authentication scenarios as you most likely don&#8217;t need it. In external tenants, there is a likelihood that you will need it. What you can [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[453,457],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7989"}],"collection":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=7989"}],"version-history":[{"count":5,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7989\/revisions"}],"predecessor-version":[{"id":8003,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7989\/revisions\/8003"}],"wp:attachment":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=7989"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=7989"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=7989"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}