Microsoft Entra ID
Use an Entra app role to decide who can reach your application. This walkthrough
connects one workforce tenant, grants ordinary portal access to signed-in users,
and requires the App.Access role for /app.
It targets caddy-security v1.3.0 with go-authcrunch v1.3.8. The configuration
keyword remains driver azure. Local OIDC fixtures check the released driver
and policies; complete the login tests in your own tenant to verify consent,
assignments, and Conditional Access.
Before you start
Complete Install and verify. You need permission to
register an application and manage its enterprise application's assignments,
two test users, and a public HTTPS portal. This example uses
https://auth.example.com/auth/; replace the hostname throughout.
The main walkthrough uses one Microsoft Entra workforce tenant in the public cloud. Other account types have different requirements. The OAuth and OIDC overview explains how the provider, portal, and application policy fit together.
Register the application
In the Microsoft Entra admin center, select the correct directory, then open Entra ID → App registrations → New registration. Use these settings:
| Setting | Value |
|---|---|
| Name | A recognizable application name, such as AuthCrunch Example |
| Supported account types | Accounts in this organizational directory only |
| Redirect platform | Web |
| Redirect URI | https://auth.example.com/auth/oauth2/azure/authorization-code-callback |

After registration, copy Application (client) ID and Directory (tenant) ID
from Overview. Use the directory's GUID for ENTRA_TENANT_ID. The callback's
azure is the AuthCrunch realm, not the tenant ID. See
Microsoft's app registration guide.
Screenshots: locate app registration identifiers
Microsoft now calls Azure Active Directory Microsoft Entra ID. The earlier screens remain useful for locating app registrations and distinguishing the application ID from the directory ID.

Under Certificates & secrets → Client secrets, create a secret and store its Value, not its Secret ID. Supply that value only to the AuthCrunch server. Track its expiration in your deployment's credential process. The example uses server-side authorization code flow with a client secret and S256 PKCE; it does not require the implicit-grant checkboxes or a public-client flow. See Microsoft's authorization code flow.
Screenshots: create and save a client secret
The credential value in the second screenshot has been redacted. Copy the Value of your newly created credential into the server environment, rather than its Secret ID.

