@growth-labs/auth 0.14.1 → 0.16.0

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
@@ -51,7 +51,8 @@ auth({
51
51
  3. Requires the access token `aud` to match `resource`; wrong or missing audiences remain unauthenticated
52
52
  4. If expired but refresh token valid → silent refresh, verify the new access token audience, then set new cookies
53
53
  — one exchange per token per isolate, and never on a beacon or a prefetch (those are answered as a guest / `425`, and the next real request refreshes)
54
- 5. Populates `context.locals.user` as `User | null`
54
+ — a refresh token the issuer refuses is remembered in `<prefix>_rtx`; the issuer is not asked again while it matches, and the token cookies are cleared only when a document navigation ≥60s later is refused a second time
55
+ 5. Populates `context.locals.user` as `User | null`, `context.locals.authSession` with how the session resolved, and — whenever the refresh path was entered — `context.locals.authRefreshFingerprint` with a SHA-256 prefix of the presented refresh token (never the token) for joining a site's session event to the issuer's `tokenFingerprint`
55
56
  6. If path matches `gatedPaths` and `user` is null → redirect to `loginPath`
56
57
 
57
58
  **Routes:**
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,sBAAsB,CAAA;AAmB7B,wBAAsB,SAAS,CAC9B,OAAO,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EACxE,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GAC3B,OAAO,CAAC,QAAQ,CAAC,CA0KnB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,sBAAsB,CAAA;AA4B7B,wBAAsB,SAAS,CAC9B,OAAO,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EACxE,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,GAC3B,OAAO,CAAC,QAAQ,CAAC,CA2OnB"}
@@ -1,10 +1,11 @@
1
1
  import { getConfig } from '../config-store.js';
2
2
  import '../runtime-bridge.js';
3
3
  import { resolveClientSecret } from '../utils/client-secret.js';
4
- import { getCookie, setTokenCookies } from '../utils/cookies.js';
4
+ import { clearTokenCookies, getCookie, setTokenCookies } from '../utils/cookies.js';
5
5
  import { secretResolutionFailureResponse } from '../utils/fail-closed.js';
6
6
  import { resolveInternalIssuer } from '../utils/issuer.js';
7
7
  import { classifyRefreshCarrier, prefetchDeferredResponse, } from '../utils/refresh-carrier.js';
8
+ import { clearRejectionMarker, fingerprintRefreshToken, mayConfirmRejection, parseRejectionMarker, rejectionMarkerCookieName, setRejectionMarker, } from '../utils/refresh-rejection.js';
8
9
  import { refreshTokens, verifyAccessToken } from '../utils/tokens.js';
9
10
  const PRIVATE_NO_STORE_HEADERS = [
10
11
  'Cache-Control',
@@ -66,18 +67,64 @@ export async function onRequest(context, next) {
66
67
  if (carrier !== 'consumable')
67
68
  deferredCarrier = carrier;
68
69
  }
69
- if (!user && refreshToken && deferredCarrier === null) {
70
- const clientId = options.clientId || context.url.hostname;
71
- refreshed = await refreshTokens(issuer, refreshToken, clientId, resolvedClientSecret, options.resource, (failure) => {
72
- refreshFailure = failure;
73
- });
74
- if (refreshed) {
75
- user = {
76
- id: refreshed.claims.sub,
77
- email: refreshed.claims.email,
78
- name: refreshed.claims.name ?? null,
79
- avatarUrl: refreshed.claims.image ?? null,
80
- };
70
+ // Confirm before clear (refresh-rejection.ts). A refresh token the issuer
71
+ // has refused once is remembered in a short marker cookie; while it matches,
72
+ // the issuer is not asked again, and only a document navigation past the
73
+ // confirm window may ask a second time and clear on a second refusal.
74
+ const documentNavigation = isDocumentNavigation(request);
75
+ const nowSeconds = Math.floor(Date.now() / 1000);
76
+ const cookieWrites = [];
77
+ let suppressed = false;
78
+ let confirmedDead = false;
79
+ // Name the presented refresh token, never carry it (0.16.0). Computed once
80
+ // the refresh path is entered — deferred, suppressed, refreshed, expired or
81
+ // failed — and exposed as `context.locals.authRefreshFingerprint` so a site
82
+ // can put the same value the issuer logs as `tokenFingerprint` on its own
83
+ // session event and join the two. After a successful refresh it still names
84
+ // the token that was PRESENTED, which is the one a later lost-rotation
85
+ // rejection at the issuer will name too.
86
+ let fingerprint;
87
+ if (!user && refreshToken) {
88
+ fingerprint = await fingerprintRefreshToken(refreshToken);
89
+ context.locals.authRefreshFingerprint = fingerprint;
90
+ }
91
+ if (!user && refreshToken && fingerprint && deferredCarrier === null) {
92
+ const marker = parseRejectionMarker(getCookie(request, rejectionMarkerCookieName(options)));
93
+ const marked = marker !== null && marker.fingerprint === fingerprint;
94
+ const confirming = marked && mayConfirmRejection(marker, nowSeconds, documentNavigation);
95
+ if (marked && !confirming) {
96
+ suppressed = true;
97
+ }
98
+ else {
99
+ const clientId = options.clientId || context.url.hostname;
100
+ refreshed = await refreshTokens(issuer, refreshToken, clientId, resolvedClientSecret, options.resource, (failure) => {
101
+ refreshFailure = failure;
102
+ });
103
+ if (refreshed) {
104
+ user = {
105
+ id: refreshed.claims.sub,
106
+ email: refreshed.claims.email,
107
+ name: refreshed.claims.name ?? null,
108
+ avatarUrl: refreshed.claims.image ?? null,
109
+ };
110
+ if (marker)
111
+ cookieWrites.push((response) => clearRejectionMarker(response, options));
112
+ }
113
+ else if (refreshFailure?.reason === 'invalid_grant') {
114
+ if (confirming) {
115
+ // Second refusal, a minute or more after the first, on a request
116
+ // the member is waiting for: the token is dead. Clear it.
117
+ confirmedDead = true;
118
+ cookieWrites.push((response) => {
119
+ clearTokenCookies(response, options);
120
+ clearRejectionMarker(response, options);
121
+ });
122
+ }
123
+ else {
124
+ const firstSeen = { fingerprint, rejectedAt: nowSeconds };
125
+ cookieWrites.push((response) => setRejectionMarker(response, options, firstSeen));
126
+ }
127
+ }
81
128
  }
82
129
  }
83
130
  // 3. Populate context.locals.user and say HOW the session resolved. A site
@@ -94,15 +141,16 @@ export async function onRequest(context, next) {
94
141
  hadRefreshToken: refreshToken !== null,
95
142
  refreshFailure,
96
143
  deferred: deferredCarrier !== null,
144
+ suppressed,
97
145
  });
98
146
  context.locals.authSession = session;
99
- // The cookies are deliberately left alone, even when the state is `expired`.
100
- // The issuer keeps refresh tokens in an eventually consistent store, so a
101
- // token only seconds old can read as missing from another location and be
102
- // answered invalid_grant; today that costs one anonymous page and heals on
103
- // the next navigation, and clearing would turn it into a forced sign-in.
104
- // Recovery never needed the clear: signing in again overwrites them.
105
- const documentNavigation = isDocumentNavigation(request);
147
+ // A first refusal clears nothing: the issuer keeps refresh tokens in an
148
+ // eventually consistent store, so a token only seconds old can read as
149
+ // missing from another location and be answered invalid_grant, and a
150
+ // straggling subrequest may still carry the token a fresh sign-in replaced.
151
+ // Only the confirmed second refusal above clears, and the response that
152
+ // clears still reports `expired`, so a site's sign-in-again notice fires
153
+ // once; every request after it is anonymous.
106
154
  if (deferredCarrier !== null) {
107
155
  try {
108
156
  console.info('[auth] session refresh deferred', {
@@ -118,6 +166,18 @@ export async function onRequest(context, next) {
118
166
  if (deferredCarrier === 'prefetch')
119
167
  return prefetchDeferredResponse();
120
168
  }
169
+ else if (suppressed) {
170
+ try {
171
+ console.info('[auth] session refresh suppressed', {
172
+ pathname: context.url.pathname,
173
+ documentNavigation,
174
+ session,
175
+ });
176
+ }
177
+ catch {
178
+ /* a patched console must not turn a guest page into a 500 */
179
+ }
180
+ }
121
181
  else if (!user && refreshToken) {
122
182
  const failure = refreshFailure ?? { reason: 'unreported' };
123
183
  try {
@@ -126,6 +186,7 @@ export async function onRequest(context, next) {
126
186
  pathname: context.url.pathname,
127
187
  documentNavigation,
128
188
  session,
189
+ ...(confirmedDead ? { cleared: true } : {}),
129
190
  });
130
191
  }
131
192
  catch {
@@ -138,14 +199,25 @@ export async function onRequest(context, next) {
138
199
  // one the login renderer already knows how to explain. A rejected access
139
200
  // token with no refresh token beside it (wrong audience, say) is not a
140
201
  // session that ended, and its redirect stays exactly as it always was.
141
- const sessionEnded = refreshFailure?.reason === 'invalid_grant';
202
+ const sessionEnded = refreshFailure?.reason === 'invalid_grant' || suppressed;
142
203
  const loginUrl = new URL(`${options.loginPath}?redirect=${encodeURIComponent(context.url.pathname)}${sessionEnded ? '&error=session_expired' : ''}`, context.url.origin);
143
- return Response.redirect(loginUrl.toString(), 302);
204
+ if (cookieWrites.length === 0)
205
+ return Response.redirect(loginUrl.toString(), 302);
206
+ // Response.redirect's headers are immutable; the marker or the clear has
207
+ // to ride on this redirect, so build the same 302 by hand.
208
+ const redirect = new Response(null, {
209
+ status: 302,
210
+ headers: { Location: loginUrl.toString() },
211
+ });
212
+ for (const write of cookieWrites)
213
+ write(redirect);
214
+ return redirect;
144
215
  }
145
216
  // 5. Continue to the page
146
217
  const response = await next();
147
- // 6. If tokens were refreshed or the response must carry no-store, clone and apply.
148
- if (refreshed || context.locals.synthetic === true) {
218
+ // 6. If tokens were refreshed, a rejection marker moved, or the response must
219
+ // carry no-store, clone and apply.
220
+ if (refreshed || cookieWrites.length > 0 || context.locals.synthetic === true) {
149
221
  const modifiedResponse = new Response(response.body, {
150
222
  status: response.status,
151
223
  statusText: response.statusText,
@@ -154,6 +226,8 @@ export async function onRequest(context, next) {
154
226
  if (refreshed) {
155
227
  setTokenCookies(modifiedResponse, options, refreshed.accessToken, refreshed.refreshToken);
156
228
  }
229
+ for (const write of cookieWrites)
230
+ write(modifiedResponse);
157
231
  if (context.locals.synthetic === true) {
158
232
  for (const header of PRIVATE_NO_STORE_HEADERS) {
159
233
  modifiedResponse.headers.set(header, 'private, no-store');
@@ -173,6 +247,9 @@ function resolveSessionState(input) {
173
247
  // A rejected access token with no refresh token beside it cannot be revived.
174
248
  if (!input.hadRefreshToken)
175
249
  return 'expired';
250
+ // A refusal remembered from a moment ago is still a refusal.
251
+ if (input.suppressed)
252
+ return 'expired';
176
253
  return input.refreshFailure?.reason === 'invalid_grant' ? 'expired' : 'refresh_failed';
177
254
  }
178
255
  /** Sec-Fetch-* is authoritative where the browser sends it; Accept is the fallback. */
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -140,10 +140,12 @@ export interface TokenExchangeResult {
140
140
  * may pass; `deferred` holds a refresh token this request deliberately did not
141
141
  * spend, because it is a beacon or a prefetch whose response the browser would
142
142
  * not apply (0.14.0) — the next consumable request refreshes, so treat it as a
143
- * guest request and nothing more. NONE is proof the session is dead (the
144
- * issuer's token store is eventually consistent), so never clear cookies on
145
- * them: signing in again overwrites the cookies, and the middleware itself
146
- * clears nothing.
143
+ * guest request and nothing more. NONE is proof the session is dead on its own
144
+ * (the issuer's token store is eventually consistent), so a site never clears
145
+ * cookies on them. The middleware clears the token cookies only after a second
146
+ * `invalid_grant` on a document navigation a minute or more after the first
147
+ * (0.15.0, `<prefix>_rtx` marker); that response still reports `expired`, and
148
+ * every request after it is `anonymous`.
147
149
  */
148
150
  export type AuthSessionState = 'anonymous' | 'verified' | 'refreshed' | 'expired' | 'refresh_failed' | 'deferred';
149
151
  declare global {
@@ -151,6 +153,18 @@ declare global {
151
153
  interface Locals {
152
154
  user: User | null;
153
155
  authSession?: AuthSessionState;
156
+ /**
157
+ * 16-hex SHA-256 prefix of the refresh token this request presented
158
+ * (0.16.0). Set only when the refresh path was entered — `authSession`
159
+ * is `refreshed`, `expired`, `refresh_failed` or `deferred` — and never
160
+ * when the access token verified or no refresh token was present. It
161
+ * is the value the issuer logs as `tokenFingerprint` on
162
+ * `oauth.token_refresh.rejected` and the value carried in the
163
+ * `{prefix}_rtx` marker: credential-linked, never person-linked, and
164
+ * the same token after a successful rotation. A site may write it on
165
+ * a session-outcome analytics event; it must never write the token.
166
+ */
167
+ authRefreshFingerprint?: string;
154
168
  synthetic?: boolean;
155
169
  }
156
170
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { ResolvedAuthOptions } from '../options.js';
2
+ /** Seconds between the first rejection and the one that may clear. Past KV's convergence. */
3
+ export declare const REFRESH_REJECTION_CONFIRM_AFTER_SECONDS = 60;
4
+ /** How long a rejection is remembered. Long enough for a second navigation, short enough to forget. */
5
+ export declare const REFRESH_REJECTION_MARKER_MAX_AGE_SECONDS = 900;
6
+ export interface RefreshRejectionMarker {
7
+ fingerprint: string;
8
+ /** Unix seconds when the first rejection was seen. */
9
+ rejectedAt: number;
10
+ }
11
+ export declare function rejectionMarkerCookieName(options: Pick<ResolvedAuthOptions, 'cookiePrefix'>): string;
12
+ /** SHA-256 hex prefix of the refresh token: names it without carrying it. */
13
+ export declare function fingerprintRefreshToken(refreshToken: string): Promise<string>;
14
+ export declare function parseRejectionMarker(value: string | null): RefreshRejectionMarker | null;
15
+ export declare function formatRejectionMarker(marker: RefreshRejectionMarker): string;
16
+ /**
17
+ * Whether this request may ask the issuer a second time and clear on refusal:
18
+ * a document navigation, carrying the marked token, past the confirm window.
19
+ */
20
+ export declare function mayConfirmRejection(marker: RefreshRejectionMarker, nowSeconds: number, documentNavigation: boolean): boolean;
21
+ export declare function setRejectionMarker(response: Response, options: ResolvedAuthOptions, marker: RefreshRejectionMarker): void;
22
+ export declare function clearRejectionMarker(response: Response, options: ResolvedAuthOptions): void;
23
+ //# sourceMappingURL=refresh-rejection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-rejection.d.ts","sourceRoot":"","sources":["../../src/utils/refresh-rejection.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAGxD,6FAA6F;AAC7F,eAAO,MAAM,uCAAuC,KAAK,CAAA;AACzD,uGAAuG;AACvG,eAAO,MAAM,wCAAwC,MAAM,CAAA;AAK3D,MAAM,WAAW,sBAAsB;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,GAChD,MAAM,CAER;AAED,6EAA6E;AAC7E,wBAAsB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMnF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,sBAAsB,GAAG,IAAI,CAOxF;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAE5E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,sBAAsB,EAC9B,UAAU,EAAE,MAAM,EAClB,kBAAkB,EAAE,OAAO,GACzB,OAAO,CAIT;AAYD,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,sBAAsB,GAC5B,IAAI,CAKN;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAK3F"}
@@ -0,0 +1,89 @@
1
+ // Confirm before clear.
2
+ //
3
+ // 0.13.0 chose never to clear a cookie on invalid_grant, for two recorded
4
+ // reasons (packages #626): the issuer keeps refresh tokens in Workers KV, so a
5
+ // token seconds old can read as missing from another colo and be refused once;
6
+ // and a subrequest still carrying the OLD refresh token can arrive after the
7
+ // member has just signed in again, and a clear on its response would wipe the
8
+ // new session. Both are real. Neither needs a dead token re-presented on every
9
+ // request for hours, which is what 407 issuer rejections from 29 tokens in a
10
+ // day were (2026-09-20).
11
+ //
12
+ // So the middleware keeps a short marker of the rejection instead:
13
+ //
14
+ // 1. The first invalid_grant for a refresh token sets `<prefix>_rtx` holding a
15
+ // SHA-256 prefix of that token and the time. Nothing else changes: the
16
+ // session is `expired`, the notice a site shows still fires, no cookie is
17
+ // cleared.
18
+ // 2. While the marker matches the presented token, requests do not ask the
19
+ // issuer again; they report `expired` at once. That ends the amplification.
20
+ // 3. A document navigation at least CONFIRM_AFTER seconds later asks once
21
+ // more. A second invalid_grant clears the token cookies and the marker; a
22
+ // success clears the marker and carries on.
23
+ //
24
+ // A straggler can never clear: it is a subrequest, and its marker names the old
25
+ // token, not the new one. A consistency miss can never clear: the second ask
26
+ // happens after KV has converged, succeeds, and drops the marker. The marker
27
+ // carries a digest, never a token, and its only power is to hurry the clearing
28
+ // of the cookies it sits beside.
29
+ import { setCookie } from './cookies.js';
30
+ /** Seconds between the first rejection and the one that may clear. Past KV's convergence. */
31
+ export const REFRESH_REJECTION_CONFIRM_AFTER_SECONDS = 60;
32
+ /** How long a rejection is remembered. Long enough for a second navigation, short enough to forget. */
33
+ export const REFRESH_REJECTION_MARKER_MAX_AGE_SECONDS = 900;
34
+ const FINGERPRINT_HEX_LENGTH = 16;
35
+ const MARKER_PATTERN = /^([0-9a-f]{16})\.(\d{1,12})$/;
36
+ export function rejectionMarkerCookieName(options) {
37
+ return `${options.cookiePrefix}_rtx`;
38
+ }
39
+ /** SHA-256 hex prefix of the refresh token: names it without carrying it. */
40
+ export async function fingerprintRefreshToken(refreshToken) {
41
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(refreshToken));
42
+ return [...new Uint8Array(digest)]
43
+ .map((byte) => byte.toString(16).padStart(2, '0'))
44
+ .join('')
45
+ .slice(0, FINGERPRINT_HEX_LENGTH);
46
+ }
47
+ export function parseRejectionMarker(value) {
48
+ if (!value)
49
+ return null;
50
+ const match = MARKER_PATTERN.exec(value);
51
+ if (!match)
52
+ return null;
53
+ const rejectedAt = Number(match[2]);
54
+ if (!Number.isSafeInteger(rejectedAt))
55
+ return null;
56
+ return { fingerprint: match[1], rejectedAt };
57
+ }
58
+ export function formatRejectionMarker(marker) {
59
+ return `${marker.fingerprint}.${marker.rejectedAt}`;
60
+ }
61
+ /**
62
+ * Whether this request may ask the issuer a second time and clear on refusal:
63
+ * a document navigation, carrying the marked token, past the confirm window.
64
+ */
65
+ export function mayConfirmRejection(marker, nowSeconds, documentNavigation) {
66
+ return (documentNavigation && nowSeconds - marker.rejectedAt >= REFRESH_REJECTION_CONFIRM_AFTER_SECONDS);
67
+ }
68
+ function markerAttributes(options) {
69
+ return {
70
+ path: '/',
71
+ httpOnly: true,
72
+ secure: true,
73
+ sameSite: 'lax',
74
+ ...(options.sessionCookieDomain && { domain: options.sessionCookieDomain }),
75
+ };
76
+ }
77
+ export function setRejectionMarker(response, options, marker) {
78
+ setCookie(response, rejectionMarkerCookieName(options), formatRejectionMarker(marker), {
79
+ ...markerAttributes(options),
80
+ maxAge: REFRESH_REJECTION_MARKER_MAX_AGE_SECONDS,
81
+ });
82
+ }
83
+ export function clearRejectionMarker(response, options) {
84
+ setCookie(response, rejectionMarkerCookieName(options), '', {
85
+ ...markerAttributes(options),
86
+ maxAge: 0,
87
+ });
88
+ }
89
+ //# sourceMappingURL=refresh-rejection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-rejection.js","sourceRoot":"","sources":["../../src/utils/refresh-rejection.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/auth",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -2,7 +2,7 @@ import { getConfig } from '../config-store.js'
2
2
  import '../runtime-bridge.js'
3
3
  import type { AuthSessionState, User } from '../types.js'
4
4
  import { resolveClientSecret } from '../utils/client-secret.js'
5
- import { getCookie, setTokenCookies } from '../utils/cookies.js'
5
+ import { clearTokenCookies, getCookie, setTokenCookies } from '../utils/cookies.js'
6
6
  import { secretResolutionFailureResponse } from '../utils/fail-closed.js'
7
7
  import { resolveInternalIssuer } from '../utils/issuer.js'
8
8
  import {
@@ -10,6 +10,15 @@ import {
10
10
  prefetchDeferredResponse,
11
11
  type RefreshCarrier,
12
12
  } from '../utils/refresh-carrier.js'
13
+ import {
14
+ clearRejectionMarker,
15
+ fingerprintRefreshToken,
16
+ mayConfirmRejection,
17
+ parseRejectionMarker,
18
+ type RefreshRejectionMarker,
19
+ rejectionMarkerCookieName,
20
+ setRejectionMarker,
21
+ } from '../utils/refresh-rejection.js'
13
22
  import { type RefreshFailure, refreshTokens, verifyAccessToken } from '../utils/tokens.js'
14
23
 
15
24
  const PRIVATE_NO_STORE_HEADERS = [
@@ -80,24 +89,68 @@ export async function onRequest(
80
89
  const carrier = classifyRefreshCarrier(request)
81
90
  if (carrier !== 'consumable') deferredCarrier = carrier
82
91
  }
83
- if (!user && refreshToken && deferredCarrier === null) {
84
- const clientId = options.clientId || context.url.hostname
85
- refreshed = await refreshTokens(
86
- issuer,
87
- refreshToken,
88
- clientId,
89
- resolvedClientSecret,
90
- options.resource,
91
- (failure) => {
92
- refreshFailure = failure
93
- },
94
- )
95
- if (refreshed) {
96
- user = {
97
- id: refreshed.claims.sub,
98
- email: refreshed.claims.email,
99
- name: refreshed.claims.name ?? null,
100
- avatarUrl: refreshed.claims.image ?? null,
92
+
93
+ // Confirm before clear (refresh-rejection.ts). A refresh token the issuer
94
+ // has refused once is remembered in a short marker cookie; while it matches,
95
+ // the issuer is not asked again, and only a document navigation past the
96
+ // confirm window may ask a second time and clear on a second refusal.
97
+ const documentNavigation = isDocumentNavigation(request)
98
+ const nowSeconds = Math.floor(Date.now() / 1000)
99
+ const cookieWrites: Array<(response: Response) => void> = []
100
+ let suppressed = false
101
+ let confirmedDead = false
102
+ // Name the presented refresh token, never carry it (0.16.0). Computed once
103
+ // the refresh path is entered — deferred, suppressed, refreshed, expired or
104
+ // failed — and exposed as `context.locals.authRefreshFingerprint` so a site
105
+ // can put the same value the issuer logs as `tokenFingerprint` on its own
106
+ // session event and join the two. After a successful refresh it still names
107
+ // the token that was PRESENTED, which is the one a later lost-rotation
108
+ // rejection at the issuer will name too.
109
+ let fingerprint: string | undefined
110
+ if (!user && refreshToken) {
111
+ fingerprint = await fingerprintRefreshToken(refreshToken)
112
+ context.locals.authRefreshFingerprint = fingerprint
113
+ }
114
+ if (!user && refreshToken && fingerprint && deferredCarrier === null) {
115
+ const marker = parseRejectionMarker(getCookie(request, rejectionMarkerCookieName(options)))
116
+ const marked = marker !== null && marker.fingerprint === fingerprint
117
+ const confirming = marked && mayConfirmRejection(marker, nowSeconds, documentNavigation)
118
+
119
+ if (marked && !confirming) {
120
+ suppressed = true
121
+ } else {
122
+ const clientId = options.clientId || context.url.hostname
123
+ refreshed = await refreshTokens(
124
+ issuer,
125
+ refreshToken,
126
+ clientId,
127
+ resolvedClientSecret,
128
+ options.resource,
129
+ (failure) => {
130
+ refreshFailure = failure
131
+ },
132
+ )
133
+ if (refreshed) {
134
+ user = {
135
+ id: refreshed.claims.sub,
136
+ email: refreshed.claims.email,
137
+ name: refreshed.claims.name ?? null,
138
+ avatarUrl: refreshed.claims.image ?? null,
139
+ }
140
+ if (marker) cookieWrites.push((response) => clearRejectionMarker(response, options))
141
+ } else if (refreshFailure?.reason === 'invalid_grant') {
142
+ if (confirming) {
143
+ // Second refusal, a minute or more after the first, on a request
144
+ // the member is waiting for: the token is dead. Clear it.
145
+ confirmedDead = true
146
+ cookieWrites.push((response) => {
147
+ clearTokenCookies(response, options)
148
+ clearRejectionMarker(response, options)
149
+ })
150
+ } else {
151
+ const firstSeen: RefreshRejectionMarker = { fingerprint, rejectedAt: nowSeconds }
152
+ cookieWrites.push((response) => setRejectionMarker(response, options, firstSeen))
153
+ }
101
154
  }
102
155
  }
103
156
  }
@@ -116,17 +169,17 @@ export async function onRequest(
116
169
  hadRefreshToken: refreshToken !== null,
117
170
  refreshFailure,
118
171
  deferred: deferredCarrier !== null,
172
+ suppressed,
119
173
  })
120
174
  context.locals.authSession = session
121
175
 
122
- // The cookies are deliberately left alone, even when the state is `expired`.
123
- // The issuer keeps refresh tokens in an eventually consistent store, so a
124
- // token only seconds old can read as missing from another location and be
125
- // answered invalid_grant; today that costs one anonymous page and heals on
126
- // the next navigation, and clearing would turn it into a forced sign-in.
127
- // Recovery never needed the clear: signing in again overwrites them.
128
- const documentNavigation = isDocumentNavigation(request)
129
-
176
+ // A first refusal clears nothing: the issuer keeps refresh tokens in an
177
+ // eventually consistent store, so a token only seconds old can read as
178
+ // missing from another location and be answered invalid_grant, and a
179
+ // straggling subrequest may still carry the token a fresh sign-in replaced.
180
+ // Only the confirmed second refusal above clears, and the response that
181
+ // clears still reports `expired`, so a site's sign-in-again notice fires
182
+ // once; every request after it is anonymous.
130
183
  if (deferredCarrier !== null) {
131
184
  try {
132
185
  console.info('[auth] session refresh deferred', {
@@ -139,6 +192,16 @@ export async function onRequest(
139
192
  // A prefetched document rendered as a guest would be shown to the member
140
193
  // on navigation as if they had signed out; a non-2xx is dropped instead.
141
194
  if (deferredCarrier === 'prefetch') return prefetchDeferredResponse()
195
+ } else if (suppressed) {
196
+ try {
197
+ console.info('[auth] session refresh suppressed', {
198
+ pathname: context.url.pathname,
199
+ documentNavigation,
200
+ session,
201
+ })
202
+ } catch {
203
+ /* a patched console must not turn a guest page into a 500 */
204
+ }
142
205
  } else if (!user && refreshToken) {
143
206
  const failure: RefreshFailure = refreshFailure ?? { reason: 'unreported' }
144
207
  try {
@@ -147,6 +210,7 @@ export async function onRequest(
147
210
  pathname: context.url.pathname,
148
211
  documentNavigation,
149
212
  session,
213
+ ...(confirmedDead ? { cleared: true } : {}),
150
214
  })
151
215
  } catch {
152
216
  /* a patched console must not turn a guest page into a 500 */
@@ -159,21 +223,30 @@ export async function onRequest(
159
223
  // one the login renderer already knows how to explain. A rejected access
160
224
  // token with no refresh token beside it (wrong audience, say) is not a
161
225
  // session that ended, and its redirect stays exactly as it always was.
162
- const sessionEnded = refreshFailure?.reason === 'invalid_grant'
226
+ const sessionEnded = refreshFailure?.reason === 'invalid_grant' || suppressed
163
227
  const loginUrl = new URL(
164
228
  `${options.loginPath}?redirect=${encodeURIComponent(context.url.pathname)}${
165
229
  sessionEnded ? '&error=session_expired' : ''
166
230
  }`,
167
231
  context.url.origin,
168
232
  )
169
- return Response.redirect(loginUrl.toString(), 302)
233
+ if (cookieWrites.length === 0) return Response.redirect(loginUrl.toString(), 302)
234
+ // Response.redirect's headers are immutable; the marker or the clear has
235
+ // to ride on this redirect, so build the same 302 by hand.
236
+ const redirect = new Response(null, {
237
+ status: 302,
238
+ headers: { Location: loginUrl.toString() },
239
+ })
240
+ for (const write of cookieWrites) write(redirect)
241
+ return redirect
170
242
  }
171
243
 
172
244
  // 5. Continue to the page
173
245
  const response = await next()
174
246
 
175
- // 6. If tokens were refreshed or the response must carry no-store, clone and apply.
176
- if (refreshed || context.locals.synthetic === true) {
247
+ // 6. If tokens were refreshed, a rejection marker moved, or the response must
248
+ // carry no-store, clone and apply.
249
+ if (refreshed || cookieWrites.length > 0 || context.locals.synthetic === true) {
177
250
  const modifiedResponse = new Response(response.body, {
178
251
  status: response.status,
179
252
  statusText: response.statusText,
@@ -182,6 +255,7 @@ export async function onRequest(
182
255
  if (refreshed) {
183
256
  setTokenCookies(modifiedResponse, options, refreshed.accessToken, refreshed.refreshToken)
184
257
  }
258
+ for (const write of cookieWrites) write(modifiedResponse)
185
259
  if (context.locals.synthetic === true) {
186
260
  for (const header of PRIVATE_NO_STORE_HEADERS) {
187
261
  modifiedResponse.headers.set(header, 'private, no-store')
@@ -200,12 +274,15 @@ function resolveSessionState(input: {
200
274
  hadRefreshToken: boolean
201
275
  refreshFailure: RefreshFailure | null
202
276
  deferred: boolean
277
+ suppressed: boolean
203
278
  }): AuthSessionState {
204
279
  if (input.user) return input.refreshed ? 'refreshed' : 'verified'
205
280
  if (!input.hadAccessToken && !input.hadRefreshToken) return 'anonymous'
206
281
  if (input.deferred) return 'deferred'
207
282
  // A rejected access token with no refresh token beside it cannot be revived.
208
283
  if (!input.hadRefreshToken) return 'expired'
284
+ // A refusal remembered from a moment ago is still a refusal.
285
+ if (input.suppressed) return 'expired'
209
286
  return input.refreshFailure?.reason === 'invalid_grant' ? 'expired' : 'refresh_failed'
210
287
  }
211
288
 
package/src/types.ts CHANGED
@@ -158,10 +158,12 @@ export interface TokenExchangeResult {
158
158
  * may pass; `deferred` holds a refresh token this request deliberately did not
159
159
  * spend, because it is a beacon or a prefetch whose response the browser would
160
160
  * not apply (0.14.0) — the next consumable request refreshes, so treat it as a
161
- * guest request and nothing more. NONE is proof the session is dead (the
162
- * issuer's token store is eventually consistent), so never clear cookies on
163
- * them: signing in again overwrites the cookies, and the middleware itself
164
- * clears nothing.
161
+ * guest request and nothing more. NONE is proof the session is dead on its own
162
+ * (the issuer's token store is eventually consistent), so a site never clears
163
+ * cookies on them. The middleware clears the token cookies only after a second
164
+ * `invalid_grant` on a document navigation a minute or more after the first
165
+ * (0.15.0, `<prefix>_rtx` marker); that response still reports `expired`, and
166
+ * every request after it is `anonymous`.
165
167
  */
166
168
  export type AuthSessionState =
167
169
  | 'anonymous'
@@ -177,6 +179,18 @@ declare global {
177
179
  interface Locals {
178
180
  user: User | null
179
181
  authSession?: AuthSessionState
182
+ /**
183
+ * 16-hex SHA-256 prefix of the refresh token this request presented
184
+ * (0.16.0). Set only when the refresh path was entered — `authSession`
185
+ * is `refreshed`, `expired`, `refresh_failed` or `deferred` — and never
186
+ * when the access token verified or no refresh token was present. It
187
+ * is the value the issuer logs as `tokenFingerprint` on
188
+ * `oauth.token_refresh.rejected` and the value carried in the
189
+ * `{prefix}_rtx` marker: credential-linked, never person-linked, and
190
+ * the same token after a successful rotation. A site may write it on
191
+ * a session-outcome analytics event; it must never write the token.
192
+ */
193
+ authRefreshFingerprint?: string
180
194
  synthetic?: boolean
181
195
  }
182
196
  }
@@ -0,0 +1,115 @@
1
+ // Confirm before clear.
2
+ //
3
+ // 0.13.0 chose never to clear a cookie on invalid_grant, for two recorded
4
+ // reasons (packages #626): the issuer keeps refresh tokens in Workers KV, so a
5
+ // token seconds old can read as missing from another colo and be refused once;
6
+ // and a subrequest still carrying the OLD refresh token can arrive after the
7
+ // member has just signed in again, and a clear on its response would wipe the
8
+ // new session. Both are real. Neither needs a dead token re-presented on every
9
+ // request for hours, which is what 407 issuer rejections from 29 tokens in a
10
+ // day were (2026-09-20).
11
+ //
12
+ // So the middleware keeps a short marker of the rejection instead:
13
+ //
14
+ // 1. The first invalid_grant for a refresh token sets `<prefix>_rtx` holding a
15
+ // SHA-256 prefix of that token and the time. Nothing else changes: the
16
+ // session is `expired`, the notice a site shows still fires, no cookie is
17
+ // cleared.
18
+ // 2. While the marker matches the presented token, requests do not ask the
19
+ // issuer again; they report `expired` at once. That ends the amplification.
20
+ // 3. A document navigation at least CONFIRM_AFTER seconds later asks once
21
+ // more. A second invalid_grant clears the token cookies and the marker; a
22
+ // success clears the marker and carries on.
23
+ //
24
+ // A straggler can never clear: it is a subrequest, and its marker names the old
25
+ // token, not the new one. A consistency miss can never clear: the second ask
26
+ // happens after KV has converged, succeeds, and drops the marker. The marker
27
+ // carries a digest, never a token, and its only power is to hurry the clearing
28
+ // of the cookies it sits beside.
29
+
30
+ import type { ResolvedAuthOptions } from '../options.js'
31
+ import { type CookieAttributes, setCookie } from './cookies.js'
32
+
33
+ /** Seconds between the first rejection and the one that may clear. Past KV's convergence. */
34
+ export const REFRESH_REJECTION_CONFIRM_AFTER_SECONDS = 60
35
+ /** How long a rejection is remembered. Long enough for a second navigation, short enough to forget. */
36
+ export const REFRESH_REJECTION_MARKER_MAX_AGE_SECONDS = 900
37
+
38
+ const FINGERPRINT_HEX_LENGTH = 16
39
+ const MARKER_PATTERN = /^([0-9a-f]{16})\.(\d{1,12})$/
40
+
41
+ export interface RefreshRejectionMarker {
42
+ fingerprint: string
43
+ /** Unix seconds when the first rejection was seen. */
44
+ rejectedAt: number
45
+ }
46
+
47
+ export function rejectionMarkerCookieName(
48
+ options: Pick<ResolvedAuthOptions, 'cookiePrefix'>,
49
+ ): string {
50
+ return `${options.cookiePrefix}_rtx`
51
+ }
52
+
53
+ /** SHA-256 hex prefix of the refresh token: names it without carrying it. */
54
+ export async function fingerprintRefreshToken(refreshToken: string): Promise<string> {
55
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(refreshToken))
56
+ return [...new Uint8Array(digest)]
57
+ .map((byte) => byte.toString(16).padStart(2, '0'))
58
+ .join('')
59
+ .slice(0, FINGERPRINT_HEX_LENGTH)
60
+ }
61
+
62
+ export function parseRejectionMarker(value: string | null): RefreshRejectionMarker | null {
63
+ if (!value) return null
64
+ const match = MARKER_PATTERN.exec(value)
65
+ if (!match) return null
66
+ const rejectedAt = Number(match[2])
67
+ if (!Number.isSafeInteger(rejectedAt)) return null
68
+ return { fingerprint: match[1], rejectedAt }
69
+ }
70
+
71
+ export function formatRejectionMarker(marker: RefreshRejectionMarker): string {
72
+ return `${marker.fingerprint}.${marker.rejectedAt}`
73
+ }
74
+
75
+ /**
76
+ * Whether this request may ask the issuer a second time and clear on refusal:
77
+ * a document navigation, carrying the marked token, past the confirm window.
78
+ */
79
+ export function mayConfirmRejection(
80
+ marker: RefreshRejectionMarker,
81
+ nowSeconds: number,
82
+ documentNavigation: boolean,
83
+ ): boolean {
84
+ return (
85
+ documentNavigation && nowSeconds - marker.rejectedAt >= REFRESH_REJECTION_CONFIRM_AFTER_SECONDS
86
+ )
87
+ }
88
+
89
+ function markerAttributes(options: ResolvedAuthOptions): CookieAttributes {
90
+ return {
91
+ path: '/',
92
+ httpOnly: true,
93
+ secure: true,
94
+ sameSite: 'lax',
95
+ ...(options.sessionCookieDomain && { domain: options.sessionCookieDomain }),
96
+ }
97
+ }
98
+
99
+ export function setRejectionMarker(
100
+ response: Response,
101
+ options: ResolvedAuthOptions,
102
+ marker: RefreshRejectionMarker,
103
+ ): void {
104
+ setCookie(response, rejectionMarkerCookieName(options), formatRejectionMarker(marker), {
105
+ ...markerAttributes(options),
106
+ maxAge: REFRESH_REJECTION_MARKER_MAX_AGE_SECONDS,
107
+ })
108
+ }
109
+
110
+ export function clearRejectionMarker(response: Response, options: ResolvedAuthOptions): void {
111
+ setCookie(response, rejectionMarkerCookieName(options), '', {
112
+ ...markerAttributes(options),
113
+ maxAge: 0,
114
+ })
115
+ }