Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/BladimirGS/judicial-backend/llms.txt

Use this file to discover all available pages before exploring further.

Every protected route in Judicial Backend is guarded by the protect middleware, defined in src/modules/auth/middlewares/protect.middleware.ts. It accepts only RS256-signed JWTs whose public key is fetched on-demand from the external authentication API’s JWKS endpoint. On success it attaches the decoded payload to req.user and calls next(); on any failure it raises a typed AppError that the global error handler converts into a 401 response.

Verification Flow

1

Extract Bearer token from the Authorization header

The middleware reads req.headers.authorization. If the header is absent or does not start with "Bearer " it immediately throws:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
    throw new AppError('Token no proporcionado', 401, 'UNAUTHORIZED');
}
const token = authHeader.split(' ')[1];
No further processing occurs — the request is rejected before any cryptographic work is attempted.
2

Decode the JWT header to read the key ID (kid)

jwt.decode(token, { complete: true }) performs a non-verifying decode to extract the JOSE header. The kid field identifies which key in the JWKS set was used to sign the token:
const decoded = jwt.decode(token, { complete: true });
const decodedHeader = decoded?.header as jwt.JwtHeader | undefined;

if (!decodedHeader?.kid) {
    throw new AppError(
        'Token inválido: no se pudo identificar la clave de firma',
        401,
        'INVALID_TOKEN',
    );
}
If kid is missing the token cannot be verified and is rejected as invalid.
3

Fetch the matching public key from the JWKS endpoint

The JwksClient (from jwks-rsa) is passed the kid extracted in the previous step. It resolves the correct key object from the remote JWKS document and returns the PEM-encoded public key:
const getPublicKey = createGetPublicKey(jwksClient);
const publicKey = await getPublicKey(decodedHeader.kid);
The JWKS endpoint is ${EXTERNAL_AUTH_URL}/api/AuthJWT/GetJwks. The client is initialised once at module load time with caching enabled (see JWKS Caching below).
4

Verify RS256 signature, issuer, and audience

With the public key in hand, jwt.verify performs full cryptographic verification and claim validation in a single call:
const payload = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    issuer: envs.JWT_ISSUER,
    audience: envs.JWT_AUDIENCE,
});
jsonwebtoken will throw a TokenExpiredError if the exp claim is in the past, a JsonWebTokenError for any signature or claim mismatch, or a general error for unexpected failures. All three paths are caught and converted to descriptive AppError instances with 401 status codes.
5

Populate req.user and call next()

Once verification succeeds the decoded payload is cast to JwtPayload and attached to the request object:
req.user = payload as JwtPayload;
next();
Every downstream route handler and controller in the same request lifecycle can now access req.user in a fully-typed way.

The JwtPayload Type

JwtPayload is defined in src/modules/auth/types/auth.types.ts as an open interface that captures the standard JWT registered claims while allowing arbitrary additional claims from the external identity provider:
export interface JwtPayload {
    sub?: string;              // Subject — typically the user identifier
    iss?: string;              // Issuer — must match JWT_ISSUER
    aud?: string | string[];   // Audience — must match JWT_AUDIENCE
    exp?: number;              // Expiration time (Unix timestamp)
    iat?: number;              // Issued-at time (Unix timestamp)
    [key: string]: unknown;    // Any additional claims from the external API
}
All standard claims are optional at the type level because jwt.verify enforces the critical ones (iss, aud, exp) at runtime via the options object. The index signature ([key: string]: unknown) allows downstream handlers to access provider-specific claims (for example a nombre or roles field) without casting to any.

Express Global Type Augmentation

The same auth.types.ts file augments the global Express namespace so that req.user is statically typed across the entire codebase:
declare global {
    namespace Express {
        interface Request {
            user?: JwtPayload;
        }
    }
}
No import is required in controller files — TypeScript picks up the augmentation automatically as long as the types file is included in the compilation. The ? makes user optional, which correctly models unauthenticated contexts (for example inside the auth routes themselves).

JWKS Caching

The JwksClient is configured with cache: true, cacheMaxEntries: 5, and cacheMaxAge: 600_000 (10 minutes). This means the public key for a given kid is fetched from the external API at most once every 10 minutes, regardless of how many requests arrive. The cache significantly reduces latency and eliminates unnecessary network calls to the authentication server on every protected request.
The default client is created once at module load time:
const JWKS_PATH = '/api/AuthJWT/GetJwks';

const defaultJwksClient = new JwksClient({
    jwksUri: `${envs.EXTERNAL_AUTH_URL}${JWKS_PATH}`,
    cache: true,
    cacheMaxEntries: 5,   // Keep keys for up to 5 different kid values
    cacheMaxAge: 600000,  // Refresh cache after 10 minutes (ms)
});

export const protect = createProtectMiddleware(defaultJwksClient);
The factory function createProtectMiddleware(jwksClient) accepts any object that satisfies JwksClientLike, which makes the middleware straightforward to test by injecting a stub client.

Error Responses

ConditionHTTP StatusError Code
Authorization header missing or malformed401UNAUTHORIZED
JWT header has no kid field401INVALID_TOKEN
JWT_ISSUER or JWT_AUDIENCE env vars not set500CONFIG_ERROR
Token signature invalid or claims mismatch401INVALID_TOKEN
Token has expired (exp in the past)401TOKEN_EXPIRED
Unexpected error during verification401VALIDATION_ERROR
All errors are handled by asyncHandler, which forwards them to the global error handler middleware. Clients receive a consistent JSON envelope:
{
  "status": "error",
  "message": "Token expirado",
  "code": "TOKEN_EXPIRED"
}

How protect Is Applied in routes/index.ts

Protection is registered once per route group, not per individual endpoint. This is the complete src/routes/index.ts:
router.use('/auth', authLimiter, authRoutes);                          // Public
router.use('/apelaciones', apiLimiter, protect, apelacionRoutes);      // Protected
router.use('/busquedas', apiLimiter, protect, BusquedaRoutes);         // Protected
router.use('/estadisticas', apiLimiter, protect, estadisticaRoutes);   // Protected
The protect middleware is placed after the rate limiter and before the module router, so rate limiting applies to all requests (including bad-token requests) while JWT verification happens before any business logic runs.

Adding Protection to a New Route

To protect a new module, import protect and apiLimiter in src/routes/index.ts and add a router.use line following the same pattern:
import newRoutes from '../modules/new-module/new-module.routes';

router.use('/new-route', apiLimiter, protect, newRoutes);
No changes are needed inside the module itself — protection is enforced at the router level and req.user will be available to every handler in newRoutes automatically.
During automated testing, set NODE_ENV=test. You can then inject a mock JwksClient via createProtectMiddleware(mockClient) in your test setup to supply a known key pair without making real network calls. Because protect is exported as a plain middleware created from a factory, replacing the JWKS client requires no monkey-patching.

Build docs developers (and LLMs) love