Authorization ‐ OAuth2 ‐ Tuto 6 ‐ Access Token and JWKS - demingongo/kaapi GitHub Wiki
OAuth2 and OIDC rely on access tokens to securely represent authentication and authorization. Access tokens can be either opaque (random strings stored and validated server-side) or JWTs (self-contained tokens signed with cryptographic keys).
When tokens are JWT-based, signing ensures they are authentic and tamper-proof. JWKS (JSON Web Key Set) provides a standardized way to publish the public keys used to verify those signed tokens.
The @kaapi/oauth2-auth-design package supports both opaque and JWT access tokens, offering flexible JWKS integration for projects that choose to sign tokens.
All OAuth2/OIDC flow builders provide JWKS and token verification features:
- ✅ Authorization Code Flow
- ✅ Client Credentials Flow
- ✅ Device Authorization Flow
Each builder implements the same JWKS and validation capabilities, so you can apply the examples below to any flow.
The JWKS endpoint exposes your application’s public keys, allowing resource servers to verify JWT signatures.
By default, this endpoint is available at /oauth2/keys, but you can customize its path or response.
import { OAuth2AuthorizationCodeBuilder } from '@kaapi/oauth2-auth-design';
OAuth2AuthorizationCodeBuilder
.create()
.jwksRoute(route => route.setPath('/.well-known/jwks.json'));The above creates a JWKS endpoint accessible at /.well-known/jwks.json.
It will automatically return the public keys currently active in your JWKS store.
You can also override the default JWKS controller to customize the returned payload or add extra validation logic:
OAuth2AuthorizationCodeBuilder
.create()
.jwksRoute(route => route
.setPath('/.well-known/jwks.json')
.validate(({ jwks }, request, h) => jwks)
);The key store is where signing keys are managed. By default, Kaapi uses an in-memory key store, suitable for development and testing but not for production.
You can configure it explicitly like this:
import { OAuth2AuthorizationCodeBuilder, createInMemoryKeyStore } from '@kaapi/oauth2-auth-design';
OAuth2AuthorizationCodeBuilder
.create()
.setJwksKeyStore(createInMemoryKeyStore());For production environments, you can provide your own implementation of JwksKeyStore to persist keys in a database or secure vault:
interface JwksKeyStore {
storeKeyPair(kid: string, privateKey: object, publicKey: object, ttl: number): void | Promise<void>;
getPrivateKey(): Promise<object | undefined>;
getPublicKeys(): Promise<object[]>;
}This flexibility allows integration with secure storage mechanisms like Redis, PostgreSQL, AWS KMS, or HashiCorp Vault.
To improve long-term security, it’s recommended to rotate signing keys periodically. Key rotation limits the impact of a compromised key and helps ensure cryptographic freshness over time.
You can enable automatic rotation using setJwksRotatorOptions, which defines how often new key pairs are generated.
import { OAuth2AuthorizationCodeBuilder, createInMemoryKeyStore } from '@kaapi/oauth2-auth-design';
const authCodeFlow = OAuth2AuthorizationCodeBuilder
.create()
.setJwksRotatorOptions({
intervalMs: 7.862e+9, // every 91 days
timestampStore: createInMemoryKeyStore()
})
.build();
authCodeFlow.checkAndRotateKeys();This checks whether a rotation is due and generates a new key pair if needed. You can schedule periodic checks in your app, for example every hour:
setInterval(() => {
authCodeFlow.checkAndRotateKeys().catch(console.error);
}, 3600 * 1000);If you ever need to rotate keys immediately, without waiting for the next scheduled interval, you can call:
await authCodeFlow.generateKeyPair();This forces a new key pair to be generated and stored right away, bypassing the rotation timing logic. It’s useful for emergency key replacement or manual rotation in controlled environments.
The JwksRotationTimestampStore interface defines how the last rotation timestamp is stored and retrieved.
This allows consistent rotation intervals even if the application restarts:
interface JwksRotationTimestampStore {
getLastRotationTimestamp(): Promise<number>;
setLastRotationTimestamp(rotationTimestamp: number): Promise<void>;
}To prevent old keys from lingering indefinitely, you can set a time-to-live (TTL) for public keys. When used with key rotation, this ensures that expired keys are automatically removed from your JWKS after their TTL elapses.
.setPublicKeyExpiry(8.64e+6) // 100 daysFor example:
.setJwksRotatorOptions({
intervalMs: 7.862e+9,
timestampStore: createInMemoryKeyStore()
})
.setPublicKeyExpiry(8.64e+6);This configuration rotates signing keys every 91 days and keeps the previous public keys available for 9 additional days (100 − 91) before they expire and are removed. This overlap ensures that tokens signed with an old key shortly before rotation remain verifiable for a short transition period, which is especially important in distributed systems.
The expiry value defined in setPublicKeyExpiry() is passed as the ttl argument to the storeKeyPair method of the configured JwksKeyStore.
Your custom store can use this TTL to automatically remove expired keys.
Each flow builder lets you define custom logic for validating access tokens through the validate() method.
This function determines whether a token is valid and what credentials should be attached to the authenticated request.
import { OAuth2AuthorizationCodeBuilder } from '@kaapi/oauth2-auth-design';
OAuth2AuthorizationCodeBuilder
.create()
.validate(async (request, { token }) => {
// Verify that the token is known, valid, and not expired
// (for example by checking a database, cache, or JWT claims)
const isValidToken = Boolean(token); // replace with your actual validation logic
if (!isValidToken) {
return { isValid: false };
}
// Attach credentials that will be available in request.auth.credentials
return {
isValid: true,
credentials: {
user: {
sub: '12345',
name: 'John Doe',
email: '[email protected]'
}
}
};
});Within your Kaapi routes, validated credentials are available via request.auth.credentials:
app.route(
{ method: 'GET', path: '/', auth: true },
(request) => `Hello ${request.auth.credentials.user.name}`
);When using JWT-based access tokens, the library can automatically verify them using your JWKS public keys.
However, signing tokens is still a deliberate action — you must generate them using createJwtAccessToken() (and createIdToken() for OIDC).
import { OIDCAuthorizationCodeBuilder, OAuth2TokenResponse } from '@kaapi/oauth2-auth-design';
OIDCAuthorizationCodeBuilder
.create()
.setTokenTTL(3600)
.useAccessTokenJwks(true)
.tokenRoute(route =>
route.generateToken(async ({ createJwtAccessToken, createIdToken, ttl, tokenType }) => {
// Create and sign JWT-based access token
const { token: accessToken } = await createJwtAccessToken({
sub: '12345',
scope: 'openid email',
aud: 'https://api.example.com'
});
// Create and sign ID token for OIDC
const { token: idToken } = await createIdToken({
sub: '12345',
name: 'John Doe',
email: '[email protected]'
});
return new OAuth2TokenResponse({ access_token: accessToken })
.setExpiresIn(ttl)
.setTokenType(tokenType)
.setIdToken(idToken);
})
)
.validate(async (request, { jwtAccessTokenPayload }) => {
if (!jwtAccessTokenPayload?.sub) return { isValid: false };
return {
isValid: true,
credentials: {
user: {
id: jwtAccessTokenPayload.sub,
email: jwtAccessTokenPayload.email
}
}
};
});In this example:
-
createJwtAccessToken()signs the access token with the JWKS private key. -
createIdToken()does the same for OIDC ID tokens. -
createJwtAccessToken()andcreateIdToken()automatically use the TTL value defined viasetTokenTTL()for the token expiration. - The
expclaim can be overridden if a custom expiration is needed. -
useAccessTokenJwks(true)enables automatic verification of JWT access tokens using the JWKS public keys. -
jwtAccessTokenPayloadcontains the verified and decoded claims from the incoming access token.
If you prefer opaque tokens, skip useAccessTokenJwks(true) and validate tokens using your own logic within validate().
-
Access tokens can be opaque or JWT-based.
-
JWKS enables public key distribution for verifying signed tokens.
-
The
@kaapi/oauth2-auth-designpackage provides:-
jwksRoute()— serve JWKS public keys -
setJwksKeyStore()— define key storage strategy -
setJwksRotatorOptions()— handle key rotation -
setPublicKeyExpiry()— control key TTL -
useAccessTokenJwks(true)— verify JWTs automatically -
validate()— define custom validation for opaque or JWT tokens
-
-
Supported across all flows: Authorization Code, Client Credentials, and Device Authorization.
Below is a diagram summarizing JWT access token behavior:
+-------------------+
| OAuth2/OIDC |
| Authorization |
| Server |
+-------------------+
|
| Generates JWT access token using
| createJwtAccessToken() / createIdToken()
| (signed with JWKS private key)
v
+-------------------+
| Access Token |
| (JWT signed) |
+-------------------+
|
| Sent to Client
v
+-------------------+
| Client |
+-------------------+
|
| Uses token to access
| protected resource
v
+-------------------+
| Resource Server |
| |
| useAccessTokenJwks(true)
| - Verify signature using JWKS public key
| - Extract claims
| - Validate via validate() method
| |
+-------------------+
^
| JWKS Endpoint exposes public keys
| (/oauth2/keys or custom path)
|
+-------------------+
| JWKS Key Store |
| (private + public|
| keys, rotation) |
+-------------------+
|
| Periodic rotation / TTL management
v
+-------------------+
| Rotated Keys |
+-------------------+