api-machine provides a declarative authentication system with cascading support across server, router, and endpoint levels. Authentication schemes automatically integrate with OpenAPI/Swagger documentation.
import { RestServer, BaseApiRouter, BaseApiEndpoint } from 'api-machine';
import { BearerAuthenticationScheme } from 'api-machine';
class SecureEndpoint extends BaseApiEndpoint {
override path = '/data';
async handle(request, response) {
return { message: 'Secure data' };
}
}
class ApiRouter extends BaseApiRouter {
override path = '/api';
override authentication = new BearerAuthenticationScheme({
checkToken: async (token: string) => token === 'secret-token',
schemeName: 'BearerAuth',
});
async routes() {
return [SecureEndpoint];
}
}
const server = new RestServer({
port: 4000,
authentication: new BearerAuthenticationScheme({
checkToken: async (token: string) => token === 'server-token',
}),
});api-machine supports two main types of authentication schemes:
Run authentication inline on every request. Credentials are extracted and validated immediately:
- Extract credentials from the request (e.g., Bearer tokens from Authorization header)
- Validate credentials synchronously
- Middleware runs for every incoming request
- Examples:
BearerAuthenticationScheme, API key schemes, JWT validation - Use for: Stateless authentication (tokens, API keys)
Provide an AuthFlow to obtain a session, then verify that session on every request:
- Define multi-step authentication process using
AuthFlow(e.g., challenge → authorization → token exchange) - Session is obtained once through the auth flow
- Session is then validated by checking the session ID on each subsequent request
- Examples: OAuth2, SAML, session-based authentication
- Use for: Stateful authentication flows requiring multiple steps
Authentication follows a priority hierarchy:
Endpoint → Router → Server
- Endpoint-level authentication has highest priority
- Router-level authentication applies if endpoint doesn't specify
- Server-level authentication applies as the default fallback
- Set
authentication = nullto make a route explicitly public
class PublicRouter extends BaseApiRouter {
override path = '/public';
override authentication = null;
async routes() {
return [PublicEndpoint];
}
}
class AdminEndpoint extends BaseApiEndpoint {
override path = '/admin';
override authentication = new BearerAuthenticationScheme({
checkToken: async (token) => token === 'admin-token',
});
async handle() {
return { admin: true };
}
}Validates Bearer tokens from the Authorization header on every request. Uses InlineAuthenticationScheme for synchronous inline token validation.
import { BearerAuthenticationScheme } from 'api-machine';
const auth = new BearerAuthenticationScheme({
checkToken: async (token: string) => {
// Validate token (check database, JWT, etc.)
return token === 'valid-token';
},
schemeName: 'BearerAuth', // Optional: OpenAPI scheme name
bearerFormat: 'JWT', // Optional: Token format
description: 'JWT Bearer Auth', // Optional: OpenAPI description
});How it works:
- Middleware runs for every incoming request
- Extracts
Authorization: Bearer <token>header - Validates token using
checkToken() - Returns 401 Unauthorized if token missing or invalid
- Sets
request.authenticated = trueon successful validation
Architecture:
- Extends
InlineAuthenticationSchemefor stateless credential-based auth getCredentials()- Extracts Bearer token from Authorization header on each requestauthenticate()- Validates extracted token usingcheckToken()on each request
Apply authentication to all endpoints by default:
const server = new RestServer({
port: 4000,
authentication: new BearerAuthenticationScheme({
checkToken: async (token) => await validateToken(token),
}),
});All endpoints will require authentication unless a router or endpoint overrides it with authentication = null.
Apply authentication to all endpoints in a router:
class SecureRouter extends BaseApiRouter {
override path = '/secure';
override authentication = new BearerAuthenticationScheme({
checkToken: async (token) => token === 'router-token',
});
async routes() {
return [Endpoint1, Endpoint2]; // Both require auth
}
}Override parent authentication for specific endpoints:
class AdminEndpoint extends BaseApiEndpoint {
override path = '/admin';
override authentication = new BearerAuthenticationScheme({
checkToken: async (token) => token === 'admin-token',
schemeName: 'AdminAuth',
});
async handle() {
return { admin: true };
}
}InlineAuthenticationScheme runs authentication inline on every request. Credentials are extracted from the request and validated immediately, without requiring a separate session object.
How it works:
- Middleware runs for each incoming request
getCredentials(request)extracts credentials from the requestauthenticate({ credentials, request })validates the credentials- If validation succeeds, request continues; otherwise an error is thrown
- No session is created or stored
Key Methods:
getCredentials(request)- Extract credentials from the requestauthenticate({ credentials, request })- Validate the extracted credentials
Use cases:
- API key validation (fixed key in header)
- JWT token validation (stateless bearer tokens)
- Basic authentication (username/password in header)
- Any credential that can be validated synchronously on each request
import { InlineAuthenticationScheme } from 'api-machine';
import { ApiRequest } from 'api-machine';
class ApiKeyScheme extends InlineAuthenticationScheme {
constructor(private apiKey: string) {
super();
}
getSecurityScheme() {
return {
type: 'apiKey' as const,
in: 'header' as const,
name: 'X-API-Key',
};
}
getCredentials(request: ApiRequest): unknown {
const key = request.headers['x-api-key'];
if (!key) {
throw new UnauthorizedError('API key is required');
}
return key;
}
async authenticate(options: {
credentials: string;
request: ApiRequest;
}): Promise<void> {
if (options.credentials !== this.apiKey) {
throw new UnauthorizedError('Invalid API key');
}
}
}SessionAuthenticationScheme manages stateful authentication. It provides an AuthFlow to obtain a session, and then verifies that session on each request.
How it works:
getAuthFlow()defines the multi-step authentication process (e.g., challenge → authorization → token exchange)- User goes through the auth flow to obtain a session
- Session ID is stored (typically in cookies or request state)
- On each subsequent request, middleware calls
checkSession()to verify the session exists and is valid - Only after session is verified, the request continues
Key Methods:
getAuthFlow()- Define the authentication flow steps used to obtain a sessiongetAuthRouter(basePath?)- Generate a router that automatically exposes all auth steps as API endpointsgetMiddleware()- Middleware that checks the session on each request (inherited)
Usage:
// Create your session authentication scheme
const oauth2 = new OAuth2Scheme();
// Include in your main router
class MainRouter extends BaseApiRouter {
async routes() {
return [
oauth2.getAuthRouter('/auth'), // Auth endpoints
SecureApiRouter, // Protected API (uses oauth2 as authentication)
PublicRouter, // Public endpoints
];
}
}Use cases:
- OAuth2 with authorization code flow
- SAML-based authentication
- Multi-step authentication requiring user interaction
- Session-based systems where session state must be maintained
import { SessionAuthenticationScheme, AuthFlow, BaseApiEndpoint } from 'api-machine';
class OAuth2Scheme extends SessionAuthenticationScheme {
getSecurityScheme() {
return {
type: 'oauth2' as const,
flows: {
authorizationCode: {
authorizationUrl: 'http://localhost:4001/auth/challenge',
tokenUrl: 'http://localhost:4001/auth/token',
scopes: { 'read:data': 'Read data' },
},
},
};
}
getAuthFlow(): AuthFlow {
return {
// Challenge step
challenge: class extends BaseApiEndpoint {
override path = '/challenge';
override description = 'Generate authorization challenge (PKCE)';
override async handle() {
const challenge = generateChallenge();
return { challenge };
}
},
// Authorization step
authorization: class extends BaseApiEndpoint {
override path = '/authorize';
override description = 'User authorizes application at OAuth2 provider';
override async handle(request) {
// User is redirected to provider, returns with authorization code
const code = request.query.code as string;
return { code };
}
},
// Token exchange step
tokenExchange: class extends BaseApiEndpoint {
override path = '/token';
override description = 'Exchange authorization code for access token and session';
override async handle(request) {
const accessToken = await exchangeCodeForToken(request.body.code);
const session = await createSession(accessToken);
return { session };
}
},
};
}
}Session Validation Flow: After a session is obtained through the auth flow, each request:
- Extracts the session ID (from cookies, headers, etc.)
- Calls
checkSession()to verify the session still exists - Validates the session is not expired
- Sets
request.authenticated = trueand allows request to continue
An AuthFlow is a named collection of BaseApiEndpoint classes representing the complete multi-step authentication process used to obtain a session.
BaseApiEndpoint is the abstract class used for authentication flow steps, allowing each step to leverage endpoint features:
- Request validation (body, query, params, headers via valsan)
- Middleware support
- Standardized error handling
- OpenAPI integration
See src/authentication/auth-step.ts and src/authentication/auth-flow.ts for type definitions.
Typical step responsibilities:
challenge- Generate a random challenge or nonce (e.g., PKCE code challenge)authorization- Handle user interaction (redirect to OAuth provider, login form, etc.)tokenExchange- Exchange credentials for tokens and create a session
Step example with validation:
class TokenExchangeStep extends BaseApiEndpoint {
override path = '/token';
override description = 'Exchange code for tokens';
// Define request validation just like endpoints
override body = new ObjectValSan({
schema: {
code: new StringValidator(),
grantType: new StringValidator(),
}
});
override async handle(request) {
// request.body is validated
return { accessToken: 'token123' };
}
}Make routes explicitly public by setting authentication = null:
class PublicEndpoint extends BaseApiEndpoint {
override path = '/public';
override authentication = null; // No authentication required
async handle() {
return { public: true };
}
}This bypasses any parent router or server authentication.
Create custom authentication by extending InlineAuthenticationScheme or SessionAuthenticationScheme depending on your needs.
For credential-based auth (API keys, tokens):
import { InlineAuthenticationScheme } from 'api-machine';
import { ApiRequest } from 'api-machine';
class CustomKeyScheme extends InlineAuthenticationScheme {
getSecurityScheme() {
return {
type: 'apiKey' as const,
in: 'header' as const,
name: 'X-Custom-Key',
};
}
getCredentials(request: ApiRequest): unknown {
return request.headers['x-custom-key'];
}
async authenticate(options: {
credentials: string;
request: ApiRequest;
}): Promise<void> {
if (!await this.validateKey(options.credentials)) {
throw new UnauthorizedError('Invalid key');
}
}
private async validateKey(key: string): Promise<boolean> {
// Your validation logic
return key === 'valid-key';
}
}Required Methods:
getSecurityScheme(): Returns OpenAPI SecuritySchemeObjectgetMiddleware(): Returns Express middleware function (inherited)getSecurityRequirement(): Optional, returns OpenAPI SecurityRequirementObject
The authentication module exports the following. See src/authentication/index.ts for the complete list:
AuthenticationScheme- Base class for all schemesInlineAuthenticationScheme- For credential-based authSessionAuthenticationScheme- For stateful multi-step authBaseApiEndpoint- Base class for auth flow stepsAuthFlow- Type for collections of stepsBearerAuthenticationScheme- Built-in Bearer token schemeAuthenticatedRequest- Request type with auth flag
Authentication schemes automatically generate OpenAPI security definitions:
const bearerAuth = new BearerAuthenticationScheme({
checkToken: async (token) => await validate(token),
schemeName: 'BearerAuth',
description: 'JWT Bearer Authentication',
});
const oauth2Auth = new OAuth2Scheme();
// Automatically generates OAuth2 security scheme in OpenAPI spec-
Choose the right scheme type:
- Use
InlineAuthenticationSchemefor stateless auth that happens on every request (API keys, JWT tokens, bearer tokens) - Use
SessionAuthenticationSchemefor stateful auth with multi-step flows that obtain a session once, then check it on each request (OAuth2, SAML, login forms)
- Use
-
Inline scheme design:
- Keep
getCredentials()focused on extraction only - Keep
authenticate()focused on validation only - Runs on every request, so keep it fast
- Keep
-
Session scheme design:
- Define clear, distinct steps in
AuthFlow(challenge → authorization → tokenExchange) - Session is obtained once, then validated on subsequent requests
- Use efficient session lookup/validation to minimize request overhead
- Define clear, distinct steps in
-
Use server-level auth as default - Apply authentication at the server level and override only where needed
-
Make public routes explicit - Use
authentication = nullto clearly mark public endpoints -
Validate thoroughly:
- For inline: validate credentials against secure sources (database, JWT verification)
- For session: verify session exists and hasn't expired
-
Use descriptive names - Help API consumers understand your authentication approach
- Inline: use scheme names like "BearerAuth", "ApiKeyAuth"
- Session: use clear auth flow step names (e.g., 'challenge', 'authorization', 'tokenExchange')
-
Leverage cascading - Set auth at the appropriate level (server/router/endpoint) based on your needs
See examples/authentication-example.ts for a complete working example.