Protect a DaloyJS API with Microsoft Entra ID (MSAL)
Microsoft Entra ID (formerly Azure Active Directory) is Microsoft's enterprise identity platform. For a backend API, the job is to verify the v2.0 access token in the Authorization header against the tenant's public JWKS. The built-in jwk() does that with no extra package, on every DaloyJS runtime. Use @azure/msal-node on top when your API needs to call another protected service (OAuth 2.0 on-behalf-of, client credentials, etc.).
- 01asyncClient appEntra IDUser signs in. Entra ID mints a v2.0 access token (RS256)aud = the API app's client ID (GUID)
- 02requestClient appDaloyJS APICall API with Authorization: Bearer <access token>
- 03asyncDaloyJS APITenant JWKSjwk() fetches signing keys (cached)GET /{tenantId}/discovery/v2.0/keys
- 04noteDaloyJS APIDaloyJS APIjwk() checks issuer, audience, expiry; guards check scp / rolesiss = login.microsoftonline.com/{tenantId}/v2.0
- 05responseDaloyJS APIClient appReturn protected data after the scope or role guard passes
1. Register the API in Entra ID
- In the Microsoft Entra admin center, go to Entra ID -> App registrations -> New registration and register your API app.
- Under Expose an API, set an Application ID URI (e.g.
api://my-daloy-api) and add one or more scopes (e.g.access_as_user). - In the API app's Manifest, set
api.requestedAccessTokenVersionto2. The default (null) issues v1.0 access tokens, whoseissdoes not end in/v2.0and whoseaudmay be the Application ID URI, so the configuration below would reject them. - Register your client app separately and grant it permission to the scope above. Note the tenant ID and the API app's Application (client) ID.
- The OIDC discovery document lives at
https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configurationand references the JWKS athttps://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys.
2. Install
Nothing beyond @daloyjs/core. Add @azure/msal-node only if the API itself needs to acquire downstream tokens (see below).
3. Environment variables
4. Verify tokens and guard routes
jwk() fetches the tenant JWKS over HTTPS with caching, requires a kid, pins the algorithm allowlist, and checks iss, aud, exp and nbf. Since 1.10.0 it reads Entra's scp claim, which is a space-delimited string ("scp": "access_as_user User.Read"), into ctx.state.user.scopes. Earlier versions only accepted an array there, so every Entra user failed requireScopes() with 403.
With both hooks in preBody, the status codes do not depend on the request body: missing, expired or wrongly signed tokens are 401, a valid token without an accepted scope is 403, and only an authorized caller can get a 400 for a malformed body. On Node, add serve(app, { bufferedBodyMaxBytes: 0 }) if unauthenticated clients sending Expect: 100-continue must never be invited to upload; see request-body buffering.
App roles
Define App roles on the API registration and assign them to users, groups, or client applications. Granted roles arrive in the roles claim, which requireRoles() reads by default:
App roles vs delegated scopes: app-only tokens (client-credentials flow, no user) carry granted application roles in roles and no scp, while user-delegated tokens carry scopes in scp. Guard machine-to-machine routes with requireRoles() and user routes with requireScopes(). If one route must accept both callers, write a small preBody hook that passes when either ctx.state.user.scopes has the scope or ctx.state.user.claims.roles has the role.
Multi-tenant apps
The issuer above pins one tenant. A multi-tenant API that accepts several tenants must check the tid claim against an allowlist, in jwk({ verify }):
Acquiring downstream tokens with MSAL Node
If your API needs to call Microsoft Graph or another protected service on behalf of the user, use MSAL Node's ConfidentialClientApplication:
Prefer certificates over client secrets in production, and store credentials in Azure Key Vault or your platform's secret manager.
Notes
- The
issuerfor v2.0 tokens ishttps://login.microsoftonline.com/{tenantId}/v2.0. Multi-tenant apps must validate thetidclaim against an allowlist rather than relying on the issuer alone. - Don't validate tokens you don't own. Microsoft Graph tokens may not be JWTs and aren't meant to be inspected by your app.
- Entra ID rotates signing keys regularly, never pin keys, always resolve them through the JWKS endpoint.
See also AWS Cognito, Auth0, and the auth integrations overview.