Entra App Registration Setup
This guide walks through the Azure / Entra-side configuration required to let users sign in to Wayd with Microsoft Entra ID. It covers:
- Creating the API app registration (the OAuth2 resource — what tokens are issued for)
- Creating the client app registration (the OAuth2 client — what the frontend uses to log in)
- Wiring them together (scope, permissions, consent)
- Getting the right values into Wayd's configuration
Before you start
Decide two things first:
-
Single Entra tenant vs multiple Entra tenants. Pick single-tenant on the app registration if this deployment only accepts users from one Entra tenant — the common case. Pick multi-tenant if users from more than one Entra tenant need to sign in. The multi-tenant label just tells Entra it's allowed to issue tokens for other tenants' users; Wayd still restricts which specific tenants are accepted via the runtime
AllowedTenantIdsallowlist.(Note: "tenant" here means an Entra tenant — the directory a user belongs to in Microsoft's identity system. Wayd has no separate app-level tenant concept today.)
-
Who will consent. The first time a user from a given tenant signs in, someone with admin rights in that tenant must grant consent for Wayd to read basic profile info. Plan for that.
You need two app registrations because that's the standard OAuth2 shape: a client (the SPA, which never holds secrets) and a protected resource (the API, which declares scopes and validates incoming tokens). Microsoft recommends this split even when both apps are "yours" — do not collapse them into one.
Step 1 — Create the API app registration
This is the backend. It defines what "being authenticated to Wayd's API" means.
- Azure Portal → Microsoft Entra ID → App registrations → New registration.
- Name it something like
Wayd API ({env})— e.g.,Wayd API (prod). - Supported account types:
- Single tenant only - Default Directory — the common choice. Use this when Wayd only accepts users from this Entra tenant.
- Multiple Entra ID tenants — use this only when Wayd needs to accept users from more than one Entra tenant. Select the Allow only certain tenants (Preview) sub-option and list each tenant ID you intend to onboard. You'll also mirror that list in Wayd's
AllowedTenantIdsconfig — see the note below on why we want both. - ❌ Any Entra ID Tenant + Personal Microsoft accounts — do not pick this. Wayd always rejects personal Microsoft accounts (MSA) via the allowlist, so tokens issued under this setting fail validation. The option misrepresents the app's intent.
- ❌ Personal accounts only — do not pick this. Every sign-in will be rejected.
- Redirect URI: leave blank. The API never receives redirects.
- Click Register.
When you pick Multiple Entra ID tenants, Entra offers an Allow only certain tenants (Preview) sub-option that restricts which tenants it will even issue tokens for. Use it, and also keep Wayd's AllowedTenantIds allowlist populated — defense in depth.
Why restrict at Entra. With Allow all tenants, a user in any Entra tenant can successfully acquire a valid, signed Entra access token with aud = Wayd's API client ID. Wayd rejects it at /api/auth/exchange (401, tid not in allowlist), so they can't use it — but they got issued a real signed credential naming your resource. Narrow exposure, but non-zero, and easy to close: set Entra to refuse the token in the first place.
Why also keep AllowedTenantIds. It's the runtime allowlist Wayd's exchange handler actually enforces, and a MicrosoftEntraId provider can't be saved with an empty list (fail-safe default). Treat the Entra-side restriction as the outer perimeter and AllowedTenantIds as the inner one.
Trade-offs to accept.
- Two places to update. Onboarding a new tenant means an Azure portal trip and editing the provider's
AllowedTenantIdsin Settings → Access → Identity Providers. Put both in your onboarding runbook so they can't drift. - Preview dependency. Allow only certain tenants is still marked Preview. If Microsoft changes or removes it, you fall back to
AllowedTenantIdsas the sole gate — recoverable, not catastrophic.
Copy the Application (client) ID shown on the Overview page — you'll need it everywhere. We'll call it {api-client-id}.
Set the token version to v2
By default, new app registrations issue v1 access tokens. Wayd expects v2. Change it:
- Open the registration → Manage section → Manifest.
- In the JSON editor, find the
apiobject (near the top) and itsrequestedAccessTokenVersionfield:{"api": {"requestedAccessTokenVersion": null,...}} - Change
nullto2:{"api": {"requestedAccessTokenVersion": 2,...}} - Save.
The field is api.requestedAccessTokenVersion in the current Microsoft manifest schema. Older tutorials and blog posts reference accessTokenAcceptedVersion at the root of the manifest — that name belongs to a deprecated schema and will not take effect on current app registrations. See the Microsoft app manifest reference.
Why v2: v2 tokens put the bare client-ID GUID in the aud claim (e.g., 2fcc0e63-...) and the v2.0 URL in the iss claim. v1 tokens use api://{clientId} for aud and a sts.windows.net issuer. Wayd's validator expects v2 shape. Mixing the two produces a hard-to-diagnose 401 Invalid token on every exchange attempt.
Expose an API scope
The client app needs a named scope to request access to. Create one:
- Registration → Expose an API.
- Application ID URI → click Add → accept the default (
api://{api-client-id}) → Save. - Scopes defined by this API → Add a scope:
- Scope name:
access_as_user - Who can consent: Admins and users
- Admin consent display name:
Access Wayd as the signed-in user - Admin consent description:
Allows the app to call Wayd's API as the signed-in user. - State: Enabled
- Add scope
- Scope name:
The full scope string is api://{api-client-id}/access_as_user. Copy it — you'll need it as the Scopes value when you create the identity provider in Settings → Access → Identity Providers (see Step 3).
Create a client secret (only if you want the Entra people-sync connector)
Wayd's token-exchange auth flow does not need a client secret — it validates incoming user tokens against Entra's public JWKS, which is public-key-only. Sign-in works without a secret.
A secret is only needed for the Microsoft Graph people-sync connector (Wayd calling Graph via the client-credentials flow). The secret lives on the Entra connection row in the database — an admin enters it when creating the connection in Settings → System → Connections. It is never read from configuration or environment variables.
To create the secret:
- Registration → Certificates & secrets → Client secrets → New client secret.
- Description:
Entra connector - {env}(e.g.,Entra connector - prod). - Expires: pick a policy that matches your rotation cadence (Azure defaults to 180 days; 12 or 24 months is more manageable).
- Add.
- Copy the secret
Valueimmediately — the portal shows it once and never again. If you lose it you'll have to delete this credential and create a new one.
Then create the Entra connection in Settings → System → Connections, pasting the secret into the connection's credentials (it's encrypted at rest). To rotate later, edit the connection row and re-enter the new secret.
Grant Microsoft Graph permissions (only if you want the Entra people-sync connector)
The Entra connector authenticates as the API app registration (client-credentials flow) and calls Microsoft Graph to read user data. Grant it the minimum permissions needed:
- API registration → API permissions → Add a permission → Microsoft Graph → Application permissions.
- Add the following:
User.Read.All— required. Without it Graph returns users with emptygivenName/surname, and the sync silently filters all rows out.GroupMember.Read.All— only if you plan to scope the sync to an Entra group (the connection's optionalAllUsersGroupObjectIdfield).
- Click Grant admin consent for
{tenant name}→ Yes. Application permissions only take effect after admin consent.
If you skip this step, sync runs will complete "successfully" but report 0 employees fetched. The Wayd app logs will show the Graph response was received but every row was dropped by the name filter.
Step 2 — Create the client (SPA) app registration
This is the frontend. It's what the browser OIDC client (oidc-client-ts) identifies as when initiating the authorization flow.
- Azure Portal → App registrations → New registration.
- Name:
Wayd Client ({env})— e.g.,Wayd Client (prod). - Supported account types: pick the same option as the API registration — Single tenant only or Multiple Entra ID tenants. Never the personal-accounts variants (see Step 1).
- Redirect URI:
- Platform: Single-page application (SPA)
- URL: your frontend's origin +
/login— e.g.,https://wayd.ahstech.com/loginfor prod,http://localhost:3000/loginfor dev
- Register.
Copy the Application (client) ID from the Overview page — we'll call it {client-id}.
Add the API permission
The client registration needs permission to request tokens for the API registration.
- Client registration → API permissions → Add a permission.
- Tab APIs my organization uses → search for your API registration name → select it.
- Delegated permissions → check
access_as_user→ Add permissions. - Back on the API permissions list, click Grant admin consent for
{tenant name}→ Yes.
Admin consent pre-authorizes every user in your Entra tenant so they don't see a consent prompt on first login. If you're accepting users from multiple Entra tenants, each onboarded tenant's admin will need to grant this consent for their users.
(Optional) Pre-authorize the client in the API registration
This is a nice-to-have that avoids separate user consent when clients outside your tenant sign in:
- API registration → Expose an API → Authorized client applications → Add a client application.
- Client ID:
{client-id}. - Check the
access_as_userscope. - Add application.
Skip this if everything stays in one Entra tenant; it's only relevant if clients in other Entra tenants need to call the API.
Step 3 — Create the provider in Wayd
You now have two GUIDs. Identity providers are managed entirely in the database — there is no config-file or environment-variable setup. As an admin, go to Settings → Access → Identity Providers, choose Create Identity Provider, pick Microsoft Entra ID as the provider type, and enter the values below.
| UI field | Value | Purpose |
|---|---|---|
Authority | https://login.microsoftonline.com/{your-tenant-id}/v2.0 | OIDC discovery / signature validation endpoint |
ClientId | {client-id} (SPA registration) | Browser OIDC client uses this as client_id when redirecting to Entra |
Scopes | openid profile email api://{api-client-id}/access_as_user | The API scope makes Entra issue an access token with aud = {api-client-id} |
Audience | {api-client-id} (API registration) | Backend pins the token's aud claim to this value |
AllowedTenantIds | {your tenant ID} | Tenants permitted to sign in |
Five things to note:
ClientId: the client (SPA) app registration's client ID — the value the browser presents to Entra at sign-in.Scopes: include the API scope string from Step 1's "Expose an API" section alongside the standardopenid profile email. Requesting the API scope causes Entra to issue an access token withaud = {api-client-id}, which is what Wayd's token validator expects. Without it, Entra issues a token withaud = {client-id}and every exchange fails with 401.Audience: bare GUID of the API registration (not the client, notapi://...). This is why Step 1's v2 switch matters — with v1 tokens this value would need to beapi://{api-client-id}instead.Authority: use the tenant-specific URL (https://login.microsoftonline.com/{your-tenant-id}/v2.0) for single-tenant deployments. Use/common/v2.0only for true multi-tenant deployments; theAllowedTenantIdsallowlist still controls which tenants are accepted at runtime.AllowedTenantIds: must be non-empty. The provider can't be saved with an empty list — that's a deliberate fail-safe default.
Once saved, use Test in the provider list (or Test discovery on the provider's page) to verify the Authority resolves a valid .well-known/openid-configuration document before anyone signs in. The database is the source of truth from creation onward; to onboard additional tenants or change scopes later, edit the row in the same UI.
Step 4 — Verify
Sign in to the frontend. Then:
- Decode the access token that the frontend sends to
/api/auth/exchange. Open DevTools → Network → find thePOST /api/auth/exchangerequest → copy thesubjectTokenfield from the body → paste into jwt.ms (it decodes locally; nothing is sent anywhere). - Confirm:
aud={api-client-id}(bare GUID, noapi://prefix — if you see the prefix, token version is wrong)iss=https://login.microsoftonline.com/{your tenant ID}/v2.0ver="2.0"tid={your tenant ID}
- The exchange should return a Wayd JWT, and the app should load.
Troubleshooting
401 Invalid token on /api/auth/exchange. Decode the token at jwt.ms and compare:
| Token field | Must equal |
|---|---|
aud | The provider's Audience (bare client-ID GUID for v2) |
tid | One of the provider's AllowedTenantIds |
iss | https://login.microsoftonline.com/{tid}/v2.0 for v2 |
ver | "2.0" |
exp | In the future |
If aud has api:// in it but the provider's Audience is a bare GUID, the API registration is still issuing v1 tokens. Redo Step 1's v2 switch. Users will need to get a fresh token — have them clear site data (DevTools → Application → Storage → Clear site data) and sign in again.
401 Invalid token on /api/auth/exchange with no matching OidcProvider row. The provider name the frontend sent doesn't match any enabled row in Identity.OidcProviders. Either the provider hasn't been created yet (see Step 3) or an admin disabled the row via the Settings UI. Confirm the row exists and IsEnabled = 1; OidcTokenValidator logs the unknown-provider rejection.
Microsoft Entra ID row missing AllowedTenantIds. The OidcProvider.Create entity invariant requires at least one tenant ID for MicrosoftEntraId rows; the validator and admin form both enforce it, so the provider can't be saved without a tenant list.
User signs in, immediately redirects back to /login. Usually means the exchange succeeded but the login page's storage write raced a redirect. Check browser console for the precise error; most commonly it's a CORS failure (see Configuration → CORS) or a mismatched NEXT_PUBLIC_API_BASE_URL.
User from a new Entra tenant gets rejected. Add their tenant ID to AllowedTenantIds. The tenant's admin also needs to admin-consent the access_as_user permission (see Step 2) — otherwise users see Microsoft's consent prompt on first sign-in, and individual users may not be allowed to consent depending on the tenant's policies.
403 Registration is disabled for this identity provider. The token validated fine, but the provider's AllowAutoRegistration is off (the default) and this user has no existing account. Either enable auto-registration on the provider or pre-create the user. See User provisioning.
403 Registration is restricted to users with an employee record. Auto-registration is on, but the provider's RequireEmployeeRecord is on and no Employee matches the user's email. Add a matching employee (e.g. via People Sync), turn off the employee-record requirement on the provider, or pre-create the user.
People sync / Graph calls fail with 401. The Entra connection's client secret has expired, was never set, or is wrong. Edit the connection in Settings → System → Connections and re-enter the secret. This is independent of the token-exchange flow — login still works, only Graph-backed people sync breaks. See Create a client secret.
References
- App manifest reference — authoritative on
requestedAccessTokenVersion. - Access tokens — v1 vs v2 differences.
- Access token claims reference — meaning of
aud,iss,tid,ver, etc. - Configuration — full Wayd-side auth config reference, including the identity model and tenant-migration flow.