@growth-labs/auth 0.14.1 → 0.15.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,6 +51,7 @@ 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
+ — 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
54
55
  5. Populates `context.locals.user` as `User | null`
55
56
  6. If path matches `gatedPaths` and `user` is null → redirect to `loginPath`
56
57
 
@@ -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,CAgOnB"}
@@ -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,53 @@ export async function onRequest(context, next) {
66
67
  if (carrier !== 'consumable')
67
68
  deferredCarrier = carrier;
68
69
  }
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;
69
79
  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
- };
80
+ const fingerprint = await fingerprintRefreshToken(refreshToken);
81
+ const marker = parseRejectionMarker(getCookie(request, rejectionMarkerCookieName(options)));
82
+ const marked = marker !== null && marker.fingerprint === fingerprint;
83
+ const confirming = marked && mayConfirmRejection(marker, nowSeconds, documentNavigation);
84
+ if (marked && !confirming) {
85
+ suppressed = true;
86
+ }
87
+ else {
88
+ const clientId = options.clientId || context.url.hostname;
89
+ refreshed = await refreshTokens(issuer, refreshToken, clientId, resolvedClientSecret, options.resource, (failure) => {
90
+ refreshFailure = failure;
91
+ });
92
+ if (refreshed) {
93
+ user = {
94
+ id: refreshed.claims.sub,
95
+ email: refreshed.claims.email,
96
+ name: refreshed.claims.name ?? null,
97
+ avatarUrl: refreshed.claims.image ?? null,
98
+ };
99
+ if (marker)
100
+ cookieWrites.push((response) => clearRejectionMarker(response, options));
101
+ }
102
+ else if (refreshFailure?.reason === 'invalid_grant') {
103
+ if (confirming) {
104
+ // Second refusal, a minute or more after the first, on a request
105
+ // the member is waiting for: the token is dead. Clear it.
106
+ confirmedDead = true;
107
+ cookieWrites.push((response) => {
108
+ clearTokenCookies(response, options);
109
+ clearRejectionMarker(response, options);
110
+ });
111
+ }
112
+ else {
113
+ const firstSeen = { fingerprint, rejectedAt: nowSeconds };
114
+ cookieWrites.push((response) => setRejectionMarker(response, options, firstSeen));
115
+ }
116
+ }
81
117
  }
82
118
  }
83
119
  // 3. Populate context.locals.user and say HOW the session resolved. A site
@@ -94,15 +130,16 @@ export async function onRequest(context, next) {
94
130
  hadRefreshToken: refreshToken !== null,
95
131
  refreshFailure,
96
132
  deferred: deferredCarrier !== null,
133
+ suppressed,
97
134
  });
98
135
  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);
