Authorization ‐ OIDC Device Authorization - demingongo/kaapi GitHub Wiki
🚀 Kaapi + OIDC Device Authorization Flow Example
This is a fully working example of a Kaapi server implementing the OIDC Device Authorization 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, full client authentication, etc.).
⚠️ Note: This example uses in-memory storage and simplified logic for demonstration only.
✅ Features Demonstrated
- Device Authorization flow (
/oauth2/device_authorization) - Token polling (
/oauth2/token) - JWKS endpoint (
/.well-known/jwks.json) - Access token validation
- Key rotation
- Grouping multiple auth strategies with
GroupAuthDesign - Swagger/OpenAPI integration with Bearer token
- Restricted route example
🛠 Full Example Code
import { Kaapi, GroupAuthDesign, BearerAuthDesign } from '@kaapi/kaapi';
import {
OIDCDeviceAuthorizationBuilder,
createInMemoryKeyStore,
OAuth2TokenResponse,
NoneAuthMethod,
DeviceFlowOAuth2ErrorCode,
} from '@kaapi/oauth2-auth-design';
// Registered clients and allowed scopes
const REGISTERED_CLIENTS = {
'service-api-client': ['read', 'write'],
'internal-service': ['read', 'write', 'admin'],
};
/**
* In-memory device codes store
* @type {Map<string, { clientId: string; scopes: string[]; verified: boolean; userCode: string }>}
*/
const deviceCodesStore = new Map();
// === OIDC Device Authorization Builder ===
const authDesign = OIDCDeviceAuthorizationBuilder.create()
.strategyName('device-authorization')
.setTokenTTL(600)
.setJwksKeyStore(createInMemoryKeyStore())
.setJwksRotatorOptions({
intervalMs: 7.862e9,
timestampStore: createInMemoryKeyStore(),
})
.setPublicKeyExpiry(8.64e6)
.useAccessTokenJwks(true)
.addClientAuthenticationMethod(new NoneAuthMethod())
.setScopes({
read: 'Read access',
write: 'Write access',
admin: 'Administrative access',
})
// Step 1: Device Authorization
.authorizationRoute((route) =>
route.setPath('/oauth2/device_authorization').generateCode(async ({ clientId, scope }) => {
const allowedScopes = REGISTERED_CLIENTS[clientId];
if (!allowedScopes) return null;
const requestedScopes = scope?.split(' ') ?? [];
const grantedScopes = requestedScopes.filter((s) => allowedScopes.includes(s));
if (grantedScopes.length === 0) return null;
const deviceCode = `device-${Date.now()}`;
const userCode = `user-${Math.random().toString(36).substring(2, 8)}`;
deviceCodesStore.set(deviceCode, { clientId, scopes: grantedScopes, verified: false, userCode });
return {
device_code: deviceCode,
user_code: userCode,
verification_uri: 'http://localhost:3000/verify',
verification_uri_complete: `http://localhost:3000/verify?user_code=${userCode}`,
expires_in: 600,
interval: 5,
};
})
)
// Step 2: Token polling
.tokenRoute((route) =>
route
.setPath('/oauth2/token')
.generateToken(async ({ deviceCode, clientId, ttl, tokenType, createJwtAccessToken }) => {
const entry = deviceCodesStore.get(deviceCode);
if (!entry || entry.clientId !== clientId) return null;
if (!entry.verified) return { error: DeviceFlowOAuth2ErrorCode.AUTHORIZATION_PENDING };
const { token: accessToken } = await createJwtAccessToken({
sub: 'device-user',
scope: entry.scopes,
});
deviceCodesStore.delete(deviceCode);
return new OAuth2TokenResponse({ access_token: accessToken })
.setExpiresIn(ttl)
.setTokenType(tokenType)
.setScope(entry.scopes.join(' '));
})
)
// Step 3: JWKS endpoint
.jwksRoute((route) => route.setPath('/.well-known/jwks.json'))
// Step 4: Access Token Validation
.validate(async (_req, { jwtAccessTokenPayload }) => {
if (!jwtAccessTokenPayload?.sub) return { isValid: false };
return {
isValid: true,
credentials: {
user: {
id: jwtAccessTokenPayload.sub,
aud: jwtAccessTokenPayload.aud,
},
scope: Array.isArray(jwtAccessTokenPayload.scope)
? jwtAccessTokenPayload.scope
: [],
},
};
})
.build();
// === GroupAuthDesign for multiple auth strategies ===
const groupAuth = new GroupAuthDesign([
authDesign,
new BearerAuthDesign({ strategyName: 'bearer-auth' }) // for SwaggerUI token auth
]);
// === Kaapi App Setup ===
const app = new Kaapi({
port: 3000,
host: 'localhost',
docs: {
path: '/docs/api',
host: { url: 'http://localhost:3000' },
},
});
// Extend app with grouped auth strategies
await app.extend(groupAuth);
// Default strategy
app.base().auth.default({
strategies: groupAuth.getStrategies(),
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 Device Authorization Server running at http://localhost:3000');
🔑 Verification Endpoint (/verify)
import Joi from 'joi';
// === Verification Endpoint ===
app.route(
{
method: 'GET',
path: '/verify',
options: {
validate: {
query: Joi.object({
user_code: Joi.string(),
}),
},
},
},
(request, h) => {
const userCode = request.query.user_code;
if (!userCode) return h.response('Missing user_code').code(400);
const entry = Array.from(deviceCodesStore.values()).find((v) => v.userCode === userCode);
if (!entry) return h.response('Invalid user_code').code(404);
entry.verified = true;
return h.response(
`Device verified successfully for client: ${entry.clientId}, scopes: ${entry.scopes.join(' ')}`
);
}
);
💡 Tip:
/verifyis where the user confirms the device code. Use this to mark device codes as verified.
👋 Restricted Route Example (/greetings)
// === Restricted Route Example ===
app.route({
method: 'GET',
path: '/greetings',
auth: true,
options: {
auth: {
access: {
entity: 'user',
scope: ['read', 'admin'],
},
},
},
}, req => `Hello ${req.auth.credentials.user.id}`);
💡 Tip:
/greetingsis an authenticated route example. Make sure you pass a valid Bearer token.
📌 Notes & Tips
-
Copy/Paste Ready: This is a working example. Developers can copy, run, and experiment, then replace the in-memory logic with DB/persistent storage or modify the authentication logic.
-
SwaggerUI Limitation: Device flow cannot be executed directly in SwaggerUI.
✅ Workflow:
- Check the device authorization endpoint and other OIDC info at
/.well-known/openid-configuration. - Run the device flow with an external tool.
- Get the access token.
- Use it in SwaggerUI via Bearer token authorization.
- Check the device authorization endpoint and other OIDC info at
-
Bearer Token in OpenAPI Docs: Achieved by adding
BearerAuthDesignto the Kaapi app (already included in theGroupAuthDesignabove). SwaggerUI will then show the Authorize button. -
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, you can adjust both the check frequency and the rotation interval as needed.