API Authentication
Bearer tokens, Cognito validation, scopes, and rate limits.
All /v1/ endpoints require a valid Cognito Bearer token unless marked @public. Authentication is enforced via Express middleware — never validate tokens inside individual controllers.
Request flow
Cognito JWT middleware
pnpm add jsonwebtoken jwks-rsa
pnpm add -D @types/jsonwebtoken// src/lib/cognito.ts
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: `https://cognito-idp.${process.env.COGNITO_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}/.well-known/jwks.json`,
cache: true,
rateLimit: true,
});
function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) {
client.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
}
export function verifyCognitoToken(token: string): Promise<jwt.JwtPayload> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
issuer: `https://cognito-idp.${process.env.COGNITO_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`,
algorithms: ["RS256"],
},
(err, decoded) => (err ? reject(err) : resolve(decoded as jwt.JwtPayload)),
);
});
}// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from "express";
import { verifyCognitoToken } from "../lib/cognito";
export async function authenticate(req: Request, res: Response, next: NextFunction) {
const auth = req.headers.authorization;
if (!auth?.startsWith("Bearer ")) {
return res
.status(401)
.json({ error: { code: "unauthorized", message: "Missing bearer token" } });
}
try {
const payload = await verifyCognitoToken(auth.slice(7));
req.user = {
sub: payload.sub!,
email: payload.email as string,
scopes: (payload["custom:scopes"] as string)?.split(" ") ?? [],
};
next();
} catch {
return res
.status(401)
.json({ error: { code: "unauthorized", message: "Invalid or expired token" } });
}
}// src/types/express.d.ts
declare namespace Express {
interface Request {
user?: {
sub: string;
email: string;
scopes: string[];
};
}
}Scope middleware
// src/middleware/scope.middleware.ts
import { Request, Response, NextFunction } from "express";
export function requireScope(...required: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userScopes = req.user?.scopes ?? [];
const hasScope = required.some((s) => userScopes.includes(s) || userScopes.includes("admin:*"));
if (!hasScope) {
return res.status(403).json({ error: { code: "forbidden", message: "Insufficient scope" } });
}
next();
};
}Scopes reference
| Scope | Access |
|---|---|
read:projects | List and view projects |
write:projects | Create and update projects |
read:users | View user profiles |
write:users | Update user profiles |
admin:* | Full admin access — bypasses scope checks |
Scopes are stored in the Cognito custom attribute custom:scopes as a space-separated string.
Rate limiting
pnpm add express-rate-limit// src/middleware/rate-limit.middleware.ts
import rateLimit from "express-rate-limit";
export const standardLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: { code: "rate_limit_exceeded", message: "Too many requests" } },
});
export const authLimiter = rateLimit({
windowMs: 60 * 1000,
max: 1000,
keyGenerator: (req) => req.user?.sub ?? req.ip ?? "anonymous",
});Apply globally in app.ts:
import { standardLimiter } from "./middleware/rate-limit.middleware";
app.use(standardLimiter);
app.use("/v1", authLimiter, v1Router);| Tier | Limit | Window | Key |
|---|---|---|---|
| Unauthenticated | 100 req | 1 min | IP address |
| Authenticated | 1,000 req | 1 min | Cognito sub |
| Service account | 10,000 req | 1 min | API key (internal) |
Error responses
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired token.",
"requestId": "req_01HZ..."
}
}| Status | Code | When |
|---|---|---|
| 401 | unauthorized | Missing or invalid token |
| 403 | forbidden | Valid token, insufficient scope |
| 429 | rate_limit_exceeded | Rate limit hit |
Testing with curl
# Get token from Cognito (or copy from browser devtools)
export TOKEN="eyJhbG..."
curl http://localhost:4000/v1/projects \
-H "Authorization: Bearer $TOKEN"