Configuring Azure AD OIDC for Backstage
Organizations standardized on Microsoft Entra ID (formerly Azure AD) want their portal to authenticate against the same directory as everything else, so a developer signs in once and the portal already knows their identity and groups. This how-to registers Backstage as an Entra application, wires the OIDC provider, and maps Entra groups onto catalog identity. It is an Entra-specific application of OIDC & SSO Configuration within Authentication, RBAC & Security Governance.
Prerequisites
- Backstage 1.20+ reachable over HTTPS at a stable URL.
- Rights to register an application in your Entra ID tenant, or an admin who will.
- Decide up front whether the portal reads group claims, since that changes the app’s API permissions.
- Client credentials injected at runtime as
${AZURE_CLIENT_ID},${AZURE_CLIENT_SECRET},${AZURE_TENANT_ID}— never committed.
Exact Configuration
-
Register the app in Entra. Set the redirect URI to the portal’s OIDC handler and note the client and tenant IDs.
Redirect URI: https://portal.example.com/api/auth/microsoft/handler/frame Supported account types: single tenant API permissions: openid, profile, email, User.Read (+ GroupMember.Read.All if reading groups) -
Wire the Microsoft provider in Backstage config.
# app-config.yaml # Requires @backstage/plugin-auth-backend-module-microsoft-provider >= 0.2.0 auth: environment: production providers: microsoft: production: clientId: ${AZURE_CLIENT_ID} clientSecret: ${AZURE_CLIENT_SECRET} tenantId: ${AZURE_TENANT_ID} additionalScopes: ['GroupMember.Read.All'] -
Resolve the signed-in user to a catalog identity so ownership and RBAC line up.
// packages/backend/src/authResolver.ts // Match the Entra email to a User entity signInResolver: async ({ profile }, ctx) => ctx.signInWithCatalogUser({ filter: { 'spec.profile.email': profile.email } }),
Validation
# Requires curl, jq, and a test account
# 1. The provider metadata is reachable
curl -s "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0/.well-known/openid-configuration" \
| jq '.issuer'
# Expected: the tenant issuer URL
# 2. Sign-in resolves to a catalog user (check portal logs after login)
grep -q "signInWithCatalogUser" backend.log && echo "resolver matched a user"
# Expected: resolver matched a user
# 3. Group claims arrive when requested
# Decode the ID token and look for the groups claim
echo "${ID_TOKEN}" | cut -d. -f2 | base64 -d 2>/dev/null | jq 'has("groups")'
# Expected: true when GroupMember.Read.All is granted
Edge Cases & Troubleshooting
| Symptom | Root Cause | Resolution |
|---|---|---|
| Redirect mismatch error | Registered URI differs from the portal’s handler | Match the redirect URI exactly, including the path |
| Sign-in works, no identity | Resolver finds no matching catalog user | Ingest users from Entra or relax the resolver filter |
| Group claim missing | GroupMember.Read.All not consented |
Grant and admin-consent the permission |
| Groups truncated | Group overage past the token limit | Read groups via the Graph API instead of the claim |
| Token audience rejected | Wrong tenant or client ID | Confirm tenantId and clientId match the registration |
Frequently Asked Questions
Do I need group claims, or is sign-in enough?
If your RBAC and ownership derive from Entra groups, you need them; if identity alone drives access, you can skip them and avoid the Graph permission entirely. Reading groups adds an admin-consented permission and, on large tenants, a Graph lookup to handle overage — so only opt in when groups actually feed authorization.
How does this differ from the Okta setup?
The OIDC mechanics are identical — register an app, wire the provider, resolve to a catalog user — only the provider module and permission model differ. If you have already configured Okta for Backstage, the Entra setup is the same shape with Microsoft-specific group-overage handling layered on top.