Skip to main content

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:

  1. 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 AllowedTenantIds allowlist.

    (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.)

  2. 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.

  1. Azure Portal → Microsoft Entra ID → App registrations → New registration.
  2. Name it something like Wayd API ({env}) — e.g., Wayd API (prod).
  3. 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 AllowedTenantIds config — 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.
  4. Redirect URI: leave blank. The API never receives redirects.
  5. Click Register.
Why both Entra-side and Wayd-side tenant restriction?

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 AllowedTenantIds in 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 AllowedTenantIds as 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:

  1. Open the registration → Manage section → Manifest.
  2. In the JSON editor, find the api object (near the top) and its requestedAccessTokenVersion field:
    {
    "api": {
    "requestedAccessTokenVersion": null,
    ...
    }
    }
  3. Change null to 2:
    {
    "api": {
    "requestedAccessTokenVersion": 2,
    ...
    }
    }
  4. Save.
Field name

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:

  1. Registration → Expose an API.
  2. Application ID URI → click Add → accept the default (api://{api-client-id}) → Save.
  3. 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

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:

  1. Registration → Certificates & secrets → Client secrets → New client secret.
  2. Description: Entra connector - {env} (e.g., Entra connector - prod).
  3. Expires: pick a policy that matches your rotation cadence (Azure defaults to 180 days; 12 or 24 months is more manageable).
  4. Add.
  5. Copy the secret Value immediately — 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:

  1. API registration → API permissions → Add a permission → Microsoft Graph → Application permissions.
  2. Add the following:
    • User.Read.All — required. Without it Graph returns users with empty givenName/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 optional AllUsersGroupObjectId field).
  3. 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.

  1. Azure Portal → App registrations → New registration.
  2. Name: Wayd Client ({env}) — e.g., Wayd Client (prod).
  3. 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).
  4. Redirect URI:
    • Platform: Single-page application (SPA)
    • URL: your frontend's origin + /login — e.g., https://wayd.ahstech.com/login for prod, http://localhost:3000/login for dev
  5. 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.

  1. Client registration → API permissions → Add a permission.
  2. Tab APIs my organization uses → search for your API registration name → select it.
  3. Delegated permissions → check access_as_user → Add permissions.
  4. 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:

  1. API registration → Expose an API → Authorized client applications → Add a client application.
  2. Client ID: {client-id}.
  3. Check the access_as_user scope.
  4. 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 fieldValuePurpose
Authorityhttps://login.microsoftonline.com/{your-tenant-id}/v2.0OIDC discovery / signature validation endpoint
ClientId{client-id} (SPA registration)Browser OIDC client uses this as client_id when redirecting to Entra
Scopesopenid profile email api://{api-client-id}/access_as_userThe 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 standard openid profile email. Requesting the API scope causes Entra to issue an access token with aud = {api-client-id}, which is what Wayd's token validator expects. Without it, Entra issues a token with aud = {client-id} and every exchange fails with 401.
  • Audience: bare GUID of the API registration (not the client, not api://...). This is why Step 1's v2 switch matters — with v1 tokens this value would need to be api://{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.0 only for true multi-tenant deployments; the AllowedTenantIds allowlist 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:

  1. Decode the access token that the frontend sends to /api/auth/exchange. Open DevTools → Network → find the POST /api/auth/exchange request → copy the subjectToken field from the body → paste into jwt.ms (it decodes locally; nothing is sent anywhere).
  2. Confirm:
    • aud = {api-client-id} (bare GUID, no api:// prefix — if you see the prefix, token version is wrong)
    • iss = https://login.microsoftonline.com/{your tenant ID}/v2.0
    • ver = "2.0"
    • tid = {your tenant ID}
  3. 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 fieldMust equal
audThe provider's Audience (bare client-ID GUID for v2)
tidOne of the provider's AllowedTenantIds
isshttps://login.microsoftonline.com/{tid}/v2.0 for v2
ver"2.0"
expIn 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​