@access-tokens/express
v1.1.0
Published
Express routes and middleware for personal access token authentication
Downloads
997
Readme
@access-tokens/express
Express routes and middleware for Personal Access Token (PAT) authentication with OAuth 2.0-compatible JWT token exchange.
Features
- Ready-to-Use Routes: Pre-built authentication and admin token management endpoints
- JWT Token Exchange: OAuth 2.0-compatible token endpoint for PAT-to-JWT exchange
- Express Middleware:
requireJwt,requireAdmin, andrequireRolemiddleware for route protection - JOSE Integration: Industry-standard JWT signing and verification
- TypeScript: Full type safety with Express request augmentation
- Flexible Configuration: Customizable paths, token lifetime, and key management
Installation
npm install @access-tokens/express @access-tokens/coreQuick Start
import express from "express";
import { DynamoDBPat } from "@access-tokens/core";
import {
createAuthRouter,
createAdminTokensRouter,
createRequireJwt,
createRequireAdmin,
createRequireRole,
buildSignerVerifier,
generateKeySet,
} from "@access-tokens/express";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const app = express();
app.use(express.json());
// Initialize DynamoDB
const dynamoClient = new DynamoDBClient({ region: "us-east-1" });
const docClient = DynamoDBDocumentClient.from(dynamoClient);
const pat = new DynamoDBPat({ tableName: "tokens", docClient });
// Generate JWT signing keys
const keySet = await generateKeySet("my-key-id-1");
const signerVerifier = await buildSignerVerifier({
keySet,
issuer: "my-app",
ttl: "1h",
});
// Add authentication and token admin routes
app.use("/auth", createAuthRouter({ pat, signerVerifier }));
app.use("/admin", createAdminTokensRouter({ pat, signerVerifier }));
const requireJwt = createRequireJwt({ signerVerifier });
const requireAdmin = createRequireAdmin();
const requireEditor = createRequireRole({ role: "editor" });
// Your protected routes
app.get("/api/data", requireJwt, (req, res) => {
res.json({
message: "User data",
user: req.user, // { sub, owner, admin, roles }
});
});
app.get("/api/admin/data", requireJwt, requireAdmin, (req, res) => {
res.json({
message: "Admin data",
user: req.user, // { sub, owner, admin, roles }
});
});
app.put("/api/content", requireJwt, requireEditor, (req, res) => {
res.json({
message: "Content updated",
user: req.user, // { sub, owner, admin, roles }
});
});
app.listen(3000, () => console.log("Server running on port 3000"));API Reference
Routes
createAuthRouter(options)
Creates an Express router with authentication endpoints.
Options:
{
pat: DynamoDBPat; // DynamoDBPat instance
signerVerifier: JwtSignerVerifier; // JWT signer/verifier from buildSignerVerifier()
logger?: pino.Logger; // Optional logger
}Note: JWT lifetime (TTL) is configured in buildSignerVerifier().
Endpoints:
POST /token- Exchange PAT for JWT (OAuth 2.0 token endpoint)Request Body:
{ grant_type?: "client_credentials"; // Optional, must be "client_credentials" if provided client_secret?: string; // PAT (for OAuth 2.0 client_secret_post method) client_id?: string; // Optional, accepted but not used state?: string; // Optional, echoed back in response }Authentication Methods (checked in this order):
- Body parameter (OAuth 2.0
client_secret_post): Includeclient_secretin request body - Basic authentication (OAuth 2.0
client_secret_basic): UseAuthorization: Basic <base64>header (format:Basic base64(":<token>")) - Bearer token: Use
Authorization: Bearer <token>header
Response:
{ "access_token": "eyJ...", // The signed JWT "token_type": "Bearer", // Always "Bearer" "expires_in": 3600, // JWT lifetime in seconds "state": "..." // Optional, echoed from request }- Body parameter (OAuth 2.0
Examples:
# Method 1: Body parameter (client_secret_post)
curl -X POST http://localhost:3000/auth/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_secret":"pat_abc123..."}'
# Method 2: Basic authentication (client_secret_basic)
curl -X POST http://localhost:3000/auth/token \
-H "Authorization: Basic $(echo -n ":pat_abc123..." | base64)"
# Method 3: Bearer token (non-OAuth)
curl -X POST http://localhost:3000/auth/token \
-H "Authorization: Bearer pat_abc123..."createAdminTokensRouter(options)
Creates an Express router with admin token management endpoints. Requires JWT authentication and admin privileges.
Options:
{
pat: DynamoDBPat; // DynamoDBPat instance
signerVerifier: JwtSignerVerifier; // JWT signer/verifier
logger?: pino.Logger; // Optional logger
}Endpoints:
GET /tokens- List tokens- Query Params:
afterTokenId,limit,includeRevoked,includeExpired,includeSecretPhc,hasRole - Response:
{ "records": [...] }
- Query Params:
POST /tokens- Issue a new token- Request Body:
{ "owner": "[email protected]", "isAdmin"?: false, "roles"?: ["reader"], "tokenId"?: "...", "expiresAt"?: 1234567890 } - Response:
{ "token": "pat_...", "record": {...} }
- Request Body:
PUT /tokens/:tokenId- Register pre-generated token- Request Body:
{ "secretPhc": "...", "owner": "...", "isAdmin"?: false, "roles"?: ["reader"], "expiresAt"?: 1234567890 } - Response:
{ "record": {...} }
- Request Body:
PATCH /tokens/:tokenId- Update token- Request Body:
{ "owner"?: "...", "isAdmin"?: true, "secretPhc"?: "...", "roles"?: ..., "expiresAt"?: 1234567890 } - Roles Update Syntax:
{ "roles": ["role1", "role2"] } // Replace all roles { "roles": { "add": ["admin"] } } // Atomic add (cannot combine with remove) { "roles": { "remove": ["guest"] } } // Atomic remove (cannot combine with add) - Response: 204 No Content
- Request Body:
PUT /tokens/:tokenId/revoke- Revoke token- Request Body:
{ "expiresAt"?: 1234567890 }(optional) - Response: 204 No Content
- Request Body:
PUT /tokens/:tokenId/restore- Restore revoked token- Response: 204 No Content
POST /tokens/batch- Batch retrieve tokens- Request Body:
{ "tokenIds": ["id1", "id2"], "includeSecretPhc"?: false } - Response:
{ "found": [...], "missing": [...] }
- Request Body:
Note: All endpoints require JWT authentication and admin privileges. They
use requireJwt and requireAdmin middleware internally.
Middleware
createRequireJwt(options)
Creates middleware that validates JWT tokens and populates req.user.
Options:
{
signerVerifier: JwtSignerVerifier; // JWT signer/verifier from buildSignerVerifier()
logger?: pino.Logger; // Optional logger
}Request Extension:
req.user = {
sub: string; // Token ID
owner: string; // Token owner
admin: boolean; // Admin status
roles: string[]; // Array of role strings
};Note: The roles array comes from the JWT payload and reflects the roles assigned to the token at the time the JWT was issued.
Usage:
const requireJwt = createRequireJwt({ signerVerifier });
app.get("/protected", requireJwt, (req, res) => {
console.log("User:", req.user?.owner);
res.json({ data: "secret" });
});createRequireAdmin(options?)
Creates middleware that requires req.user.admin to be true. Must be used after requireJwt.
Options:
{
logger?: pino.Logger; // Optional logger
}Usage:
const requireAdmin = createRequireAdmin();
app.delete("/users/:id", requireJwt, requireAdmin, (req, res) => {
// Only admin users can access this
res.json({ success: true });
});createRequireRole(options)
Creates middleware that requires req.user.roles to include a specific role. Must be used after requireJwt.
Options:
{
role: string; // Required role name
logger?: pino.Logger; // Optional logger
}Usage:
const requireEditor = createRequireRole({ role: "editor" });
app.put("/content/:id", requireJwt, requireEditor, (req, res) => {
// Only users with "editor" role can access this
res.json({ success: true });
});JWT Utilities
generateKeySet(kid: string, algorithm?: "EdDSA" | "RS256")
Generates a new asymmetric key set for JWT signing.
Parameters:
kid: string- Key ID (required) - unique identifier for this key setalgorithm?: "EdDSA" | "RS256"- Signing algorithm (default: "EdDSA")
Returns:
{
active_kid: string; // The active key ID
private_keys: JWK[]; // Array of private keys in JWK format
public_keys: JWK[]; // Array of public keys in JWK format
}Example:
const keySet = await generateKeySet("my-key-id-1");
// or with specific algorithm
const rsaKeySet = await generateKeySet("rsa-key-1", "RS256");Note: Store keys securely (e.g., AWS Secrets Manager, environment variables). Generate once and reuse.
buildSignerVerifier(config)
Creates JWT signer and verifier from a key set.
Config:
{
keySet: KeySet; // From generateKeySet()
issuer: string; // JWT issuer claim
ttl: string; // Token time-to-live (e.g., "1h", "30m")
}Returns:
JwtSignerVerifier {
sign: (claims) => Promise<string>;
verify: (jws: string) => Promise<JWTVerifyResult>;
jwks: { keys: readonly JWK[] };
}Example:
const keySet = await generateKeySet("my-key-id");
const signerVerifier = await buildSignerVerifier({
keySet,
issuer: "my-app",
ttl: "1h",
});OAuth 2.0 Flow
This library implements a simplified OAuth 2.0 client credentials flow:
- Client authenticates with PAT to
POST /auth/token - Server validates PAT and issues short-lived JWT (default: 1 hour)
- Client uses JWT for subsequent API requests via
Authorization: Bearer <jwt> - Server validates JWT using
requireJwtmiddleware
Why JWT Exchange?
- Performance: Avoid DynamoDB lookup and scrypt on every request
- Scalability: Stateless JWT verification
- Short-lived: Reduced risk if JWT is compromised
- Standard: OAuth 2.0 compatible
Security Best Practices
- Use HTTPS - Always use TLS in production
- Secure Key Storage - Store private keys in secure vaults (AWS Secrets Manager, etc.)
- Short JWT Lifetime - Default 1 hour is recommended
- Rotate Keys - Implement key rotation for long-running services
- Validate Issuer/Audience - Configure these in production
- Rate Limiting - Add rate limiting to
/auth/tokenendpoint
Key Generation
Generate keys using the included tool:
pnpm --filter @access-tokens/express genkeyOr programmatically:
import { generateKeySet } from "@access-tokens/express";
const keys = await generateKeySet("my-key-id-1");
console.log("Public Key:", keys.public_keys);
console.log("Private Key:", keys.private_keys);Store these keys securely and pass them to your application via environment variables.
Error Handling
All endpoints return standard HTTP error codes:
400 Bad Request- Invalid request body or parameters401 Unauthorized- Invalid or missing token403 Forbidden- Insufficient permissions (not admin)404 Not Found- Token not found500 Internal Server Error- Server error
Example error response:
{
"error": "Invalid token",
"details": "Token has been revoked"
}TypeScript Types
The package extends Express types:
declare global {
namespace Express {
interface Request {
user?: {
sub: string; // Token ID
owner: string; // Token owner
admin: boolean; // Admin status
roles: string[]; // Array of role strings
};
logger?: Logger; // Optional Pino logger
clientIp?: string; // Optional client IP (from request-ip)
}
}
}Requirements
- Node.js 20+
- Express 4.18+ or 5.0+
- @access-tokens/core
Related Packages
- @access-tokens/core - Core token management library
- @access-tokens/client - HTTP client for PAT API
- @access-tokens/cli - Command-line token management
License
ISC © 2025 Loan Crate, Inc.