Define and assign application access
In this app registration, open App roles → Create app role:
| Field | Value |
|---|---|
| Display name | Example app member |
| Allowed member types | Users/Groups |
| Value | App.Access |
| Description | Can use the example application |
| Enabled | Yes |
The role's Value is the string matched by AuthCrunch. Its display name and generated role ID are different values.
Open Entra ID → Enterprise apps, select the corresponding enterprise
application, then Users and groups → Add user/group. Assign Alice to
Example app member and leave Bob without that role. For this sign-in app,
Entra emits the assigned value in the ID token's roles claim. See
Microsoft's app role guide.
| Stage | Value |
|---|---|
| Entra ID token | roles: ["App.Access"] |
| Portal transform | Matches realm azure and role App.Access |
| AuthCrunch token | Adds app/member |
| Application policy | Requires app/member |
Review the enterprise application's Properties → Assignment required?.
When it is Yes, Entra can reject an unassigned user before AuthCrunch receives
a token. When it is No, an otherwise eligible user can sign in without an
app role, letting you verify AuthCrunch's 403 response. Keep your organization's
intended setting; do not broaden sign-in just to reproduce a test. A separate
non-access role can admit a test user without granting App.Access when
assignment is required. Use ordinary test accounts rather than tenant admins.
See Entra assignment behavior.
Configure AuthCrunch
Save this as Caddyfile and replace auth.example.com in the site address and
policy URL. The complete file is embedded from the
canonical Entra example.
# Set the public portal hostname and ENTRA_TENANT_ID before use.
{
admin off
persist_config off
security {
oauth identity provider azure {
driver azure
realm azure
client_id {env.ENTRA_CLIENT_ID}
client_secret {env.ENTRA_CLIENT_SECRET}
scopes openid email profile
tenant_id {$ENTRA_TENANT_ID}
email claim check disabled
login icon text "Microsoft Entra ID"
}
authentication portal myportal {
enable identity provider azure
crypto default token lifetime 900
crypto key sign-verify {env.JWT_SHARED_KEY}
cookie path /
ui {
links {
"Example app" /app icon "las la-external-link-alt"
"My identity" /auth/whoami icon "las la-user"
}
}
transform user {
match realm azure
action add role authp/user
}
transform user {
match realm azure
match role App.Access
action add role app/member
}
}
authorization policy apppolicy {
set auth url https://auth.example.com/auth/
crypto key verify {env.JWT_SHARED_KEY}
allow roles app/member
}
}
}
auth.example.com {
route {
redir /auth /auth/ 308
authenticate /auth/* with myportal
@app path /app /app/*
route @app {
authorize with apppolicy
respond "You reached the Entra-protected app." 200
}
respond "AuthCrunch is running. Open /app to begin." 200
}
}
Set the environment:
export ENTRA_TENANT_ID='YOUR_DIRECTORY_TENANT_GUID'
export ENTRA_CLIENT_ID='YOUR_APPLICATION_CLIENT_ID'
export ENTRA_CLIENT_SECRET='YOUR_CLIENT_SECRET_VALUE'
export JWT_SHARED_KEY="$(openssl rand -hex 32)"
{$ENTRA_TENANT_ID} expands when the Caddyfile is parsed. The credentials and
shared signing key use runtime {env.VARIABLE} placeholders. For a managed
service, use its protected environment and preserve the signing key across
restarts. Never put a real client secret in the Caddyfile or browser code.
The tenant ID selects the discovery document at
https://login.microsoftonline.com/TENANT_GUID/v2.0/.well-known/openid-configuration.
Inspect that document from the AuthCrunch host and confirm its issuer identifies
your directory. The driver discovers the authorization, token, and signing-key
endpoints and validates the ID token against the client ID and exact issuer.
./bin/authcrunch adapt --adapter caddyfile --config Caddyfile >/dev/null
./bin/authcrunch run --config Caddyfile
Adaptation verifies syntax; startup contacts the discovery/key endpoints. The host needs outbound HTTPS, public DNS, and working certificate issuance for the portal. The example disables the admin endpoint; stop with Ctrl+C and restart after edits.
Why email is optional here
Entra does not guarantee an email claim for every account. The example requests
openid email profile but uses app roles, not email, to authorize access.
It therefore includes email claim check disabled in the provider block.
Signature, issuer, audience, state, nonce, and PKCE checks remain enabled.
An optional email claim can improve display information, but requesting it cannot
create an address the account does not have. preferred_username is not a
substitute for a verified email, and this driver's claim parser does not copy it,
oid, or tid into the portal token. The portal sub comes from the provider's
app-specific subject. See Entra's ID-token claims
and the released parser.
If another portal feature or transform requires email, configure and verify that
claim separately. Removing email claim check disabled restores the driver's
email-presence requirement; it does not verify ownership of an address.
Verify sign-in and denial
- Open
https://auth.example.com/appand choose Microsoft Entra ID. - Sign in as Alice and complete any tenant consent or Conditional Access steps.
- Open My identity (
/auth/whoami). Confirm realmazureand rolesApp.Access,authp/user, andapp/member. - Select Example app or reopen
/app. Expect the protected response. - Use a separate browser session for Bob. If Entra admits him without
App.Access, verify a 403 on/app,/app/, and/app/nested. An Entra assignment rejection is a separate, earlier boundary.
Removing an assignment does not rewrite an existing AuthCrunch token. This example uses a 900-second token lifetime; obtain a new token when testing changed roles. Portal sign-out does not end the Microsoft SSO session in this configuration, so use isolated browser sessions to test different users.
After these checks, replace the protected respond with your application's
reverse_proxy, keeping authorize before it and both /app matchers.
Group claims as an alternative
If your deployment uses group IDs, configure the app registration's
Token configuration → Add groups claim and include the needed groups in the
ID token. With the default format, values are group object IDs, not display
names. Replace the App.Access transform with a matcher for the exact emitted ID:
transform user {
match realm azure
match role "YOUR_GROUP_OBJECT_ID"
action add role app/member
}
AuthCrunch combines the ID token's groups into portal roles. Keeping both the
group and app-role transforms grants access through either rule. To require
both, put both matchers in one transform. See
Microsoft's group-claim configuration.
Microsoft limits group lists in JWTs to 200 entries, including nested groups. Over that limit, the token can contain an overage indicator instead of the list. This release does not resolve Entra group overage through Microsoft Graph. A missing group must not grant app access; prefer the explicit app role or a suitably limited group claim. Adding Graph permissions alone does not implement the missing lookup.
Other account types
| Account population | Configuration boundary |
|---|---|
| One workforce tenant | Use the directory GUID as in this walkthrough. Guest users still need the intended assignment and app role. |
| Multiple workforce tenants | organizations and common discovery return a templated issuer. This release compares issuer strings literally and does not implement tenant-template validation. Do not use the driver's default common as a working multitenant configuration. |
| Personal Microsoft accounts (Live, Xbox, Outlook.com) | Register an app that supports personal accounts and use tenant_id consumers. Use an explicit account policy, not the workforce app-role setup above. |
The public consumers discovery document supplies the fixed Microsoft-account
issuer. To adapt the example for personal accounts, replace {$ENTRA_TENANT_ID}
with consumers and replace the App.Access transform with:
transform user {
match realm azure
match sub "REPLACE_WITH_PERSONAL_ACCOUNT_SUB"
action add role app/member
}
Sign in to the portal first, copy the intended account's sub from
/auth/whoami, update the matcher, restart, and log in again. Entra subjects are
specific to the app registration. Check an unselected account is denied. Keep
the email-presence exception if the account does not supply email.
Microsoft documents tenant and issuer validation; the release's validator explains the exact-match limit. An app registration accepting more account types does not add multitenant validation to this driver. External ID customer tenants, B2C policies, sovereign clouds, and live personal-account consent are outside the validated walkthrough.
Troubleshoot
| Symptom | Check |
|---|---|
AADSTS50011 / redirect mismatch | Register the exact Web callback, including /auth/ and realm azure. |
| Client authentication fails | Use the secret Value rather than Secret ID, the correct client ID, and an unexpired credential. |
Issuer contains {tenantid} | Set a concrete directory GUID instead of common or organizations; do not disable token checks. |
| Alice gets 403 | Verify her assignment to this enterprise application, role Value App.Access, and a fresh ID token for this client. |
| Bob is blocked by Entra | Check Assignment required and tenant policy; this is earlier than AuthCrunch's app policy. |
An unassigned user reaches /app | Check for another grant of app/member or a policy that allows authp/user. |
| Email is missing | Expected for some accounts; the example does not authorize by email. Check dependent portal features separately. |
| A group member gets 403 | Inspect the ID token for the group object ID or overage, and confirm the claim is included in the ID token. |
| Sign-in resumes after portal logout | The Microsoft SSO session survives; use a separate browser session for another user. |
Continue to Generic OpenID Connect for claim extraction and discovery details.