@jtekt/express-authentication-middleware
v0.2.0
Published
Express middleware that authenticates incoming requests and attaches the authenticated user to `req.user` and `res.locals.user`.
Readme
express-authentication-middleware
Express middleware that authenticates incoming requests and attaches the authenticated user to req.user and res.locals.user.
The middleware supports four authentication strategies — API key, OIDC, local JWT, and identification — and selects the right one automatically based on the incoming request.
Table of Contents
- Features
- Installation
- Quick Start
- How It Works
- API Reference
- TypeScript Support
- Configuration Examples
- Accessing the user in route handlers
- Optional authentication
- Error handling
- Contributing
Features
- Populates
req.userandres.locals.userfor every authenticated request - Four authentication strategies, selected automatically per request:
- API key — validates against an external API key manager
- OIDC — verifies JWTs signed by an OpenID Connect provider via JWKS
- Local JWT — verifies JWTs signed with a shared secret
- Identification — resolves the user by forwarding the token to an external endpoint
identifierFieldper strategy — maps any response field to the user identifieridentifierFieldNameglobally — controls what property name the identifier is written to onreq.user- Full TypeScript support with exported types for all configuration objects and the extended request type
Installation
npm install @jtekt/express-authentication-middlewareyarn add @jtekt/express-authentication-middlewarepnpm add @jtekt/express-authentication-middlewareQuick Start
import express from 'express';
import middleware, { type Options } from '@jtekt/express-authentication-middleware';
const app = express();
const options: Options = {
strategies: {
apiKey: {
validationUrl: 'https://api-keys.example.com/validate',
},
},
};
app.use(middleware(options));
app.get('/profile', (req, res) => {
res.json(res.locals.user);
});
app.listen(3000);How It Works
The middleware inspects every incoming request and determines which configured strategy to apply:
- API key — if the
x-api-keyheader is present andstrategies.apiKeyis configured, the key is validated againstvalidationUrl. All other strategies are skipped. - OIDC vs local JWT — if a Bearer token is found, the middleware decodes its header to check for a
kidclaim:- Token with a
kid→ OIDC verification (ifstrategies.oidcis configured) - Token without a
kid→ local JWT verification (ifstrategies.localis configured)
- Token with a
- Identification — if
strategies.identificationis configured and no earlier strategy matched, the token (Bearer or API key) is forwarded to the identification URL which returns the user object.
Once a strategy succeeds, req.user and res.locals.user are populated and next() is called. If no strategy matches or a strategy fails, next(err) is called with an appropriate HTTP error.
Example of the strategies flow:
flowchart TD
request([Request])
apiKey[Send to API Key Manager]
oidc[Validate with OIDC JWKS]
identification[Send to identification service]
local[Local validation and decoding of JWT]
hasApiKey{Headers has X-API-Key?}
jwtHasKid{JWT Has Kid}
strategyIsLocal{Is strategy local?}
request --> hasApiKey
hasApiKey -- yes --> apiKey
hasApiKey -- no --> jwtHasKid
jwtHasKid -- yes --> oidc
jwtHasKid -- no --> strategyIsLocal
strategyIsLocal -- yes --> local
strategyIsLocal -- no --> identificationAPI Reference
middleware(options)
The default export. Returns an Express middleware function.
import middleware from '@jtekt/express-authentication-middleware';
app.use(middleware(options));Parameters
| Parameter | Type | Description |
| --------- | --------- | ---------------------------------------- |
| options | Options | Authentication configuration (see below) |
Returns (req, res, next) => Promise<void>
Options
type Options = {
strategies: {
apiKey?: ApiKeyOptions;
oidc?: OidcOptions;
local?: LocalOptions;
identification?: IdentificationOptions;
};
identifierFieldName?: string;
};strategies is required and at least one strategy must be enabled. If strategies is empty the middleware responds with 500 No authentication methods configured.
| Option | Type | Default | Description |
| --------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------- |
| strategies | object | — | Strategies to enable (see each strategy below) |
| identifierFieldName | string | 'id' | Property name written on req.user when identifierField is used to remap an identifier (see below) |
ApiKeyOptions
type ApiKeyOptions = {
validationUrl: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------ |
| validationUrl | string | yes | API key validation endpoint. Receives POST { api_key: "<key>" } |
| identifierField | string | no | Field in the validation response to remap as the user identifier |
OidcOptions
type OidcOptions = {
jwksUri: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------- |
| jwksUri | string | yes | JWKS URI of the OIDC provider (e.g. {KEYCLOAK_URL}/realms/{REALM_NAME}/protocol/openid-connect/certs) |
| identifierField | string | no | Field in the verified token payload to remap as the user identifier |
The middleware fetches the signing key from the JWKS URI using the kid from the token header, then verifies the signature with jwt.verify. JWKS responses are cached and rate-limited internally.
LocalOptions
type LocalOptions = {
secret: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------- |
| secret | string | yes | Shared secret used to verify the JWT signature |
| identifierField | string | no | Field in the verified token payload to remap as the user identifier |
IdentificationOptions
type IdentificationOptions = {
url: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------- |
| url | string | yes | Identification endpoint URL. Receives GET with Authorization: Bearer <token> or X-API-Key: <key> |
| identifierField | string | no | Field in the response body to remap as the user identifier |
TypeScript Support
Extended request type
Use ReqWithUser for typed access to req.user in route handlers:
import type { ReqWithUser } from '@jtekt/express-authentication-middleware';
import type { RequestHandler } from 'express';
const handler: RequestHandler = (req: ReqWithUser, res) => {
console.log(req.user); // User | undefined
};User and ReqWithUser types
type User = Record<string, any> & {
id: string;
};
type ReqWithUser = Request & {
user?: User;
};User is an open object (Record<string, any>) so all claims from the token payload or API response are accessible. The id field is populated when identifierField / identifierFieldName are configured.
Configuration Examples
API Key
Clients send the key in the x-api-key header:
x-api-key: my-secret-keyapp.use(
middleware({
strategies: {
apiKey: {
validationUrl: 'https://api-keys.example.com/validate',
},
},
})
);sequenceDiagram
Client->>App: X-Api-Key: abc
App->>API Key Manager: POST /validate { api_key: "abc" }
API Key Manager->>App: { user_id: 123 }
App->>Client: DataOIDC
Clients send an OIDC-signed Bearer token:
Authorization: Bearer <oidc-jwt>app.use(
middleware({
strategies: {
oidc: {
jwksUri: '{KEYCLOAK_URL}/realms/{REALM_NAME}/protocol/openid-connect/certs',
identifierField: 'sub',
},
},
})
);sequenceDiagram
Client->>App: Authorization: Bearer <Access token>
App->>OIDC Provider: Fetch JWKS
OIDC Provider->>App: JWKS
App->>App: Validate and decode access token
App->>Client: DataLocal JWT
Clients send a token signed by your application:
Authorization: Bearer <local-jwt>app.use(
middleware({
strategies: {
local: {
secret: process.env.JWT_SECRET!,
},
},
})
);sequenceDiagram
Client->>App: Authorization: Bearer <JWT>
App->>App: Verify and decode token
App->>Client: DataIdentification endpoint
The middleware forwards the token to your identification service:
app.use(
middleware({
strategies: {
identification: {
url: 'https://account-manager.example.com/v3/users/self',
},
},
})
);sequenceDiagram
Client->>App: Authorization: Bearer <JWT>
App->>Account manager: GET /v3/users/self Authorization: Bearer <JWT>
Account manager->>App: { user: {...} }
App->>Client: DataCombining strategies
app.use(
middleware({
strategies: {
local: {
secret: process.env.JWT_SECRET!,
},
apiKey: {
validationUrl: 'https://api-key-manager.example.com/validate',
},
identification: {
url: 'https://account-manager.example.com/v3/users/self',
},
oidc: {
jwksUri: '{KEYCLOAK_URL}/realms/{REALM_NAME}/protocol/openid-connect/certs',
},
},
})
);identifierField and identifierFieldName
These two options work together to normalise user identity across strategies:
identifierField(per strategy) — the field name in the authentication response or token payload that holds the user's identifier.identifierFieldName(top-level) — the property name that identifier value is written to onreq.user. Defaults to'id'.
// The API key validation endpoint returns { user_id: "123", email: "..." }
// identifierField says "read user_id as the identifier"
// identifierFieldName says "write it as req.user['my-id']"
middleware({
strategies: {
apiKey: {
validationUrl: 'https://api-keys.example.com/validate',
identifierField: 'user_id',
},
},
identifierFieldName: 'my-id',
});
// req.user['my-id'] === "123"If identifierField is provided but the field is not present in the response, the middleware calls next with a 400 error.
If neither identifierField nor identifierFieldName is configured, the user object is attached to req.user as-is with no remapping.
Accessing the user in route handlers
import type { ReqWithUser } from '@jtekt/express-authentication-middleware';
app.get('/profile', (req: ReqWithUser, res) => {
// Via the request object
console.log(req.user?.id);
// Via res.locals — useful in view templates or downstream middleware
console.log(res.locals.user);
res.json(req.user);
});Optional authentication
Register the middleware only on the routes that require it:
// Public — no middleware
app.get('/health', (_req, res) => res.send('ok'));
// Protected — middleware applied to everything under /api
app.use('/api', middleware(options));
app.get('/api/profile', (req: ReqWithUser, res) => res.json(req.user));Error handling
Authentication failures are forwarded to next(err) as http-errors objects. Add a standard Express error handler to format them:
import type { ErrorRequestHandler } from 'express';
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
const status = err.status ?? 500;
res.status(status).json({ error: err.message });
};
app.use(errorHandler);Common error scenarios:
| Situation | Status |
| --------------------------------------- | ------ |
| No authentication methods configured | 500 |
| x-api-key present but invalid | 401 |
| Token present but signature invalid | 401 |
| Token missing or malformed | 401 |
| identifierField not found in response | 400 |
Contributing
# Install dependencies
npm install
# Start the development server (runs example.ts with hot reload)
npm run dev
# Compile TypeScript to dist/
npm run build