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' ? { id: 'router-user' } : false,
schemeName: 'BearerAuth',
});
async routes() {
return [SecureEndpoint];
}
}
const server = new RestServer({
port: 4000,
authentication: new BearerAuthenticationScheme({
checkToken: async (token: string) =>
token === 'server-token' ? { id: 'server-user' } : false,
}),
});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
Authentication resolves only through the namespaced api-machine.authn key.
The nearest dependency container wins, including registrations made in inject(). An explicit null under this key stops inheritance and makes the route public.
- Endpoint-level authentication has highest priority
- Router-level authentication applies if endpoint doesn't specify
- Server-level authentication applies as the default fallback
A public endpoint inside an authorization-protected router must explicitly set both authentication = null and authorization = null.
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' ? { id: 'admin-user' } : false,
});
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.)
if (token !== 'valid-token') return false;
return { id: 'user-123' };
},
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 - If
checkToken()returns a principal object, attaches it torequest.principal
Return a trusted { id: string } principal when routes use authorization. Return false to reject.
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) =>
token === 'valid-token' ? await findPrincipalForToken(token) : false,
}),
});All endpoints will require authentication unless a router or endpoint overrides it with authentication = null.
For deployments that must not start with an unintentionally anonymous endpoint, set requireAuthentication: true. Startup then fails if any endpoint has no effective scheme; an explicit authentication = null remains an intentional public exception. This option defaults to false for backward compatibility.
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' ? { id: 'router-user' } : false,
});
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' ? { id: 'admin-user' } : false,
schemeName: 'AdminAuth',
});
async handle() {
return { admin: true };
}
}Configure an authorizer at the server and declare requirements on protected endpoints. The authorizer is inherited through the Hidi container tree; endpoint or router authorizer values override inherited providers. A route requirement is separate from the service and is evaluated only when declared.
import {
ApiRequest,
BaseApiEndpoint,
BaseApiRouter,
BearerAuthenticationScheme,
RestServer,
} from 'api-machine';
class ReportsEndpoint extends BaseApiEndpoint {
override path = '/reports';
override authorization = { action: 'read', resource: 'reports' };
async handle(request: ApiRequest) {
return { principal: request.principal };
}
}
class ReportsRouter extends BaseApiRouter {
async routes() {
return [ReportsEndpoint];
}
}
class ReportsServer extends RestServer {
override router = ReportsRouter;
constructor() {
super({
port: 4000,
authentication: new BearerAuthenticationScheme({
checkToken: async (token) =>
token === 'valid-token' ? { id: 'user-123' } : false,
}),
authorizer: {
async evaluate({ principal, action, resource }) {
return {
allowed:
principal.id === 'user-123' &&
action === 'read' &&
resource === 'reports',
};
},
},
});
}
}
const server = new ReportsServer();Unauthenticated requests receive 401; authenticated requests denied by the policy receive 403. Set authentication = null on a router or endpoint to explicitly make it public. A route with an authorization requirement still needs a trusted principal, even if authentication was opted out; otherwise it receives 401.
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
Authentication engines may also expose a protection policy that decides whether a credential attempt is allowed before credentials are verified.
This is especially useful for:
- brute-force prevention
- temporary lockout after repeated failures
- server-side cooldown windows tied to a specific credential or subject
The shared IAM contract defines:
interface AuthenticationProtectionResult {
allowed: boolean;
retryAfterSeconds?: number;
}and the protection hooks:
interface AuthenticationProtection {
beforeAttempt(attempt): Promise<AuthenticationProtectionResult>;
onFailure(attempt): Promise<void>;
onSuccess(attempt): Promise<void>;
}When a protection result denies access, the HTTP layer may translate that into a 429 Too Many Requests response with a Retry-After header.
import { throwIfAuthenticationBlocked } from 'api-machine';
const result = { allowed: false, retryAfterSeconds: 45 };
throwIfAuthenticationBlocked(result);
// -> throws TooManyRequestsError('Authentication temporarily unavailable')
// -> full response includes Retry-After: 45import { InlineAuthenticationScheme, UnauthorizedError } from 'api-machine';
import { ApiRequest } from 'api-machine';
class ApiKeyScheme extends InlineAuthenticationScheme {
constructor(
private apiKey: string,
private principal: { id: 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<{ id: string }> {
if (options.credentials !== this.apiKey) {
throw new UnauthorizedError('Invalid API key');
}
return this.principal;
}
}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 - Middleware attaches the stored
session.principal, if present, before the request continues. Authorization requires that trusted identity.
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 - Applies the session driver's validation policy. Expiry and principal revocation are application/driver responsibilities.
- Sets
request.authenticated = true, attachessession.principalwhen present, and allows the request to continue
Create sessions from the principal returned by trusted authentication:
sessionDriver.createSession({ id: sessionId, principal });The built-in InMemorySessionDriver is demonstration-only: it checks existence, not expiry or principal deactivation. Its cookie lifetime is not server-side revocation. Use a production session driver with an explicit lifecycle policy.
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
override authorization = null; // No inherited policy requirement
async handle() {
return { public: true };
}
}Note: Set both authentication and authorization to null if you would like to override both. Clearing authentication alone does not clear an inherited authorization requirement.
Create custom authentication by extending InlineAuthenticationScheme or SessionAuthenticationScheme depending on your needs.
For credential-based auth (API keys, tokens):
import { InlineAuthenticationScheme, UnauthorizedError } 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<{ id: string }> {
if (!await this.validateKey(options.credentials)) {
throw new UnauthorizedError('Invalid key');
}
return { id: 'demo-api-client' };
}
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 schemeAuthenticatedPrincipal- Trusted identity with at least a string IDAuthenticatedRequest- Request type with auth flag and optional principal
Authentication schemes automatically generate OpenAPI security definitions:
const bearerAuth = new BearerAuthenticationScheme({
checkToken: async (token) =>
token === 'valid-token' ? await findPrincipalForToken(token) : false,
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 - Clear both authentication and inherited authorization requirements with
authentication = nullandauthorization = null -
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.