OAuth
Verify tokens from an authorization server. Declare the identity, secure the routes, verify in the guard, and serve the discovery document.
When your tokens come from an authorization server such as better-auth, Auth0 or Keycloak, your API verifies them rather than issuing them. OAuth 2.1 calls that a resource server.
Four steps: declare the identity, secure the routes, verify in the guard, and serve the discovery document.
Declare where tokens come from
A scope is a permission the token carries. Declare the permissions once, and the roles built from them, as on Access Control:
The identity carries everything about the authorization server. issuer is the server's identifier (RFC 8414), flows names its endpoints and the scopes it issues for the OpenAPI spec, and resourceMetadata is where your discovery document is served:
Give the authorization server the same list, so a token can only carry permissions the code declares. With Better Auth:
For an OpenID Connect server, k.identity.openIdConnect({ openIdConnectUrl, resourceMetadata, roles }) takes the place of oauth2 above, and the issuer derives from the discovery URL.
Secure the routes
A route names what it requires, as for any identity:
defineConfig writes users:write into the route's security requirement, so the OpenAPI document lists it as the scope createUser needs.
Verify in the guard
The guard receives the token as oauth2. Check the signature against the authorization server's JWKS, the issuer, and the audience, then return the user's role and the token's scopes as permissions:
The guard sits on the identity, so this continues the declaration above:
Name it under auth.identities on your config and a route's auth: 'user' runs it.
The caller holds what both the role and the token allow. When a route requires a permission the role holds but the token lacks, Kizuna answers 403 with the insufficient_scope challenge (RFC 6750), so the client knows a new token would pass. When the role lacks it, the 403 has no challenge, since no token could help.
Both challenges point at the discovery document, and so does every 401 from deny:
Serve the discovery document
Clients that do not know your authorization server find it through RFC 9728 Protected Resource Metadata at /.well-known/oauth-protected-resource. Serve it on the framework, next to api.mount:
The document builds from the identity, so it says what you declared. resource is the canonical URI of your API, the same value your guard checks as the token audience.
Serve it when OAuth clients discover your API rather than being configured for it. A first-party app with a hardcoded authorization server never looks at it.
See ProtectedResourceMetadataSchema for the document's fields and builders.
Reference
- Authentication for identities and guards
- Access Control for what a route's
authcan say k.identityProtectedResourceMetadataSchema