@jtekt/express-authentication-middleware
v0.0.6
Published
Express middleware that authenticates incoming requests and attaches the authenticated user to `req.user` and `res.locals.user`.
Downloads
0
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.
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: {
url: '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 against the configured URL. All other strategies are skipped. - OIDC vs local JWT — if a Bearer token (or raw token) is found in the request, 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.localJwtis configured)
- Token with a
- Identification — if
strategies.identificationis configured and no earlier strategy matched, the token is forwarded to an external 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.
The token is read from the first available location, checked in this order:
Authorizationheader —Bearer <token>or raw token valueCookieheader —jwtortokencookie- Query string —
jwtortokenparameter
API 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;
localJwt?: LocalJwtOptions;
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 = {
url: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | ----------------------------------------------------------------- |
| url | 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 = {
issuer: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------- |
| issuer | string | yes | JWKS URI of the OIDC provider (e.g. https://gitlab.com) |
| 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.
LocalJwtOptions
type LocalJwtOptions = {
jwtSecret: string;
identifierField?: string;
};| Option | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------- |
| jwtSecret | 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> |
| 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 and req.jwt in route handlers:
import type { ReqWithUser } from 'express-authentication-middleware/types';
import type { RequestHandler } from 'express';
const handler: RequestHandler = (req: ReqWithUser, res) => {
console.log(req.user); // User | undefined
console.log(req.jwt); // string | undefined
};User and ReqWithUser types
// types/index.ts (exported)
type User = Record<string, any> & {
id: string;
};
type ReqWithUser = Request & {
user?: User;
jwt?: string;
};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
Strategy selection
All strategies are nested under the strategies key. Enable only the ones your application needs.
// API key only
middleware({
strategies: {
apiKey: { url: 'https://api-keys.example.com/validate' },
},
});
// OIDC only
middleware({
strategies: {
oidc: { issuer: 'https://accounts.example.com' },
},
});
// Local JWT only
middleware({
strategies: {
localJwt: { jwtSecret: process.env.JWT_SECRET! },
},
});
// Identification endpoint only
middleware({
strategies: {
identification: { url: 'https://api.example.com/me' },
},
});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: {
url: '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.
Examples
API key
Clients send the key in the x-api-key header:
x-api-key: my-secret-keyapp.use(
middleware({
strategies: {
apiKey: {
url: 'https://api-keys.example.com/validate',
},
},
})
);OIDC
Clients send an OIDC-signed Bearer token:
Authorization: Bearer <oidc-jwt>app.use(
middleware({
strategies: {
oidc: {
issuer: 'https://gitlab.com',
identifierField: 'sub',
},
},
})
);Local JWT
Clients send a token signed by your application:
Authorization: Bearer <local-jwt>app.use(
middleware({
strategies: {
localJwt: {
jwtSecret: process.env.JWT_SECRET!,
},
},
})
);Identification endpoint
The middleware forwards the token to your identification service:
app.use(
middleware({
strategies: {
identification: {
url: 'https://api.example.com/me',
},
},
})
);Accessing the user in route handlers
import type { ReqWithUser } from 'express-authentication-middleware/types';
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 |
| Identification endpoint returns non-2xx | 401 |
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