@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 +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +89 -23
- package/dist/middleware/auth.js.map +1 -1
- package/dist/types.d.ts +6 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/refresh-rejection.d.ts +23 -0
- package/dist/utils/refresh-rejection.d.ts.map +1 -0
- package/dist/utils/refresh-rejection.js +89 -0
- package/dist/utils/refresh-rejection.js.map +1 -0
- package/package.json +1 -1
- package/src/middleware/auth.ts +96 -30
- package/src/types.ts +6 -4
- package/src/utils/refresh-rejection.ts +115 -0
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;
|
|
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"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// the
|
|
104
|
-
//
|
|
105
|
-
|
|
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
|
-
|
|
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
|
|
148
|
-
|
|
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;
|
|
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,iBAAiB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACnF,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,EACN,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EAEpB,yBAAyB,EACzB,kBAAkB,GAClB,MAAM,+BAA+B,CAAA;AACtC,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;IAED,0EAA0E;IAC1E,6EAA6E;IAC7E,yEAAyE;IACzE,sEAAsE;IACtE,MAAM,kBAAkB,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAA;IACxD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IAChD,MAAM,YAAY,GAAwC,EAAE,CAAA;IAC5D,IAAI,UAAU,GAAG,KAAK,CAAA;IACtB,IAAI,aAAa,GAAG,KAAK,CAAA;IACzB,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QACvD,MAAM,WAAW,GAAG,MAAM,uBAAuB,CAAC,YAAY,CAAC,CAAA;QAC/D,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,OAAO,EAAE,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;QAC3F,MAAM,MAAM,GAAG,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,WAAW,KAAK,WAAW,CAAA;QACpE,MAAM,UAAU,GAAG,MAAM,IAAI,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,kBAAkB,CAAC,CAAA;QAExF,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3B,UAAU,GAAG,IAAI,CAAA;QAClB,CAAC;aAAM,CAAC;YACP,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAA;YACzD,SAAS,GAAG,MAAM,aAAa,CAC9B,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,oBAAoB,EACpB,OAAO,CAAC,QAAQ,EAChB,CAAC,OAAO,EAAE,EAAE;gBACX,cAAc,GAAG,OAAO,CAAA;YACzB,CAAC,CACD,CAAA;YACD,IAAI,SAAS,EAAE,CAAC;gBACf,IAAI,GAAG;oBACN,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG;oBACxB,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK;oBAC7B,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI;oBACnC,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI;iBACzC,CAAA;gBACD,IAAI,MAAM;oBAAE,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAA;YACrF,CAAC;iBAAM,IAAI,cAAc,EAAE,MAAM,KAAK,eAAe,EAAE,CAAC;gBACvD,IAAI,UAAU,EAAE,CAAC;oBAChB,iEAAiE;oBACjE,0DAA0D;oBAC1D,aAAa,GAAG,IAAI,CAAA;oBACpB,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;wBAC9B,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;wBACpC,oBAAoB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;oBACxC,CAAC,CAAC,CAAA;gBACH,CAAC;qBAAM,CAAC;oBACP,MAAM,SAAS,GAA2B,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,CAAA;oBACjF,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;gBAClF,CAAC;YACF,CAAC;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;QAClC,UAAU;KACV,CAAC,CAAA;IACF,OAAO,CAAC,MAAM,CAAC,WAAW,GAAG,OAAO,CAAA;IAEpC,wEAAwE;IACxE,uEAAuE;IACvE,qEAAqE;IACrE,4EAA4E;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,6CAA6C;IAC7C,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,UAAU,EAAE,CAAC;QACvB,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE;gBACjD,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;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;gBACP,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3C,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,IAAI,UAAU,CAAA;QAC7E,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,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAA;QACjF,yEAAyE;QACzE,2DAA2D;QAC3D,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE;YACnC,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,EAAE;SAC1C,CAAC,CAAA;QACF,KAAK,MAAM,KAAK,IAAI,YAAY;YAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QACjD,OAAO,QAAQ,CAAA;IAChB,CAAC;IAED,0BAA0B;IAC1B,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,CAAA;IAE7B,8EAA8E;IAC9E,mCAAmC;IACnC,IAAI,SAAS,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAC/E,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,KAAK,MAAM,KAAK,IAAI,YAAY;YAAE,KAAK,CAAC,gBAAgB,CAAC,CAAA;QACzD,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,KAQ5B;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,6DAA6D;IAC7D,IAAI,KAAK,CAAC,UAAU;QAAE,OAAO,SAAS,CAAA;IACtC,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"}
|
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
|
|
144
|
-
* issuer's token store is eventually consistent), so
|
|
145
|
-
* them
|
|
146
|
-
*
|
|
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 {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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
package/src/middleware/auth.ts
CHANGED
|
@@ -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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// the
|
|
127
|
-
//
|
|
128
|
-
|
|
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
|
|
176
|
-
|
|
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
|
|
162
|
-
* issuer's token store is eventually consistent), so
|
|
163
|
-
* them
|
|
164
|
-
*
|
|
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
|
+
}
|