Protect a DaloyJS API with Keycloak
Keycloak is the most widely deployed open-source identity provider: you run it yourself, and it handles login, MFA, social and enterprise federation, and token issuance. It speaks standard OpenID Connect, so a DaloyJS API needs no Keycloak SDK. The built-in jwk() verifies the realm's access tokens, and requireRoles() reads Keycloak's realm and client roles.
- 01asyncClient appKeycloak realmAuthorization code + PKCE; Keycloak mints an access token (RS256)iss = https://sso.example.com/realms/acme
- 02requestClient appDaloyJS APICall API with Authorization: Bearer <access token>
- 03asyncDaloyJS APIRealm JWKSjwk() fetches signing keys (cached)GET {issuer}/protocol/openid-connect/certs
- 04noteDaloyJS APIDaloyJS APIjwk() checks iss, aud, expiry; requireRoles() reads realm_access.roles
- 05responseDaloyJS APIClient app401 bad token · 403 missing role · 200 otherwise
1. Configure the realm
- Create a realm (for example
acme), or use an existing one. Your issuer ishttps://<keycloak-host>/realms/<realm>. Keycloak 16 and older add/authbefore/realms. - Create a client for your front end (public client, Standard flow with PKCE) and note its client ID.
- Create a client for the API itself (for example
books-api). It does not need a flow enabled; it exists so tokens can name it as their audience and so you can define client roles on it. - Add an audience mapper. By default Keycloak access tokens do not name your API in
aud. Create a client scope (for examplebooks-api-audience) with an Audience mapper whose Included Client Audience isbooks-api, with Add to access token on, and add that scope to the front-end client as a default scope. - Define realm roles (shared across clients) or client roles on
books-api, and assign them to users or groups.
Check the result: decode a fresh access token locally and confirm iss, an aud that includes books-api, and the roles you assigned.
2. Install
Nothing beyond @daloyjs/core.
3. Environment variables
4. Verify tokens
If you changed the realm's signing algorithm (for example to ES256 or PS256), list it in algorithms instead. jwk() accepts asymmetric algorithms only.
5. Guard routes with roles
Keycloak nests roles: realm roles in realm_access.roles, client roles in resource_access.<client-id>.roles. Pass requireRoles() a reader for the one you use:
Prefer client roles for API permissions: they are scoped to books-api, so a role named admin in another client cannot unlock your API. A reader that finds nothing (no resource_access, a different client) means "no roles", and the request gets 403.
Keycloak also emits OAuth scopes in scope (space-delimited). Custom client scopes such as books:write work with requireScopes() as usual.
Service accounts (machine-to-machine)
To let a backend call the API with no user, enable Service accounts roles on that backend's client and assign it client roles on books-api. It gets tokens through the client credentials grant, and they verify with the same jwk() setup. Pin which clients may call internal routes with the azp claim:
Running Keycloak
- Serve it over HTTPS with a stable hostname. The issuer is part of every token, so set Keycloak's
hostnameoption to the public URL. A token issued ashttp://localhost:8080/realms/acmewill not verify againsthttps://sso.example.com/realms/acme. - Your API must reach the JWKS URL. In Kubernetes or Docker Compose, the in-cluster hostname often differs from the public issuer. The
jwksURL must still behttps://; point it at an internal TLS endpoint, or keep it on the public hostname. - Key rotation. When you rotate realm keys, keep the old key passive until every token signed with it has expired.
jwk()picks the newkidup automatically. - Same pattern for other self-hosted IdPs. Zitadel, Authentik, Ory Hydra, and Dex all publish a JWKS; only the URLs and the roles claim change. For Zitadel, roles live in
urn:zitadel:iam:org:project:rolesas an object keyed by role name:requireRoles(["admin"], { claim: (c) => Object.keys((c["urn:zitadel:iam:org:project:roles"] as object | undefined) ?? {}) }).
See also Auth architecture, Entra ID, and the auth overview.