Authorization ‐ OIDC Client Credentials - demingongo/kaapi GitHub Wiki
🚀 Kaapi + OIDC Client Credentials Flow Example
This is a fully working example of a Kaapi server implementing the OIDC Client Credentials Flow.
It is intended for documentation and learning purposes, so you can copy/paste it, run it, and experiment. Then adapt the logic for your production needs (e.g., database persistence, encryption, or enhanced client validation).
⚠️ Note: This example uses in-memory client definitions and simplified logic for demonstration only.
✅ Features Demonstrated
-
Client Credentials Flow (
/oauth2/token) -
JWKS endpoint (
/.well-known/jwks.json) -
Access token validation
-
Multiple client authentication methods:
ClientSecretBasicClientSecretPost
-
Key rotation
-
Swagger/OpenAPI integration (works directly in Swagger UI)
-
Restricted route example with access control
🛠 Full Example Code
import { Kaapi } from '@kaapi/kaapi';
import {
OIDCClientCredentialsBuilder,
OAuth2TokenResponse,
OAuth2ErrorCode,
ClientSecretBasic,
ClientSecretPost,
createInMemoryKeyStore,
} from '@kaapi/oauth2-auth-design';
// Valid clients and allowed scopes
const VALID_CLIENTS = [
{
client_id: 'service-api-client',
client_secret: 's3cr3tK3y123!',
allowed_scopes: ['read', 'write'],
},
{
client_id: 'internal-service',
client_secret: 'Int3rnalK3y!',
allowed_scopes: ['read', 'write', 'admin'],
},
];
// === OIDC Client Credentials Builder ===
const authDesign = OIDCClientCredentialsBuilder.create()
.strategyName('client-credentials')
.setTokenTTL(600)
.setJwksKeyStore(createInMemoryKeyStore())
.setJwksRotatorOptions({
intervalMs: 7.862e9,
timestampStore: createInMemoryKeyStore(),
})
.setPublicKeyExpiry(8.64e6)
.useAccessTokenJwks(true)
// Client authentication methods
.addClientAuthenticationMethod(new ClientSecretBasic())
.addClientAuthenticationMethod(new ClientSecretPost())
// Define available scopes
.setScopes({
read: 'Grants read-only access to protected resources',
write: 'Grants write access to protected resources',
admin: 'Administrative access',
})
// Token route definition
.tokenRoute((route) =>
route
.setPath('/oauth2/token')
.generateToken(async ({ clientId, clientSecret, ttl, tokenType, scope, createJwtAccessToken }) => {
// Validate client credentials
const client = VALID_CLIENTS.find(
(c) => c.client_id === clientId && c.client_secret === clientSecret
);
if (!client) {
return {
error: OAuth2ErrorCode.INVALID_CLIENT,
error_description: 'Invalid client_id or client_secret',
};
}
// Determine requested scopes
const requestedScopes = (scope ?? '').split(/\s+/).filter(Boolean);
// Compute granted scopes
let grantedScopes = client.allowed_scopes;
if (requestedScopes.length > 0) {
grantedScopes = requestedScopes.filter((s) =>
client.allowed_scopes.includes(s)
);
}
if (grantedScopes.length === 0) {
return {
error: OAuth2ErrorCode.INVALID_SCOPE,
error_description: 'No valid scopes granted for this client',
};
}
// Generate a signed JWT access token
const { token: accessToken } = await createJwtAccessToken({
sub: clientId,
scope: grantedScopes,
});
// Return token response
return new OAuth2TokenResponse({ access_token: accessToken })
.setExpiresIn(ttl)
.setTokenType(tokenType)
.setScope(grantedScopes.join(' '));
})
)
// JWKS endpoint
.jwksRoute((route) => route.setPath('/.well-known/jwks.json'))
// Access Token Validation
.validate(async (_req, { jwtAccessTokenPayload }) => {
if (!jwtAccessTokenPayload?.sub) return { isValid: false };
return {
isValid: true,
credentials: {
app: {
id: jwtAccessTokenPayload.sub,
},
scope: Array.isArray(jwtAccessTokenPayload.scope)
? jwtAccessTokenPayload.scope
: [],
},
};
})
.build();
// === Kaapi App Setup ===
const app = new Kaapi({
port: 3000,
host: 'localhost',
docs: {
path: '/docs/api',
host: { url: 'http://localhost:3000' },
},
});
// Extend app with auth strategy
await app.extend(authDesign);
// Default strategy
app.base().auth.default({
strategy: authDesign.getStrategyName(),
mode: 'try',
});
// === Key rotation check every hour (rotation happens according to intervalMs) ===
setInterval(() => authDesign.checkAndRotateKeys().catch(console.error), 3600 * 1000);
// Start server
await app.listen();
app.log.info('🚀 Kaapi OIDC Client Credentials Auth Server running at http://localhost:3000');
👋 Restricted Route Example (/greetings)
// === Restricted Route Example ===
app.route(
{
method: 'GET',
path: '/greetings',
auth: true,
options: {
auth: {
access: {
entity: 'app',
scope: ['read', 'admin'],
},
},
},
},
(req) => `Hello ${req.auth.credentials.app.id}`
);
💡 Tip:
/greetingsis an authenticated route example. Call it with a valid Bearer token obtained via the client credentials flow.
📌 Notes & Tips
-
Copy/Paste Ready: This is a working example. Developers can copy it, run it, and experiment. Replace in-memory clients with database-backed or secure storage in production.
-
SwaggerUI Compatible: The Client Credentials flow can be fully tested in SwaggerUI. Simply click Authorize, enter your
client_idandclient_secret, select scopes, and Swagger will handle the token request automatically. -
Key Rotation: The server calls
authDesign.checkAndRotateKeys()every hour to check if keys need rotation. Actual rotation happens according to the configured interval (intervalMs: 7.862e9≈ 91 days). In production, adjust both intervals as needed. -
OIDC Metadata: The server automatically exposes its configuration at
/.well-known/openid-configuration, listing available endpoints, grant types, and supported scopes.