UXDL Docs

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

bash
pnpm add jsonwebtoken jwks-rsa
pnpm add -D @types/jsonwebtoken
typescript
// 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)),
    );
  });
}
typescript
// 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" } });
  }
}
typescript
// src/types/express.d.ts
declare namespace Express {
  interface Request {
    user?: {
      sub: string;
      email: string;
      scopes: string[];
    };
  }
}

Scope middleware

typescript
// 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

ScopeAccess
read:projectsList and view projects
write:projectsCreate and update projects
read:usersView user profiles
write:usersUpdate 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

bash
pnpm add express-rate-limit
typescript
// 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:

typescript
import { standardLimiter } from "./middleware/rate-limit.middleware";
 
app.use(standardLimiter);
app.use("/v1", authLimiter, v1Router);
TierLimitWindowKey
Unauthenticated100 req1 minIP address
Authenticated1,000 req1 minCognito sub
Service account10,000 req1 minAPI key (internal)

Error responses

json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or expired token.",
    "requestId": "req_01HZ..."
  }
}
StatusCodeWhen
401unauthorizedMissing or invalid token
403forbiddenValid token, insufficient scope
429rate_limit_exceededRate limit hit

Testing with curl

bash
# Get token from Cognito (or copy from browser devtools)
export TOKEN="eyJhbG..."
 
curl http://localhost:4000/v1/projects \
  -H "Authorization: Bearer $TOKEN"

Official documentation