136
+ // A first refusal clears nothing: the issuer keeps refresh tokens in an
137
+ // eventually consistent store, so a token only seconds old can read as
138
+ // missing from another location and be answered invalid_grant, and a
139
+ // straggling subrequest may still carry the token a fresh sign-in replaced.
140
+ // Only the confirmed second refusal above clears, and the response that
141
+ // clears still reports `expired`, so a site's sign-in-again notice fires
142
+ // once; every request after it is anonymous.
106
143
  if (deferredCarrier !== null) {
107
144
  try {
108
145
  console.info('[auth] session refresh deferred', {
@@ -118,6 +155,18 @@ export async function onRequest(context, next) {
118
155
  if (deferredCarrier === 'prefetch')
119
156
  return prefetchDeferredResponse();
120
157
  }
158
+ else if (suppressed) {
159
+ try {
160
+ console.info('[auth] session refresh suppressed', {
161
+ pathname: context.url.pathname,
162
+ documentNavigation,
163
+ session,
164
+ });
165
+ }
166
+ catch {
167
+ /* a patched console must not turn a guest page into a 500 */
168
+ }
169
+ }
121
170
  else if (!user && refreshToken) {
122
171
  const failure = refreshFailure ?? { reason: 'unreported' };
123
172
  try {
@@ -126,6 +175,7 @@ export async function onRequest(context, next) {
126
175
  pathname: context.url.pathname,
127
176
  documentNavigation,
128
177
  session,
178
+ ...(confirmedDead ? { cleared: true } : {}),
129
179
  });
130
180
  }
131
181
  catch {
@@ -138,14 +188,25 @@ export async function onRequest(context, next) {
138
188
  // one the login renderer already knows how to explain. A rejected access
139
189
  // token with no refresh token beside it (wrong audience, say) is not a
140
190
  // session that ended, and its redirect stays exactly as it always was.
141
- const sessionEnded = refreshFailure?.reason === 'invalid_grant';
191
+ const sessionEnded = refreshFailure?.reason === 'invalid_grant' || suppressed;
142
192
  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);
193
+ if (cookieWrites.length === 0)
194
+ return Response.redirect(loginUrl.toString(), 302);
195
+ // Response.redirect's headers are immutable; the marker or the clear has
196
+ // to ride on this redirect, so build the same 302 by hand.
197
+ const redirect = new Response(null, {
198
+ status: 302,
199
+ headers: { Location: loginUrl.toString() },
200
+ });
201
+ for (const write of cookieWrites)
202
+ write(redirect);
203
+ return redirect;
144
204
  }
145
205
  // 5. Continue to the page
146
206
  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) {
207
+ // 6. If tokens were refreshed, a rejection marker moved, or the response must
208
+ // carry no-store, clone and apply.
209
+ if (refreshed || cookieWrites.length > 0 || context.locals.synthetic === true) {
149
210
  const modifiedResponse = new Response(response.body, {
150
211
  status: response.status,
151
212
  statusText: response.statusText,
@@ -154,6 +215,8 @@ export async function onRequest(context, next) {
154
215
  if (refreshed) {
155
216
  setTokenCookies(modifiedResponse, options, refreshed.accessToken, refreshed.refreshToken);
156
217
  }
218
+ for (const write of cookieWrites)
219
+ write(modifiedResponse);
157
220
  if (context.locals.synthetic === true) {
158
221
  for (const header of PRIVATE_NO_STORE_HEADERS) {
159
222
  modifiedResponse.headers.set(header, 'private, no-store');
@@ -173,6 +236,9 @@ function resolveSessionState(input) {
173
236
  // A rejected access token with no refresh token beside it cannot be revived.
174
237
  if (!input.hadRefreshToken)
175
238
  return 'expired';
239
+ // A refusal remembered from a moment ago is still a refusal.
240
+ if (input.suppressed)
241
+ return 'expired';
176
242
  return input.refreshFailure?.reason === 'invalid_grant' ? 'expired' : 'refresh_failed';
177
243
  }
178
244
  /** 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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,sBAAsB,CAAA;AAE7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAC/D,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAA;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EACN,sBAAsB,EACtB,wBAAwB,GAExB,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAuB,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAE1F,MAAM,wBAAwB,GAAG;IAChC,eAAe;IACf,mBAAmB;IACnB,8BAA8B;CACrB,CAAA;AAEV,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,OAAwE,EACxE,IAA6B;IAE7B,MAAM,OAAO,GAAG,SAAS,EAAE,CAAA;IAC3B,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAE3B,8BAA8B;IAC9B,MAAM,WAAW,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,KAAK,CAAC,CAAA;IACpE,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,KAAK,CAAC,CAAA;IAErE,6BAA6B;IAC7B,IAAI,IAAI,GAAgB,IAAI,CAAA;IAC5B,IAAI,SAAS,GAA8C,IAAI,CAAA;IAC/D,IAAI,oBAAwC,CAAA;IAE5C,uEAAuE;IACvE,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,uEAAuE;IACvE,IAAI,YAAY,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACxD,MAAM,YAAY,GAAG,MAAM,mBAAmB,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;QAC7E,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAA;YAC1B,OAAO,+BAA+B,CAAC,OAAO,EAAE;gBAC/C,OAAO,EAAE,oBAAoB;gBAC7B,IAAI,EAAE,YAAY,CAAC,IAAI;aACvB,CAAC,CAAA;QACH,CAAC;QACD,oBAAoB,GAAG,YAAY,CAAC,MAAM,IAAI,SAAS,CAAA;IACxD,CAAC;IAED,IAAI,WAAW,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC7F,IAAI,QAAQ,EAAE,CAAC;YACd,IAAI,GAAG;gBACN,EAAE,EAAE,QAAQ,CAAC,GAAG;gBAChB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,IAAI;gBAC3B,SAAS,EAAE,QAAQ,CAAC,KAAK,IAAI,IAAI;aACjC,CAAA;YACD,IAAI,QAAQ,CAAC,SAAS,KAAK,IAAI,IAAI,QAAQ,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC;gBACtE,OAAO,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAA;YAChC,CAAC;QACF,CAAC;IACF,CAAC;IAED,0EAA0E;IAC1E,4EAA4E;IAC5E,sEAAsE;IACtE,sEAAsE;IACtE,IAAI,cAAc,GAAG,IAA6B,CAAA;IAClD,IAAI,eAAe,GAA0B,IAAI,CAAA;IACjD,IAAI,CAAC,IAAI,IAAI,YAAY,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAA;QAC/C,IAAI,OAAO,KAAK,YAAY;YAAE,eAAe,GAAG,OAAO,CAAA;IACxD,CAAC;IACD,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAA;QACzD,SAAS,GAAG,MAAM,aAAa,CAC9B,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,oBAAoB,EACpB,OAAO,CAAC,QAAQ,EAChB,CAAC,OAAO,EAAE,EAAE;YACX,cAAc,GAAG,OAAO,CAAA;QACzB,CAAC,CACD,CAAA;QACD,IAAI,SAAS,EAAE,CAAC;YACf,IAAI,GAAG;gBACN,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG;gBACxB,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK;gBAC7B,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI;gBACnC,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI;aACzC,CAAA;QACF,CAAC;IACF,CAAC;IAED,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,yEAAyE;IACzE,0EAA0E;IAC1E,mBAAmB;IACnB,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAA;IAC1B,MAAM,OAAO,GAAG,mBAAmB,CAAC;QACnC,IAAI;QACJ,SAAS,EAAE,SAAS,KAAK,IAAI;QAC7B,cAAc,EAAE,WAAW,KAAK,IAAI;QACpC,eAAe,EAAE,YAAY,KAAK,IAAI;QACtC,cAAc;QACd,QAAQ,EAAE,eAAe,KAAK,IAAI;KAClC,CAAC,CAAA;IACF,OAAO,CAAC,MAAM,CAAC,WAAW,GAAG,OAAO,CAAA;IAEpC,6EAA6E;IAC7E,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,yEAAyE;IACzE,qEAAqE;IACrE,MAAM,kBAAkB,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAA;IAExD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAE;gBAC/C,OAAO,EAAE,eAAe;gBACxB,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ;aAC9B,CAAC,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACR,6DAA6D;QAC9D,CAAC;QACD,yEAAyE;QACzE,yEAAyE;QACzE,IAAI,eAAe,KAAK,UAAU;YAAE,OAAO,wBAAwB,EAAE,CAAA;IACtE,CAAC;SAAM,IAAI,CAAC,IAAI,IAAI,YAAY,EAAE,CAAC;QAClC,MAAM,OAAO,GAAmB,cAAc,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAA;QAC1E,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE;gBAC7C,GAAG,OAAO;gBACV,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ;gBAC9B,kBAAkB;gBAClB,OAAO;aACP,CAAC,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACR,6DAA6D;QAC9D,CAAC;IACF,CAAC;IAED,0BAA0B;IAC1B,IAAI,CAAC,IAAI,IAAI,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACpE,wEAAwE;QACxE,yEAAyE;QACzE,uEAAuE;QACvE,uEAAuE;QACvE,MAAM,YAAY,GAAG,cAAc,EAAE,MAAM,KAAK,eAAe,CAAA;QAC/D,MAAM,QAAQ,GAAG,IAAI,GAAG,CACvB,GAAG,OAAO,CAAC,SAAS,aAAa,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GACxE,YAAY,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAC3C,EAAE,EACF,OAAO,CAAC,GAAG,CAAC,MAAM,CAClB,CAAA;QACD,OAAO,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAA;IACnD,CAAC;IAED,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAA;IAE7B,oFAAoF;IACpF,IAAI,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QACpD,MAAM,gBAAgB,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;YACpD,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;SACtC,CAAC,CAAA;QACF,IAAI,SAAS,EAAE,CAAC;YACf,eAAe,CAAC,gBAAgB,EAAE,OAAO,EAAE,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QAC1F,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YACvC,KAAK,MAAM,MAAM,IAAI,wBAAwB,EAAE,CAAC;gBAC/C,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;YAC1D,CAAC;QACF,CAAC;QACD,OAAO,gBAAgB,CAAA;IACxB,CAAC;IAED,OAAO,QAAQ,CAAA;AAChB,CAAC;AAED,SAAS,mBAAmB,CAAC,KAO5B;IACA,IAAI,KAAK,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,CAAA;IACjE,IAAI,CAAC,KAAK,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO,WAAW,CAAA;IACvE,IAAI,KAAK,CAAC,QAAQ;QAAE,OAAO,UAAU,CAAA;IACrC,6EAA6E;IAC7E,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO,SAAS,CAAA;IAC5C,OAAO,KAAK,CAAC,cAAc,EAAE,MAAM,KAAK,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAA;AACvF,CAAC;AAED,uFAAuF;AACvF,SAAS,oBAAoB,CAAC,OAAgB;IAC7C,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAClD,IAAI,IAAI;QAAE,OAAO,IAAI,KAAK,UAAU,CAAA;IACpC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAClD,IAAI,IAAI;QAAE,OAAO,IAAI,KAAK,UAAU,CAAA;IACpC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AACnE,CAAC;AAED,SAAS,WAAW,CAAC,QAAgB,EAAE,UAAoB;IAC1D,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAClC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA,CAAC,6BAA6B;YACjE,OAAO,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;QACnC,CAAC;QACD,OAAO,QAAQ,KAAK,OAAO,CAAA;IAC5B,CAAC,CAAC,CAAA;AACH,CAAC"}
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 {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,IAAI;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAA;AACpC,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAA;AAEtC,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,oBAAoB;IACpC,YAAY,EAAE;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,SAAS,CAAC,EAAE,aAAa,GAAG,iBAAiB,GAAG,MAAM,CAAA;IACtD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC7C,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC5C,gBAAgB,EAAE,MAAM,CAAA;IACxB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACvC,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,MAAM,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACzC,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAA;AAE/E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,kBAAkB,CAAA;AACjF,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,kBAAkB,CAAA;AACnG,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,4BAA4B,KAAK,kBAAkB,CAAA;AACjG,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,kBAAkB,CAAA;AACvF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,yBAAyB,KAAK,kBAAkB,CAAA;AAC3F,MAAM,MAAM,wBAAwB,GAAG,CACtC,OAAO,EAAE,+BAA+B,KACpC,kBAAkB,CAAA;AAEvB;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,cAAc,CAAC,EAAE,gBAAgB,CAAA;IACjC,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,cAAc,CAAC,EAAE,sBAAsB,CAAA;IACvC,aAAa,CAAC,EAAE,qBAAqB,CAAA;IACrC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,UAAU,CAAC,EAAE,kBAAkB,CAAA;IAC/B,gBAAgB,CAAC,EAAE,wBAAwB,CAAA;CAC3C;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE;QACP,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACD;AAED,MAAM,WAAW,mBAAmB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAA;IAC1D,MAAM,EAAE;QACP,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,gBAAgB,GACzB,WAAW,GACX,UAAU,GACV,WAAW,GACX,SAAS,GACT,gBAAgB,GAChB,UAAU,CAAA;AAGb,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,GAAG,CAAC;QACb,UAAU,MAAM;YACf,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;YACjB,WAAW,CAAC,EAAE,gBAAgB,CAAA;YAC9B,SAAS,CAAC,EAAE,OAAO,CAAA;SACnB;KACD;CACD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,IAAI;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAA;AACpC,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAA;AAEtC,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,oBAAoB;IACpC,YAAY,EAAE;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,SAAS,CAAC,EAAE,aAAa,GAAG,iBAAiB,GAAG,MAAM,CAAA;IACtD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,6BAA6B;IAC7C,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,4BAA4B;IAC5C,gBAAgB,EAAE,MAAM,CAAA;IACxB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACvC,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,MAAM,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACzC,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAA;AAE/E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,kBAAkB,CAAA;AACjF,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,kBAAkB,CAAA;AACnG,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,4BAA4B,KAAK,kBAAkB,CAAA;AACjG,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,kBAAkB,CAAA;AACvF,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,yBAAyB,KAAK,kBAAkB,CAAA;AAC3F,MAAM,MAAM,wBAAwB,GAAG,CACtC,OAAO,EAAE,+BAA+B,KACpC,kBAAkB,CAAA;AAEvB;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,cAAc,CAAC,EAAE,gBAAgB,CAAA;IACjC,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,cAAc,CAAC,EAAE,sBAAsB,CAAA;IACvC,aAAa,CAAC,EAAE,qBAAqB,CAAA;IACrC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,UAAU,CAAC,EAAE,kBAAkB,CAAA;IAC/B,gBAAgB,CAAC,EAAE,wBAAwB,CAAA;CAC3C;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE;QACP,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACD;AAED,MAAM,WAAW,mBAAmB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAA;IAC1D,MAAM,EAAE;QACP,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACD;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,gBAAgB,GACzB,WAAW,GACX,UAAU,GACV,WAAW,GACX,SAAS,GACT,gBAAgB,GAChB,UAAU,CAAA;AAGb,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,GAAG,CAAC;QACb,UAAU,MAAM;YACf,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;YACjB,WAAW,CAAC,EAAE,gBAAgB,CAAA;YAC9B,SAAS,CAAC,EAAE,OAAO,CAAA;SACnB;KACD;CACD"}
@@ -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.15.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,57 @@ export async function onRequest(
80
89
  const carrier = classifyRefreshCarrier(request)
81
90
  if (carrier !== 'consumable') deferredCarrier = carrier
82
91
  }
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
83
102
  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,
103
+ const fingerprint = await fingerprintRefreshToken(refreshToken)
104
+ const marker = parseRejectionMarker(getCookie(request, rejectionMarkerCookieName(options)))
105
+ const marked = marker !== null && marker.fingerprint === fingerprint
106
+ const confirming = marked && mayConfirmRejection(marker, nowSeconds, documentNavigation)
107
+
108
+ if (marked && !confirming) {
109
+ suppressed = true
110
+ } else {
111
+ const clientId = options.clientId || context.url.hostname
112
+ refreshed = await refreshTokens(
113
+ issuer,
114
+ refreshToken,
115
+ clientId,
116
+ resolvedClientSecret,
117
+ options.resource,
118
+ (failure) => {
119
+ refreshFailure = failure
120
+ },
121
+ )
122
+ if (refreshed) {
123
+ user = {
124
+ id: refreshed.claims.sub,
125
+ email: refreshed.claims.email,
126
+ name: refreshed.claims.name ?? null,
127
+ avatarUrl: refreshed.claims.image ?? null,
128
+ }
129
+ if (marker) cookieWrites.push((response) => clearRejectionMarker(response, options))
130
+ } else if (refreshFailure?.reason === 'invalid_grant') {
131
+ if (confirming) {
132
+ // Second refusal, a minute or more after the first, on a request
133
+ // the member is waiting for: the token is dead. Clear it.
134
+ confirmedDead = true
135
+ cookieWrites.push((response) => {
136
+ clearTokenCookies(response, options)
137
+ clearRejectionMarker(response, options)
138
+ })
139
+ } else {
140
+ const firstSeen: RefreshRejectionMarker = { fingerprint, rejectedAt: nowSeconds }
141
+ cookieWrites.push((response) => setRejectionMarker(response, options, firstSeen))
142
+ }
101
143
  }
102
144
  }
103
145
  }
@@ -116,17 +158,17 @@ export async function onRequest(
116
158
  hadRefreshToken: refreshToken !== null,
117
159
  refreshFailure,
118
160
  deferred: deferredCarrier !== null,
161
+ suppressed,
119
162
  })
120
163
  context.locals.authSession = session
121
164
 
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
-
165
+ // A first refusal clears nothing: the issuer keeps refresh tokens in an
166
+ // eventually consistent store, so a token only seconds old can read as
167
+ // missing from another location and be answered invalid_grant, and a
168
+ // straggling subrequest may still carry the token a fresh sign-in replaced.
169
+ // Only the confirmed second refusal above clears, and the response that
170
+ // clears still reports `expired`, so a site's sign-in-again notice fires
171
+ // once; every request after it is anonymous.
130
172
  if (deferredCarrier !== null) {
131
173
  try {
132
174
  console.info('[auth] session refresh deferred', {
@@ -139,6 +181,16 @@ export async function onRequest(
139
181
  // A prefetched document rendered as a guest would be shown to the member
140
182
  // on navigation as if they had signed out; a non-2xx is dropped instead.
141
183
  if (deferredCarrier === 'prefetch') return prefetchDeferredResponse()
184
+ } else if (suppressed) {
185
+ try {
186
+ console.info('[auth] session refresh suppressed', {
187
+ pathname: context.url.pathname,
188
+ documentNavigation,
189
+ session,
190
+ })
191
+ } catch {
192
+ /* a patched console must not turn a guest page into a 500 */
193
+ }
142
194
  } else if (!user && refreshToken) {
143
195
  const failure: RefreshFailure = refreshFailure ?? { reason: 'unreported' }
144
196
  try {
@@ -147,6 +199,7 @@ export async function onRequest(
147
199
  pathname: context.url.pathname,
148
200
  documentNavigation,
149
201
  session,
202
+ ...(confirmedDead ? { cleared: true } : {}),
150
203
  })
151
204
  } catch {
152
205
  /* a patched console must not turn a guest page into a 500 */
@@ -159,21 +212,30 @@ export async function onRequest(
159
212
  // one the login renderer already knows how to explain. A rejected access
160
213
  // token with no refresh token beside it (wrong audience, say) is not a
161
214
  // session that ended, and its redirect stays exactly as it always was.
162
- const sessionEnded = refreshFailure?.reason === 'invalid_grant'
215
+ const sessionEnded = refreshFailure?.reason === 'invalid_grant' || suppressed
163
216
  const loginUrl = new URL(
164
217
  `${options.loginPath}?redirect=${encodeURIComponent(context.url.pathname)}${
165
218
  sessionEnded ? '&error=session_expired' : ''
166
219
  }`,
167
220
  context.url.origin,
168
221
  )
169
- return Response.redirect(loginUrl.toString(), 302)
222
+ if (cookieWrites.length === 0) return Response.redirect(loginUrl.toString(), 302)
223
+ // Response.redirect's headers are immutable; the marker or the clear has
224
+ // to ride on this redirect, so build the same 302 by hand.
225
+ const redirect = new Response(null, {
226
+ status: 302,
227
+ headers: { Location: loginUrl.toString() },
228
+ })
229
+ for (const write of cookieWrites) write(redirect)
230
+ return redirect
170
231
  }
171
232
 
172
233
  // 5. Continue to the page
173
234
  const response = await next()
174
235
 
175
- // 6. If tokens were refreshed or the response must carry no-store, clone and apply.
176
- if (refreshed || context.locals.synthetic === true) {
236
+ // 6. If tokens were refreshed, a rejection marker moved, or the response must
237
+ // carry no-store, clone and apply.
238
+ if (refreshed || cookieWrites.length > 0 || context.locals.synthetic === true) {
177
239
  const modifiedResponse = new Response(response.body, {
178
240
  status: response.status,
179
241
  statusText: response.statusText,
@@ -182,6 +244,7 @@ export async function onRequest(
182
244
  if (refreshed) {
183
245
  setTokenCookies(modifiedResponse, options, refreshed.accessToken, refreshed.refreshToken)
184
246
  }
247
+ for (const write of cookieWrites) write(modifiedResponse)
185
248
  if (context.locals.synthetic === true) {
186
249
  for (const header of PRIVATE_NO_STORE_HEADERS) {
187
250
  modifiedResponse.headers.set(header, 'private, no-store')
@@ -200,12 +263,15 @@ function resolveSessionState(input: {
200
263
  hadRefreshToken: boolean
201
264
  refreshFailure: RefreshFailure | null
202
265
  deferred: boolean
266
+ suppressed: boolean
203
267
  }): AuthSessionState {
204
268
  if (input.user) return input.refreshed ? 'refreshed' : 'verified'
205
269
  if (!input.hadAccessToken && !input.hadRefreshToken) return 'anonymous'
206
270
  if (input.deferred) return 'deferred'
207
271
  // A rejected access token with no refresh token beside it cannot be revived.
208
272
  if (!input.hadRefreshToken) return 'expired'
273
+ // A refusal remembered from a moment ago is still a refusal.
274
+ if (input.suppressed) return 'expired'
209
275
  return input.refreshFailure?.reason === 'invalid_grant' ? 'expired' : 'refresh_failed'
210
276
  }
211
277
 
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'
@@ -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
+ }