Managing external identities to enable secure access for partners, customers, and other non-employees
Hello @Isaac Weston,
Hey Isaac, it turns out nothing magic was hiding in the Azure-Functions examples – you can absolutely host your custom auth extension in an ASP NET Core Web API. The catch is that Entra External ID calls your API using the OAuth2 client-credentials (app-only) flow, so:
• the inbound token has NO user (“scp”) scopes
• it HAS application permissions (in the “roles” claim)
You had defined only delegated scopes (“access_as_user”) in your API registration, and you’re validating for scp. The connector grabs an app‐only token, so there is no scp claim and your [Authorize] check fails with a generic auth error.
Here’s what you need to do:
- Expose an Application Permission (App Role) on your API registration – In Azure Portal > App registrations > your API > “App roles” > “+ Create app role” – Allowed member types = “Applications” – Give it a value, e.g. Identity.Access
- Grant that App Role to your custom-extension application – In your extension’s App registration > “API permissions” > “+ Add a permission” > “My APIs” > select your API > choose the new “Identity.Access” application permission – Click “Grant admin consent”
- Update your ASP NET Core code to validate roles instead of scopes In Program.cs something like:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(options => { builder.Configuration.Bind("AzureAd", options); // tell the middleware to look for "roles" (app perms) not "scp" options.TokenValidationParameters.RoleClaimType = "roles"; }); builder.Services.AddAuthorization(opts => { opts.AddPolicy("AppOnly", policy => policy.RequireRole("Identity.Access")); }); app.UseAuthentication(); app.UseAuthorization(); [ApiController] [Route("...")] public class MyController : ControllerBase { [HttpPost, Authorize(Policy="AppOnly")] public IActionResult GetClaims([FromBody] TokenIssuanceStartRequest req) { … } } - Call it exactly the way Entra External ID will: POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token • grant_type=client_credentials • client_id={your-extension-app-id} • client_secret={secret} • scope=api://{your-api-app-id-uri}/.default Then pass that token in Authorization: Bearer {the-access-token}
At that point your API will validate the JWT’s audience (your API’s App ID URI), issuer (your *.ciamlogin.com tenant), and the roles claim (“Identity.Access”) and return 200.
Hope that gets you unblocked!
Reference docs for more details:
• Custom Auth Extensions overview:
https://learn.microsoft.com/entra/external-id/customers/concept-custom-extensions
• Protect Web APIs with app-roles (no scp):
• OAuth2 client credentials grant flow:
https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow
• Troubleshooting custom extensions:
https://learn.microsoft.com/entra/identity-platform/custom-extension-troubleshoot
Note: This content was drafted with the help of an AI system. Please verify the information before relying on it for decision-making.