fastify-txstate 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -190,25 +190,54 @@ const server = new Server({
190
190
  ```
191
191
  The `authenticate` function should return a `FastifyTxStateAuthInfo` object with at least `username`, `sessionId`, and `token`. This gives us a predictable interface, since raw JWT claims may vary by provider. Returning `undefined` means the request is unauthenticated (but allowed). Throwing an error sends a 401 response.
192
192
 
193
- We provide two built-in implementations: `unifiedAuthenticate` for TxState's Unified Auth service, and `oauthAuthenticate` for standard OAuth/OIDC providers. You can also write your own for any authentication scheme — API keys, session lookups, custom JWTs, etc.
193
+ We provide a built-in `jwtAuthenticate` that validates JWT tokens from any combination of issuer types — OAuth/OIDC providers (with auto-discovery), TxState Unified Auth, raw JWKS endpoints, symmetric secrets, and asymmetric public keys. You can also write your own `authenticate` for any authentication scheme — API keys, session lookups, custom token formats, etc.
194
194
 
195
- # OAuth Authentication
196
- The `oauthAuthenticate` function we provide validates JWT tokens (access tokens or ID tokens) from any OAuth/OIDC provider. It uses the token's `iss` claim to auto-discover the provider's JWKS endpoint via `.well-known/openid-configuration` or `.well-known/oauth-authorization-server`, then verifies the signature locally.
195
+ # JWT Authentication
196
+ The `jwtAuthenticate` function validates JWTs from the `Authorization: Bearer` header or a session cookie. It supports any mix of these issuer types:
197
+
198
+ - **OAuth/OIDC** — auto-discovers the provider's JWKS via `.well-known/openid-configuration` or `.well-known/oauth-authorization-server` from the issuer's `iss` claim.
199
+ - **TxState Unified Auth** — JWKS + a `/validateToken` poll for centralized deauth.
200
+ - **JWKS endpoint** — a direct JWKS URL (no discovery).
201
+ - **Asymmetric public key** — a PEM-encoded RSA/EC public key.
202
+ - **Symmetric secret** — an HMAC shared secret.
197
203
 
198
204
  For providers like Google that issue opaque access tokens, have the client send the ID token instead — it's a standard JWT that proves the user's identity without requiring a round-trip to the provider on every request.
199
205
  ```javascript
200
- import Server, { oauthAuthenticate } from 'fastify-txstate'
206
+ import Server, { jwtAuthenticate } from 'fastify-txstate'
201
207
  const server = new Server({
202
- authenticate: req => oauthAuthenticate(req, { authenticateAll: true })
208
+ authenticate: jwtAuthenticate({ authenticateAll: true })
203
209
  })
204
210
  ```
205
211
  ## Environment Variables
206
- | Variable | Required | Description |
207
- |----------|----------|-------------|
208
- | `OAUTH_TRUSTED_ISSUERS` | Yes | Comma-separated list of trusted issuer URLs (e.g. `https://accounts.google.com,https://login.microsoftonline.com/{tenant}/v2.0`) |
209
- | `OAUTH_TRUSTED_AUDIENCES` | No | Comma-separated list of accepted `aud` values. See [Audience Validation](#audience-validation) for details. |
210
- | `OAUTH_TRUSTED_CLIENTIDS` | No | Comma-separated list of accepted `client_id` values. |
211
- | `OAUTH_ISSUER_INTERNAL_URLS` | No | Map external issuer URLs to internal URLs for docker-compose / split-horizon DNS scenarios where the browser reaches the provider on one hostname but the resource provider reaches it on another. Format: `external=internal` (e.g. `https://auth.example.com=http://keycloak:8080`). Rewrites server-to-server requests (discovery, JWKS, token exchange) but not browser redirects. |
212
+ At least one issuer must be configured. Use any combination of the env-var shortcuts below, or the JSON-based `JWT_TRUSTED_ISSUERS` for full control. Configuration from both sources is merged.
213
+
214
+ | Variable | Description |
215
+ |----------|-------------|
216
+ | `UA_URL` | URL of a TxState Unified Auth service. Creates a unified-auth issuer with `iss: 'unified-auth'`. |
217
+ | `UA_URL_INTERNAL` | Internal URL of the UA service for server-to-server requests in split-horizon DNS scenarios. |
218
+ | `OAUTH_URLS` | Comma-separated OAuth/OIDC issuer URLs (e.g. `https://accounts.google.com,https://login.microsoftonline.com/{tenant}/v2.0`). Each becomes an issuer with `iss` equal to the URL. |
219
+ | `OAUTH_INTERNAL_URLS` | Map external OAuth issuer URLs to internal URLs for docker-compose / split-horizon DNS. Format: `external=internal,external=internal` (e.g. `https://auth.example.com=http://keycloak:8080`). Rewrites server-to-server requests (discovery, JWKS, token exchange) but not browser redirects. |
220
+ | `JWT_SECRET` | Symmetric HMAC secret for verifying JWTs. Tokens must have `iss: 'jwt-secret'`. |
221
+ | `JWT_PUBLIC_KEY` | PEM-encoded asymmetric public key for verifying JWTs. Tokens must have `iss: 'jwt-public-key'` (use `JWT_TRUSTED_ISSUERS` instead for another name). Literal `\n` is converted to real newlines so PEMs survive env-var encoding. |
222
+ | `JWT_TRUSTED_AUDIENCES` | Comma-separated list of accepted `aud` values, unioned into every issuer's audience list. See [Audience Validation](#audience-validation). |
223
+ | `JWT_TRUSTED_CLIENTIDS` | Comma-separated list of accepted `client_id` values, unioned into every issuer's client-id list. |
224
+ | `JWT_TRUSTED_ISSUERS` | JSON array of issuer configs for advanced setups. See [Issuer JSON Config](#issuer-json-config). |
225
+
226
+ ### Issuer JSON Config
227
+ `JWT_TRUSTED_ISSUERS` accepts a JSON array of objects. Each object describes one issuer:
228
+
229
+ | Field | Required | Description |
230
+ |-------|----------|-------------|
231
+ | `iss` | Yes | The `iss` claim that tokens from this issuer must carry. |
232
+ | `type` | No | One of `oauth`, `jwks`, `unified-auth`, `publicKey`, `secret`. Inferred from other fields if omitted: `iss === 'unified-auth'` → unified-auth; `secret` → secret; `publicKey` → publicKey; `url` → jwks. Set explicitly to `oauth` to enable `.well-known` discovery. |
233
+ | `url` | Conditional | Required for `oauth`, `jwks`, and `unified-auth`. For `oauth` it's the issuer URL (discovery is performed relative to it); for `jwks` it's the JWKS endpoint directly. |
234
+ | `publicKey` | Conditional | PEM-encoded public key (`publicKey` type). |
235
+ | `secret` | Conditional | Symmetric HMAC secret (`secret` type). |
236
+ | `internalUrl` | No | Server-to-server URL prefix override for split-horizon DNS. |
237
+ | `audiences` | No | Array of accepted `aud` values for this issuer. Unioned with `JWT_TRUSTED_AUDIENCES`. |
238
+ | `clientIds` | No | Array of accepted `client_id` values for this issuer. Unioned with `JWT_TRUSTED_CLIENTIDS`. |
239
+ | `validateUrl` | No | (unified-auth only) Override URL for the deauth poll. Resolved relative to `url`. Defaults to `<url>/validateToken`. |
240
+ | `logoutUrl` | No | End-session URL surfaced as `req.auth.issuerConfig.logoutUrl`. For OAuth issuers this is auto-discovered; set only to override. Resolved relative to `url`. |
212
241
 
213
242
  ## Options
214
243
  | Option | Description |
@@ -216,18 +245,16 @@ const server = new Server({
216
245
  | `authenticateAll` | If true, all requests require authentication except routes in `exceptRoutes` or `optionalRoutes`. |
217
246
  | `exceptRoutes` | `Set<string>` of route URLs that skip authentication entirely and do not receive an auth object. |
218
247
  | `optionalRoutes` | `Set<string>` of route URLs that do not require authentication but populate `req.auth` if a session is available. |
219
- | `usingOAuthCookieRoutes` | Set to true if you are using `registerOAuthCookieRoutes` with `authenticateAll`. Automatically excludes cookie endpoints from authentication requirements. |
220
- | `extraClaims` | A function that receives the full JWT payload and returns extra properties to merge into the auth object (e.g. `payload => ({ roles: payload.roles })`). If you use this, you should also set `OAUTH_TRUSTED_AUDIENCES`. See [Audience Validation](#audience-validation). |
248
+ | `extraClaims` | A function that receives the full JWT payload and returns extra properties to merge into the auth object (e.g. `payload => ({ roles: payload.roles })`). If you use this, you should also set `JWT_TRUSTED_AUDIENCES` or per-issuer `audiences`. See [Audience Validation](#audience-validation). |
249
+
250
+ Calling `registerOAuthCookieRoutes` or `registerUaCookieRoutes` automatically excludes their callback/redirect routes from authentication and marks their logout routes as optional, so you do not need to list them here.
221
251
 
222
252
  ## Cookie Endpoints
223
253
  For server-rendered applications or SPAs that need cookie-based sessions, `registerOAuthCookieRoutes` implements the full OAuth authorization code flow with PKCE (S256), storing the ID token in an HttpOnly cookie. The access token and refresh token are stored in separate cookies (optionally encrypted via `OAUTH_COOKIE_SECRET`). Expired ID tokens are transparently refreshed using the refresh token cookie.
224
254
  ```javascript
225
- import Server, { oauthAuthenticate, registerOAuthCookieRoutes } from 'fastify-txstate'
255
+ import Server, { jwtAuthenticate, registerOAuthCookieRoutes } from 'fastify-txstate'
226
256
  const server = new Server({
227
- authenticate: req => oauthAuthenticate(req, {
228
- authenticateAll: true,
229
- usingOAuthCookieRoutes: true
230
- })
257
+ authenticate: jwtAuthenticate({ authenticateAll: true })
231
258
  })
232
259
  registerOAuthCookieRoutes(server.app)
233
260
  ```
@@ -239,7 +266,7 @@ The access token is available at `req.auth.accessToken` for making requests to t
239
266
  | `loginPage` | A function for rendering a login selection page when multiple issuers are configured. See [Multiple Issuers](#multiple-issuers). |
240
267
 
241
268
  ### Multiple Issuers
242
- When `OAUTH_TRUSTED_ISSUERS` contains multiple issuers, you can provide a `loginPage` function to let the user choose which provider to sign in with. The function receives an array of `{ issuerUrl, redirectHref }` and should return an HTML string.
269
+ When multiple OAuth issuers are configured (via `OAUTH_URLS` or `JWT_TRUSTED_ISSUERS`), you can provide a `loginPage` function to let the user choose which provider to sign in with. The function receives an array of `{ issuerUrl, redirectHref }` and should return an HTML string.
243
270
  ```javascript
244
271
  registerOAuthCookieRoutes(server.app, {
245
272
  loginPage: issuers => `<!DOCTYPE html>
@@ -254,8 +281,8 @@ When a user hits `/.oauthRedirect` without specifying an `issuer` query paramete
254
281
  ### Additional Environment Variables
255
282
  | Variable | Required | Description |
256
283
  |----------|----------|-------------|
257
- | `OAUTH_CLIENT_ID` | Yes | OAuth client ID for the authorization code flow. |
258
- | `OAUTH_CLIENT_SECRET` | No | OAuth client secret. PKCE secures the code exchange, but some providers require a secret even with PKCE. |
284
+ | `OAUTH_COOKIE_CLIENT_ID` | Yes | OAuth client ID for the authorization code flow. |
285
+ | `OAUTH_COOKIE_CLIENT_SECRET` | No | OAuth client secret. PKCE secures the code exchange, but some providers require a secret even with PKCE. |
259
286
  | `OAUTH_COOKIE_SECRET` | No | If set, the refresh token and access token cookies are encrypted with AES-256-GCM. If not, they are stored as plaintext (still HttpOnly and Secure). |
260
287
  | `OAUTH_COOKIE_NAME` | No | Name for the session cookie. Defaults to a random hex string. |
261
288
  | `PUBLIC_URL` | No | Base URL for the API, used to generate callback URIs (e.g. `https://myapp.example.com/api`). Derived from the request hostname if not set. |
@@ -266,6 +293,17 @@ When a user hits `/.oauthRedirect` without specifying an `issuer` query paramete
266
293
  - **`GET /.oauthCallback`** — Handles the provider's redirect. Exchanges the authorization code for tokens (using PKCE), sets the ID token, access token, and refresh token as cookies, and redirects to the original `requestedUrl`. If no ID token is returned, falls back to the access token if it is a JWT.
267
294
  - **`GET /.oauthLogout`** — Clears all OAuth cookies and redirects to the provider's `end_session_endpoint` if available, with the ID token as a hint for single sign-out.
268
295
 
296
+ ### Server-Rendered Login Redirects
297
+ For server-rendered routes where you want to redirect unauthenticated users straight into the login flow (rather than returning a 401 and letting client code react), call `requireCookieAuthOAuth(req, res)` at the top of your handler. If `req.auth` is empty it redirects the browser through `/.oauthRedirect` (preserving the current URL as `requestedUrl`) and returns `true`; otherwise it returns `false` and your handler proceeds.
298
+ ```javascript
299
+ import { requireCookieAuthOAuth } from 'fastify-txstate'
300
+ server.app.get('/dashboard', async (req, res) => {
301
+ if (await requireCookieAuthOAuth(req, res)) return
302
+ // ...render the page using req.auth
303
+ })
304
+ ```
305
+ If you are using `registerUaCookieRoutes` for TxState Unified Auth instead of OAuth, the equivalent helper is `requireCookieAuthUa(req, res)`. (The old name `requireCookieAuth` still works but is deprecated.)
306
+
269
307
  ## Client-Side Authentication (without cookie endpoints)
270
308
  If you are implementing the OAuth flow in your client application instead of using the cookie endpoints above, send the token to the API as an `Authorization: Bearer <token>` header. Here is what you need to know:
271
309
 
@@ -435,7 +473,7 @@ server.app.register(analyticsPlugin, {
435
473
  | `ELASTICSEARCH_USEREVENTS_INDEX` | No | Index name. Defaults to `interaction-analytics`. |
436
474
 
437
475
  ## Audience Validation
438
- Audience validation is a way to ensure that tokens you accept were generated with your API in mind. This helps when the token's claims include authorization like role memberships specific to your app. An attacker could register their own app with identical role names and use their token for your API, unless you specify your API as the only valid audience with OAUTH_TRUSTED_AUDIENCES.
476
+ Audience validation is a way to ensure that tokens you accept were generated with your API in mind. This helps when the token's claims include authorization like role memberships specific to your app. An attacker could register their own app with identical role names and use their token for your API, unless you specify your API as the only valid audience via `JWT_TRUSTED_AUDIENCES` (or per-issuer `audiences` in `JWT_TRUSTED_ISSUERS`).
439
477
 
440
478
  fastify-txstate is somewhat opinionated about storing authorization information in your authentication tokens. It's generally not a good idea - you'll end up with people staying in roles until their token expires, and be vulnerable to attacks like this. Let the authentication layer identify the user, and let your API match the user's identity with any authorization roles. To this end, `FastifyTxStateAuthInfo` doesn't have any spec for authorization-related claims.
441
479
 
package/lib/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './server.ts';
3
3
  export * from './analytics.ts';
4
4
  export * from './error.ts';
5
5
  export * from './filestorage.ts';
6
+ export * from './jwt-auth.ts';
6
7
  export * from './unified-auth.ts';
7
8
  export * from './oauth.ts';
8
9
  export * from './postformdata.ts';
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./server.js";
3
3
  export * from "./analytics.js";
4
4
  export * from "./error.js";
5
5
  export * from "./filestorage.js";
6
+ export * from "./jwt-auth.js";
6
7
  export * from "./unified-auth.js";
7
8
  export * from "./oauth.js";
8
9
  export * from "./postformdata.js";
@@ -0,0 +1,98 @@
1
+ import type { FastifyRequest } from 'fastify';
2
+ import { type JWTPayload } from 'jose';
3
+ import type { FastifyTxStateAuthInfo, IssuerConfig } from './server.ts';
4
+ export interface OAuthDiscovery {
5
+ issuer: string;
6
+ jwks_uri: string;
7
+ authorization_endpoint?: string;
8
+ token_endpoint?: string;
9
+ end_session_endpoint?: string;
10
+ }
11
+ declare module 'fastify' {
12
+ interface FastifyRequest {
13
+ pendingOAuthCookies?: string[];
14
+ }
15
+ }
16
+ export declare const oauthCookieName: string;
17
+ export declare const refreshCookieName: string;
18
+ export declare const accessTokenCookieName: string;
19
+ export declare const uaCookieName: string;
20
+ export declare function wrapRefreshToken(token: string): string;
21
+ export declare function unwrapRefreshToken(value: string): string | undefined;
22
+ type JwtIssuerType = 'oauth' | 'jwks' | 'unified-auth' | 'publicKey' | 'secret';
23
+ export interface JwtIssuerConfigRaw {
24
+ iss: string;
25
+ /** Explicitly set the issuer type. Optional — when omitted, the type is inferred:
26
+ * iss === 'unified-auth' → 'unified-auth'; secret → 'secret'; publicKey → 'publicKey';
27
+ * url → 'jwks'. Set type: 'oauth' to enable OAuth/OIDC auto-discovery via
28
+ * .well-known/openid-configuration on the issuer URL. */
29
+ type?: JwtIssuerType;
30
+ /** Issuer URL. For 'oauth' this is the issuer (.well-known/openid-configuration is
31
+ * fetched relative to it). For 'jwks' this is the JWKS endpoint directly. For
32
+ * 'unified-auth' this is the UA service URL. */
33
+ url?: string;
34
+ /** PEM-encoded public key (publicKey type). */
35
+ publicKey?: string;
36
+ /** Symmetric HMAC secret (secret type). */
37
+ secret?: string;
38
+ /** Override URL for the unified-auth /validateToken poll. Resolved relative to `url`. */
39
+ validateUrl?: string;
40
+ /** End-session URL surfaced as `req.auth.issuerConfig.logoutUrl`. For 'oauth' issuers
41
+ * this is auto-discovered; set this only to override. Resolved relative to `url`. */
42
+ logoutUrl?: string;
43
+ /** Server-to-server URL prefix override for split-horizon DNS (e.g. talking to the
44
+ * issuer over a docker-internal hostname while the browser uses the public URL). */
45
+ internalUrl?: string;
46
+ /** If set, only accept tokens whose `aud` claim contains one of these values. */
47
+ audiences?: string[];
48
+ /** If set, only accept tokens whose `client_id` claim matches one of these values. */
49
+ clientIds?: string[];
50
+ }
51
+ export declare function toInternalUrl(url: string): string;
52
+ export declare function getOAuthIssuerUrls(): string[];
53
+ export declare function getIssuerConfig(iss: string): IssuerConfig | undefined;
54
+ export declare function getOAuthDiscovery(issuerUrl: string): Promise<OAuthDiscovery | undefined>;
55
+ export declare function init(): void;
56
+ export interface JwtAuthenticateOptions {
57
+ /** If true, all requests require authentication, except routes listed in exceptRoutes or optionalRoutes. */
58
+ authenticateAll?: boolean;
59
+ /** Routes that skip authentication entirely. They will not receive an auth object. */
60
+ exceptRoutes?: Set<string>;
61
+ /** Routes that do not require authentication, but will fill req.auth if a session is available. */
62
+ optionalRoutes?: Set<string>;
63
+ /** Receives the full JWT payload and returns extra properties to merge into the auth object.
64
+ * If you use this, set per-issuer `audiences` to prevent tokens from other applications
65
+ * carrying unexpected authorization claims. */
66
+ extraClaims?: (payload: JWTPayload) => Record<string, unknown>;
67
+ }
68
+ export declare const registeredExceptRoutes: Set<string>;
69
+ export declare const registeredOptionalRoutes: Set<string>;
70
+ /**
71
+ * Build an `authenticate` function that validates JWTs from the Authorization Bearer
72
+ * header or a session cookie. Supports any mix of issuer types via the
73
+ * JWT_TRUSTED_ISSUERS env var:
74
+ *
75
+ * - 'oauth' — OAuth/OIDC provider with .well-known auto-discovery
76
+ * - 'jwks' — JWKS endpoint URL (no discovery)
77
+ * - 'unified-auth' — TxState Unified Auth (JWKS + /validateToken poll for deauth)
78
+ * - 'publicKey' — PEM-encoded asymmetric public key
79
+ * - 'secret' — symmetric HMAC secret
80
+ *
81
+ * Usage:
82
+ * new Server({ authenticate: jwtAuthenticate({ authenticateAll: true }) })
83
+ *
84
+ * Or with no options:
85
+ * new Server({ authenticate: jwtAuthenticate() })
86
+ *
87
+ * Calling `registerOAuthCookieRoutes` or `registerUaCookieRoutes` automatically excludes
88
+ * their callback/redirect routes from authentication requirements and marks their logout
89
+ * routes as optional, so you do not need to configure that here.
90
+ *
91
+ * If a refresh-token cookie is present (set by registerOAuthCookieRoutes) and the access
92
+ * token has expired, the returned authenticator transparently exchanges the refresh
93
+ * token for a new access token and queues the replacement cookies on
94
+ * `req.pendingOAuthCookies`. The onSend hook installed by registerOAuthCookieRoutes
95
+ * flushes those cookies onto the response.
96
+ */
97
+ export declare function jwtAuthenticate(options?: JwtAuthenticateOptions): (req: FastifyRequest) => Promise<FastifyTxStateAuthInfo | undefined>;
98
+ export {};