{"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-18T19:08:18","modified_gmt":"2026-09-18T17:08:18","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>Microsoft Entra supports <a rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-extension-tokenissuancestart-setup\" target=\"_blank\">custom authentication extensions<\/a> in both workforce and external tenants. The available extensions differ between the two 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; When migrating users between CIAM systems, a classic problem arises: you do not know the users&#8217; existing passwords. 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>This post focuses on Token Issuance in an Entra External ID tenant. I\u2019ll show how to configure the extension, connect it to an application, and implement the API that Entra calls. A later post will cover Password Migration in Entra External ID.<\/p>\n\n\n\n<h2><meta charset=\"utf-8\">Configuring a Custom Authentication Extension<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"1024\" height=\"450\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/CustomAuthExtension-_TokenIssuance_diagram-1024x450.jpg\" alt=\"\" class=\"wp-image-8037\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/CustomAuthExtension-_TokenIssuance_diagram-1024x450.jpg 1024w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/CustomAuthExtension-_TokenIssuance_diagram-300x132.jpg 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/CustomAuthExtension-_TokenIssuance_diagram-768x337.jpg 768w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/09\/CustomAuthExtension-_TokenIssuance_diagram.jpg 1100w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>In your Entra External tenant, there will be 5 things:<\/p>\n\n\n\n<ol><li>An app registration with permission <em>CustomAuthExtension.Receive.Payload<\/em> that will be used for creating an access token to call your custom API. The AppID will show up as the &#8216;aud&#8217; claim. <\/li><li>A Custom Authentication Extension that defines your custom API endpoint and references the app with the permission to call it via its identifierUri.<\/li><li>An app registration for a client app with a User Flow defined that end users will sign in to<\/li><li>A custom claims provider on the service principal for the app that end users will signs in to. Internally in Graph, this is called an Authentication Event Listener.<\/li><li>Your custom API endpoint<\/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 important part here is configuring identifierUris correctly. It must have a format like <em>api:\/\/&lt;your-FQDN-where-you-host-your-API>\/&lt;appID><\/em>. This is to make sure that 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>If you change the API endpoint during development, the Entra Admin Center may report an error when you try to update the extension. <\/figcaption><\/figure>\n\n\n\n<p><strong>Tip<\/strong>: Update identifierUris in the app manifest first. Then update the API endpoint in the Entra Admin Center.<\/p>\n\n\n\n<h3><strong>Step 2- Register the Token Issuance 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 your extension returns. 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; Create a client app with a User Flow<\/strong><\/h3>\n\n\n\n<p>Follow the Microsoft documentation for <a rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/external-id\/customers\/how-to-user-flow-sign-up-sign-in-customers\" target=\"_blank\">creating<\/a> a User Flow and <a rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/external-id\/customers\/how-to-test-user-flows\" target=\"_blank\">testing<\/a> it. You will need the app and the user flow to test that your tokens get the custom claims.<\/p>\n\n\n\n<h3><strong>Step 4 &#8211; <span style=\"font-size: revert; color: initial;\">Connect the extension to your client application<\/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 > Single 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 in the app manifest of the app that end users login to (not 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 rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/custom-extension-tokenissuancestart-setup\" target=\"_blank\">documentation<\/a> provides sample code for an Azure Function that uses Managed Identity and pre-auth before hitting your code. If you host your own 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 has 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 simply 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 after 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>Microsoft Entra supports custom authentication extensions in both workforce and external tenants. The available extensions differ between the two tenant types. Workforce &#8211; Token Issuance, Account Recovery with Verified ID External &#8211; Token Issuance, Attribute Collection Start &amp; Submit, EmailOtpSend, and Password Migration In detail, the different types can help you with: Token Issuance &#8211; [&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":12,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7989\/revisions"}],"predecessor-version":[{"id":8057,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/7989\/revisions\/8057"}],"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}]}}