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 jose library is the modern, runtime-portable choice for that. Use @azure/msal-node on top when your API needs to call another protected service (OAuth 2.0 on-behalf-of, client credentials, etc.).
Entra ID v2.0 access-token verification
Client appEntra IDDaloyJS APITenant JWKS
01asyncClient appEntra IDUser signs in; Entra ID mints a v2.0 access token (RS256)aud = api://my-daloy-api
02requestClient appDaloyJS APICall API with Authorization: Bearer <access token>
05responseDaloyJS APIClient appReturn protected data after requireAuth passes
The DaloyJS API verifies the v2.0 token against the tenant's JWKS with jose. Keys refresh automatically on a missing kid, so signing-key rollover needs no redeploy. Multi-tenant apps must also validate the tid claim against an allowlist.
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).
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-configuration and references the JWKS at https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys.
2. Install
ts
pnpm add jose
Add @azure/msal-node as well only if the API itself needs to acquire downstream tokens (see below).
3. Environment variables
ts
# .envENTRA_TENANT_ID=11111111-2222-3333-4444-555555555555ENTRA_API_AUDIENCE=api://my-daloy-api # or the API app's client ID GUIDENTRA_REQUIRED_SCOPE=access_as_user
createRemoteJWKSet caches keys in memory and refreshes on a missing kid, so key rollover is handled automatically.
5. Guard a route
ts
import { z } from "zod";import { App, secureHeaders, rateLimit } from "@daloyjs/core";import { entraPlugin } from "./plugins/entra";import { requireAuth } from "./plugins/auth"; // from the Overview pageconst app = new App();app.use(secureHeaders());app.use(rateLimit({ windowMs: 60_000, max: 100 }));app.register(entraPlugin);app.route({ method: "GET", path: "/me", operationId: "getMe", middleware: [requireAuth(process.env.ENTRA_REQUIRED_SCOPE!)], responses: { 200: { description: "OK", body: z.object({ oid: z.string().optional(), tid: z.string().optional() }) }, }, handler: ({ state }) => ({ status: 200, body: { oid: state.principal!.oid, tid: state.principal!.tid }, }),});
App roles vs delegated scopes: app-only tokens (client-credentials flow) put granted roles in roles with no scp claim, while user-delegated tokens put granted scopes in scp. Inspect both in requireAuth if you support both shapes.
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:
ts
pnpm add @azure/msal-node
ts
import { ConfidentialClientApplication } from "@azure/msal-node";const msal = new ConfidentialClientApplication({ auth: { clientId: process.env.ENTRA_CLIENT_ID!, authority: `https://login.microsoftonline.com/${process.env.ENTRA_TENANT_ID}`, clientSecret: process.env.ENTRA_CLIENT_SECRET!, // or use a certificate },});// On-behalf-of: exchange the incoming user token for a Graph tokenconst result = await msal.acquireTokenOnBehalfOf({ oboAssertion: incomingUserAccessToken, scopes: ["https://graph.microsoft.com/User.Read"],});console.log(result?.accessToken);
Prefer certificates over client secrets in production, and store credentials in Azure Key Vault or your platform's secret manager.
Notes
The issuer for v2.0 tokens is https://login.microsoftonline.com/{tenantId}/v2.0. Multi-tenant apps must validate the tid claim 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.