@fluixi/oauth2 1.0.0-alpha.11 → 1.0.0-alpha.13
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/dist/backchannel.cjs +462 -0
- package/dist/backchannel.d.ts +39 -0
- package/dist/backchannel.d.ts.map +1 -0
- package/dist/backchannel.js +97 -0
- package/dist/backchannel.mjs +307 -0
- package/dist/{browser-N432SMC3.mjs → browser-SQXLVG25.mjs} +2 -2
- package/dist/browser.cjs +6 -1
- package/dist/browser.mjs +6 -1
- package/dist/callback.mjs +1 -1
- package/dist/{chunk-ZVDC3JW7.mjs → chunk-CVUD2UAP.mjs} +1 -1
- package/dist/chunk-H5KMWPVT.mjs +108 -0
- package/dist/{chunk-4TPZVJSR.mjs → chunk-QZFYCRMT.mjs} +1 -1
- package/dist/discovery.cjs +6 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +6 -1
- package/dist/discovery.mjs +6 -1
- package/dist/{dpop-DYUR2CH2.mjs → dpop-7YXQSM5R.mjs} +4 -4
- package/dist/dpop-SP7YCDPE.mjs +22 -0
- package/dist/index.cjs +107 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.mjs +111 -11
- package/dist/server.cjs +649 -137
- package/dist/server.d.ts +64 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +376 -61
- package/dist/server.mjs +532 -139
- package/dist/service-client.cjs +7 -2
- package/dist/service-client.mjs +9 -4
- package/dist/service.cjs +104 -5
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +38 -6
- package/dist/service.mjs +105 -6
- package/dist/session-monitor.cjs +98 -0
- package/dist/session-monitor.d.ts +32 -0
- package/dist/session-monitor.d.ts.map +1 -0
- package/dist/session-monitor.js +104 -0
- package/dist/session-monitor.mjs +77 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.cjs +7 -2
- package/dist/verify.mjs +12 -7
- package/package.json +15 -15
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
2
|
-
import
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.13 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import {
|
|
3
|
+
OAuthError,
|
|
4
|
+
createDpopKey,
|
|
5
|
+
exportDpopKey,
|
|
6
|
+
importDpopKey,
|
|
7
|
+
nonceFrom,
|
|
8
|
+
proofFor
|
|
9
|
+
} from "./chunk-H5KMWPVT.mjs";
|
|
10
|
+
import "./chunk-QZFYCRMT.mjs";
|
|
3
11
|
|
|
4
12
|
// src/server.ts
|
|
5
13
|
import { createSessionCookie } from "@fluixi/session/server";
|
|
@@ -27,16 +35,6 @@ function safeReturnTo(candidate, options = {}) {
|
|
|
27
35
|
return url.toString();
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
// src/types.ts
|
|
31
|
-
var OAuthError = class extends Error {
|
|
32
|
-
constructor(message, code, description) {
|
|
33
|
-
super(message);
|
|
34
|
-
this.code = code;
|
|
35
|
-
this.description = description;
|
|
36
|
-
this.name = "OAuthError";
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
38
|
// src/client-auth.ts
|
|
41
39
|
var CANONICAL = {
|
|
42
40
|
basic: "client_secret_basic",
|
|
@@ -53,7 +51,7 @@ function resolveMethod(auth) {
|
|
|
53
51
|
}
|
|
54
52
|
async function loadJose() {
|
|
55
53
|
try {
|
|
56
|
-
return await import("./browser-
|
|
54
|
+
return await import("./browser-SQXLVG25.mjs");
|
|
57
55
|
} catch {
|
|
58
56
|
throw new OAuthError(
|
|
59
57
|
"private_key_jwt needs `jose`. Install it: npm i jose. It is an optional peer, so a client using a secret does not carry it.",
|
|
@@ -103,8 +101,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
103
101
|
try {
|
|
104
102
|
response = await fetcher(documentUrl(issuer));
|
|
105
103
|
} catch (cause) {
|
|
104
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
106
105
|
throw new OAuthError(
|
|
107
|
-
|
|
106
|
+
typeof window === "undefined" ? (
|
|
107
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
108
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
109
|
+
) : `Could not read the discovery document at ${documentUrl(issuer)}. In a browser this is usually the provider refusing a cross-origin read: give \`endpoints\` directly, or resolve them on a server. (${why})`,
|
|
108
110
|
"discovery_unreachable"
|
|
109
111
|
);
|
|
110
112
|
}
|
|
@@ -129,6 +131,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
129
131
|
jwks: doc.jwks_uri,
|
|
130
132
|
introspection: doc.introspection_endpoint,
|
|
131
133
|
revocation: doc.revocation_endpoint,
|
|
134
|
+
checkSession: doc.check_session_iframe,
|
|
132
135
|
issuer: doc.issuer ?? issuer
|
|
133
136
|
};
|
|
134
137
|
})();
|
|
@@ -137,6 +140,251 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
137
140
|
return pending;
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
// src/verify.ts
|
|
144
|
+
var TokenError = class extends OAuthError {
|
|
145
|
+
constructor(message, code = "invalid_token") {
|
|
146
|
+
super(message, code);
|
|
147
|
+
this.name = "TokenError";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
function bearerOf(request) {
|
|
151
|
+
const header = request.headers.get("authorization");
|
|
152
|
+
if (!header) return null;
|
|
153
|
+
const match = header.match(/^(?:Bearer|DPoP)\s+(.+)$/i);
|
|
154
|
+
return match ? match[1].trim() : null;
|
|
155
|
+
}
|
|
156
|
+
async function loadJose2() {
|
|
157
|
+
try {
|
|
158
|
+
return await import("./browser-SQXLVG25.mjs");
|
|
159
|
+
} catch {
|
|
160
|
+
throw new OAuthError(
|
|
161
|
+
"Token verification needs `jose`. Install it: npm i jose. It is an optional peer so an app that does not verify does not carry it.",
|
|
162
|
+
"jose_missing"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function createTokenVerifier(options) {
|
|
167
|
+
const issuers = (Array.isArray(options.issuer) ? options.issuer : [options.issuer]).filter(Boolean);
|
|
168
|
+
if (!issuers.length) {
|
|
169
|
+
throw new Error("[fluixi/oauth2] createTokenVerifier needs an issuer to accept tokens from.");
|
|
170
|
+
}
|
|
171
|
+
const discoveryIssuer = issuers[0];
|
|
172
|
+
if (options.audience === void 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"[fluixi/oauth2] createTokenVerifier needs an audience, or `audience: false` to say this service really does accept a token minted for any client of the issuer."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (options.audience === false && options.authorizedParty === void 0) {
|
|
178
|
+
console.warn(
|
|
179
|
+
"[fluixi/oauth2] This verifier checks neither `aud` nor `azp`, so a token minted for any client of the issuer is accepted. Set `authorizedParty` if that is not what you meant."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const algorithms = options.algorithms ?? ["RS256"];
|
|
183
|
+
if (algorithms.some((alg) => !alg || alg.toLowerCase() === "none")) {
|
|
184
|
+
throw new Error("[fluixi/oauth2] `none` is not a signing algorithm and cannot be accepted.");
|
|
185
|
+
}
|
|
186
|
+
let keys = null;
|
|
187
|
+
async function keySet() {
|
|
188
|
+
if (keys) return keys;
|
|
189
|
+
const jose = await loadJose2();
|
|
190
|
+
if (options.jwks) {
|
|
191
|
+
keys = jose.createLocalJWKSet(options.jwks);
|
|
192
|
+
return keys;
|
|
193
|
+
}
|
|
194
|
+
let uri = options.jwksUri;
|
|
195
|
+
if (!uri) {
|
|
196
|
+
const endpoints = await discover(discoveryIssuer);
|
|
197
|
+
uri = endpoints.jwks;
|
|
198
|
+
}
|
|
199
|
+
if (!uri) {
|
|
200
|
+
throw new OAuthError(
|
|
201
|
+
`${discoveryIssuer} published no jwks_uri. Give the verifier one directly.`,
|
|
202
|
+
"no_jwks"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
keys = jose.createRemoteJWKSet(new URL(uri));
|
|
206
|
+
return keys;
|
|
207
|
+
}
|
|
208
|
+
const checkProof = async (claims, token, request) => {
|
|
209
|
+
const jose = await loadJose2();
|
|
210
|
+
const { boundKeyOf, thumbprintOf } = await import("./dpop-SP7YCDPE.mjs");
|
|
211
|
+
const bound = boundKeyOf(claims);
|
|
212
|
+
if (!bound) {
|
|
213
|
+
throw new TokenError(
|
|
214
|
+
"This token is not bound to a key, and this service requires binding.",
|
|
215
|
+
"token_not_bound"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const proof = request.headers.get("dpop");
|
|
219
|
+
if (!proof) throw new TokenError("The request carried no DPoP proof.", "missing_dpop_proof");
|
|
220
|
+
let payload;
|
|
221
|
+
let header;
|
|
222
|
+
try {
|
|
223
|
+
const result = await jose.jwtVerify(proof, jose.EmbeddedJWK, { typ: "dpop+jwt" });
|
|
224
|
+
payload = result.payload;
|
|
225
|
+
header = result.protectedHeader;
|
|
226
|
+
} catch (cause) {
|
|
227
|
+
throw new TokenError(
|
|
228
|
+
`The DPoP proof did not verify: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
229
|
+
"invalid_dpop_proof"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const presented = await thumbprintOf(header.jwk);
|
|
233
|
+
if (presented !== bound) {
|
|
234
|
+
throw new TokenError(
|
|
235
|
+
"The DPoP proof was signed with a different key from the one this token is bound to.",
|
|
236
|
+
"dpop_key_mismatch"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const url = new URL(request.url);
|
|
240
|
+
url.search = "";
|
|
241
|
+
url.hash = "";
|
|
242
|
+
if (payload.htm !== request.method.toUpperCase()) {
|
|
243
|
+
throw new TokenError("The DPoP proof was made for a different method.", "invalid_dpop_proof");
|
|
244
|
+
}
|
|
245
|
+
if (payload.htu !== url.toString()) {
|
|
246
|
+
throw new TokenError("The DPoP proof was made for a different URL.", "invalid_dpop_proof");
|
|
247
|
+
}
|
|
248
|
+
if (typeof payload.ath === "string") {
|
|
249
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
|
|
250
|
+
let binary = "";
|
|
251
|
+
for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
|
|
252
|
+
const expected = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
253
|
+
if (payload.ath !== expected) {
|
|
254
|
+
throw new TokenError("The DPoP proof was made for a different token.", "invalid_dpop_proof");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const age = Math.abs(Date.now() / 1e3 - payload.iat);
|
|
258
|
+
if (!Number.isFinite(age) || age > (options.dpopProofAgeSec ?? 10)) {
|
|
259
|
+
throw new TokenError("The DPoP proof is too old to accept.", "invalid_dpop_proof");
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const verify = async (token, request) => {
|
|
263
|
+
if (!token) throw new TokenError("No token to verify.", "no_token");
|
|
264
|
+
const jose = await loadJose2();
|
|
265
|
+
let payload;
|
|
266
|
+
try {
|
|
267
|
+
({ payload } = await jose.jwtVerify(token, await keySet(), {
|
|
268
|
+
// jose takes a list and accepts any of them, which is what a provider naming
|
|
269
|
+
// itself two ways needs.
|
|
270
|
+
issuer: issuers,
|
|
271
|
+
...options.audience === false ? {} : { audience: options.audience },
|
|
272
|
+
algorithms,
|
|
273
|
+
clockTolerance: options.clockToleranceSec ?? 5
|
|
274
|
+
}));
|
|
275
|
+
} catch (cause) {
|
|
276
|
+
throw new TokenError(
|
|
277
|
+
`The token was not accepted: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (options.authorizedParty !== void 0) {
|
|
281
|
+
const allowed = Array.isArray(options.authorizedParty) ? options.authorizedParty : [options.authorizedParty];
|
|
282
|
+
const issuedTo = payload.azp ?? payload.client_id;
|
|
283
|
+
if (typeof issuedTo !== "string" || !allowed.includes(issuedTo)) {
|
|
284
|
+
throw new TokenError("The token was issued to a different client.", "invalid_azp");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (options.dpop) {
|
|
288
|
+
if (!request) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"[fluixi/oauth2] `dpop: true` needs the request to read its proof. Use `verify.fromRequest(request)` rather than verifying a bare token."
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
await checkProof(payload, token, request);
|
|
294
|
+
}
|
|
295
|
+
if (options.require) {
|
|
296
|
+
const verdict = options.require(payload);
|
|
297
|
+
if (verdict !== true) {
|
|
298
|
+
throw new TokenError(
|
|
299
|
+
typeof verdict === "string" ? verdict : "The token did not meet this service's requirements.",
|
|
300
|
+
"insufficient_claims"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return payload;
|
|
305
|
+
};
|
|
306
|
+
const verifier = verify;
|
|
307
|
+
verifier.bearerOf = bearerOf;
|
|
308
|
+
verifier.fromRequest = async (request) => {
|
|
309
|
+
const token = verifier.bearerOf(request);
|
|
310
|
+
if (!token) {
|
|
311
|
+
throw new TokenError("The request carried no bearer token.", "no_token");
|
|
312
|
+
}
|
|
313
|
+
return verify(token, request);
|
|
314
|
+
};
|
|
315
|
+
return verifier;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/backchannel.ts
|
|
319
|
+
var LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
320
|
+
function memoryLogouts(ttlMs = 14 * 24 * 36e5) {
|
|
321
|
+
const sids = /* @__PURE__ */ new Map();
|
|
322
|
+
const subs = /* @__PURE__ */ new Map();
|
|
323
|
+
const prune = (now) => {
|
|
324
|
+
for (const map of [sids, subs]) for (const [key, at] of map) if (now - at > ttlMs) map.delete(key);
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
add(entry) {
|
|
328
|
+
prune(Date.now());
|
|
329
|
+
if (entry.sid) sids.set(entry.sid, entry.at);
|
|
330
|
+
else if (entry.sub) subs.set(entry.sub, Math.max(entry.at, subs.get(entry.sub) ?? 0));
|
|
331
|
+
},
|
|
332
|
+
ended(session) {
|
|
333
|
+
if (session.sid && sids.has(session.sid)) return true;
|
|
334
|
+
const all = session.sub ? subs.get(session.sub) : void 0;
|
|
335
|
+
return all !== void 0 && session.since <= all;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function idTokenClaims(idToken) {
|
|
340
|
+
if (!idToken) return {};
|
|
341
|
+
try {
|
|
342
|
+
const part = idToken.split(".")[1] ?? "";
|
|
343
|
+
const json = atob(part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "="));
|
|
344
|
+
const claims = JSON.parse(json);
|
|
345
|
+
return {
|
|
346
|
+
sid: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
347
|
+
sub: typeof claims.sub === "string" ? claims.sub : void 0,
|
|
348
|
+
iat: typeof claims.iat === "number" ? claims.iat : void 0
|
|
349
|
+
};
|
|
350
|
+
} catch {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function headerOf(token) {
|
|
355
|
+
try {
|
|
356
|
+
const part = token.split(".")[0] ?? "";
|
|
357
|
+
return JSON.parse(atob(part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "=")));
|
|
358
|
+
} catch {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function logoutTokenVerifier(options) {
|
|
363
|
+
const verify = createTokenVerifier({
|
|
364
|
+
issuer: options.issuer,
|
|
365
|
+
audience: options.clientId,
|
|
366
|
+
jwksUri: options.jwksUri,
|
|
367
|
+
jwks: options.jwks,
|
|
368
|
+
require: (claims) => {
|
|
369
|
+
const events = claims.events;
|
|
370
|
+
if (!events || typeof events !== "object" || !(LOGOUT_EVENT in events)) return "no logout event";
|
|
371
|
+
if ("nonce" in claims) return "a logout token has no nonce";
|
|
372
|
+
if (typeof claims.sid !== "string" && typeof claims.sub !== "string") return "names no session and no subject";
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return async (token) => {
|
|
377
|
+
const typ = headerOf(token).typ;
|
|
378
|
+
if (typ !== void 0 && typ !== "logout+jwt") throw new Error("not a logout token");
|
|
379
|
+
const claims = await verify(token);
|
|
380
|
+
return {
|
|
381
|
+
sid: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
382
|
+
sub: typeof claims.sub === "string" ? claims.sub : void 0,
|
|
383
|
+
at: (typeof claims.iat === "number" ? claims.iat : Math.floor(Date.now() / 1e3)) * 1e3
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
140
388
|
// src/pkce.ts
|
|
141
389
|
function base64url(bytes) {
|
|
142
390
|
let binary = "";
|
|
@@ -173,81 +421,60 @@ function expiryFrom(expiresIn, now = Date.now()) {
|
|
|
173
421
|
return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
174
422
|
}
|
|
175
423
|
|
|
176
|
-
// src/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
193
|
-
return {
|
|
194
|
-
privateKey,
|
|
195
|
-
publicJwk,
|
|
196
|
-
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
197
|
-
alg
|
|
198
|
-
};
|
|
424
|
+
// src/server.ts
|
|
425
|
+
import { defineModule } from "@fluixi/start/app";
|
|
426
|
+
var RETRY_MARK = ".retry";
|
|
427
|
+
var PROMPTS = /* @__PURE__ */ new Set(["none", "login", "consent", "select_account"]);
|
|
428
|
+
var SILENT_REFUSALS = /* @__PURE__ */ new Set([
|
|
429
|
+
"login_required",
|
|
430
|
+
"consent_required",
|
|
431
|
+
"interaction_required",
|
|
432
|
+
"account_selection_required"
|
|
433
|
+
]);
|
|
434
|
+
function checkedCookie(config, sessionState) {
|
|
435
|
+
const name = `${config.cookieName ?? "fx_session"}_checked`;
|
|
436
|
+
const secure = config.secureCookie ?? true;
|
|
437
|
+
const maxAge = Math.max(0, Math.floor(config.silentRecheck ?? 60));
|
|
438
|
+
const value = sessionState ?? "1";
|
|
439
|
+
return { name, header: `${name}=${value}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure ? "; Secure" : ""}` };
|
|
199
440
|
}
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
return
|
|
441
|
+
function sessionStateFrom(url) {
|
|
442
|
+
const value = url.searchParams.get("session_state");
|
|
443
|
+
return value && /^[\w-]{1,128}\.[\w-]{1,64}$/.test(value) ? value : void 0;
|
|
203
444
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
privateKey,
|
|
211
|
-
publicJwk,
|
|
212
|
-
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
213
|
-
alg: stored.alg
|
|
214
|
-
};
|
|
445
|
+
function isPageRequest(request) {
|
|
446
|
+
if (request.method !== "GET") return false;
|
|
447
|
+
const dest = request.headers.get("sec-fetch-dest");
|
|
448
|
+
if (dest) return dest === "document";
|
|
449
|
+
return (request.headers.get("accept") ?? "").includes("text/html");
|
|
215
450
|
}
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const url = new URL(options.url);
|
|
221
|
-
url.search = "";
|
|
222
|
-
url.hash = "";
|
|
223
|
-
return url.toString();
|
|
224
|
-
} catch {
|
|
225
|
-
return options.url;
|
|
226
|
-
}
|
|
227
|
-
})();
|
|
228
|
-
const claims = {
|
|
229
|
-
htm: options.method.toUpperCase(),
|
|
230
|
-
htu,
|
|
231
|
-
jti: crypto.randomUUID()
|
|
232
|
-
};
|
|
233
|
-
if (options.accessToken) {
|
|
234
|
-
const digest = await crypto.subtle.digest(
|
|
235
|
-
"SHA-256",
|
|
236
|
-
new TextEncoder().encode(options.accessToken)
|
|
237
|
-
);
|
|
238
|
-
let binary = "";
|
|
239
|
-
for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
|
|
240
|
-
claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
451
|
+
function cookieValue(request, name) {
|
|
452
|
+
for (const part of (request.headers.get("cookie") ?? "").split(";")) {
|
|
453
|
+
const [key, ...rest] = part.trim().split("=");
|
|
454
|
+
if (key === name) return rest.join("=");
|
|
241
455
|
}
|
|
242
|
-
|
|
243
|
-
return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
|
|
456
|
+
return void 0;
|
|
244
457
|
}
|
|
245
|
-
function
|
|
246
|
-
return
|
|
458
|
+
function hasCookie(request, name) {
|
|
459
|
+
return cookieValue(request, name) !== void 0;
|
|
460
|
+
}
|
|
461
|
+
function under(pathname, path) {
|
|
462
|
+
return pathname === path || pathname.startsWith(path.endsWith("/") ? path : `${path}/`);
|
|
463
|
+
}
|
|
464
|
+
function signInRedirect(request, config) {
|
|
465
|
+
if (!isPageRequest(request)) return null;
|
|
466
|
+
const url = new URL(request.url);
|
|
467
|
+
const base = (config.basePath ?? "/auth").replace(/\/$/, "");
|
|
468
|
+
const guarded = (config.protectedPaths ?? []).some((path) => under(url.pathname, path));
|
|
469
|
+
const mode = guarded ? "required" : config.signIn ?? "manual";
|
|
470
|
+
if (mode === "manual") return null;
|
|
471
|
+
if ([base, ...config.publicPaths ?? []].some((path) => under(url.pathname, path))) return null;
|
|
472
|
+
if (url.searchParams.has("auth_error")) return null;
|
|
473
|
+
if (mode === "silent" && hasCookie(request, checkedCookie(config).name)) return null;
|
|
474
|
+
const login = new URLSearchParams({ returnTo: url.pathname + url.search });
|
|
475
|
+
if (mode === "silent") login.set("prompt", "none");
|
|
476
|
+
return new Response(null, { status: 302, headers: { location: `${base}/login?${login}` } });
|
|
247
477
|
}
|
|
248
|
-
|
|
249
|
-
// src/server.ts
|
|
250
|
-
import { defineModule } from "@fluixi/start/app";
|
|
251
478
|
var dpopNonces = /* @__PURE__ */ new Map();
|
|
252
479
|
async function prove(headers, key, method, url, accessToken) {
|
|
253
480
|
headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
|
|
@@ -260,27 +487,25 @@ function learnNonce(url, response) {
|
|
|
260
487
|
dpopNonces.set(origin, offered);
|
|
261
488
|
return fresh && !response.ok;
|
|
262
489
|
}
|
|
263
|
-
async function
|
|
490
|
+
async function sendAs(session, input, init = {}) {
|
|
491
|
+
const url = String(input);
|
|
492
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
264
493
|
const key = session.dpop && session.tokenType?.toLowerCase() === "dpop" ? await importDpopKey(session.dpop) : null;
|
|
265
494
|
const send = async () => {
|
|
266
|
-
const
|
|
495
|
+
const credentials = {
|
|
267
496
|
authorization: `${key ? "DPoP" : "Bearer"} ${session.accessToken}`
|
|
268
497
|
};
|
|
269
|
-
if (key) await prove(
|
|
270
|
-
|
|
498
|
+
if (key) await prove(credentials, key, method, url, session.accessToken);
|
|
499
|
+
const headers = new Headers(init.headers);
|
|
500
|
+
for (const [name, value] of Object.entries(credentials)) headers.set(name, value);
|
|
501
|
+
return fetch(url, { ...init, method, headers });
|
|
271
502
|
};
|
|
272
503
|
let response = await send();
|
|
273
504
|
if (key && learnNonce(url, response)) response = await send();
|
|
274
505
|
return response;
|
|
275
506
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const scopes = config.scopes ?? ["openid", "profile", "email"];
|
|
279
|
-
const cookie = createSessionCookie({
|
|
280
|
-
secret: config.cookieSecret,
|
|
281
|
-
name: config.cookieName,
|
|
282
|
-
secure: config.secureCookie
|
|
283
|
-
});
|
|
507
|
+
var withToken = (url, session) => sendAs(session, url);
|
|
508
|
+
function tokenClient(config) {
|
|
284
509
|
let endpoints = config.endpoints ?? null;
|
|
285
510
|
const resolve = async () => {
|
|
286
511
|
if (endpoints) return endpoints;
|
|
@@ -290,10 +515,6 @@ function oauthRoutes(config) {
|
|
|
290
515
|
endpoints = await discover(config.issuer);
|
|
291
516
|
return endpoints;
|
|
292
517
|
};
|
|
293
|
-
const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
|
|
294
|
-
status,
|
|
295
|
-
headers: { "content-type": "application/json", ...headers }
|
|
296
|
-
});
|
|
297
518
|
const clientAuth = {
|
|
298
519
|
clientId: config.clientId,
|
|
299
520
|
// `client_secret_post` by default here, which is what this half has always sent, so
|
|
@@ -326,17 +547,111 @@ function oauthRoutes(config) {
|
|
|
326
547
|
}
|
|
327
548
|
return payload;
|
|
328
549
|
}
|
|
550
|
+
return { resolve, tokenRequest, clientAuth };
|
|
551
|
+
}
|
|
552
|
+
var renewals = /* @__PURE__ */ new Map();
|
|
553
|
+
var RENEWAL_REUSE_MS = 3e4;
|
|
554
|
+
function renew(current, tokenRequest) {
|
|
555
|
+
const refreshToken = current.refreshToken;
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
for (const [key, entry] of renewals) if (entry.until <= now) renewals.delete(key);
|
|
558
|
+
const known = renewals.get(refreshToken);
|
|
559
|
+
if (known) return known.result;
|
|
560
|
+
const result = (async () => {
|
|
561
|
+
const key = current.dpop ? await importDpopKey(current.dpop) : null;
|
|
562
|
+
const payload = await tokenRequest({ grant_type: "refresh_token", refresh_token: refreshToken }, key);
|
|
563
|
+
return {
|
|
564
|
+
accessToken: String(payload.access_token ?? ""),
|
|
565
|
+
// A provider that rotates refresh tokens sends a new one; one that does not
|
|
566
|
+
// expects the old one to keep working.
|
|
567
|
+
refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : refreshToken,
|
|
568
|
+
idToken: typeof payload.id_token === "string" ? payload.id_token : current.idToken,
|
|
569
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
570
|
+
tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
|
|
571
|
+
dpop: current.dpop,
|
|
572
|
+
sessionState: current.sessionState
|
|
573
|
+
};
|
|
574
|
+
})().catch((error) => {
|
|
575
|
+
if (error instanceof OAuthError) return null;
|
|
576
|
+
renewals.delete(refreshToken);
|
|
577
|
+
throw error;
|
|
578
|
+
});
|
|
579
|
+
renewals.set(refreshToken, { until: now + RENEWAL_REUSE_MS, result });
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
var EXPIRY_SKEW_MS = 3e4;
|
|
583
|
+
var expired = (session) => session.expiresAt !== void 0 && session.expiresAt - EXPIRY_SKEW_MS <= Date.now();
|
|
584
|
+
var defaultLogouts = /* @__PURE__ */ new WeakMap();
|
|
585
|
+
function logoutsOf(config) {
|
|
586
|
+
if (config.logouts) return config.logouts;
|
|
587
|
+
let store = defaultLogouts.get(config);
|
|
588
|
+
if (!store) defaultLogouts.set(config, store = memoryLogouts());
|
|
589
|
+
return store;
|
|
590
|
+
}
|
|
591
|
+
async function endedAtProvider(config, session) {
|
|
592
|
+
const id = idTokenClaims(session.idToken);
|
|
593
|
+
if (!id.sid && !id.sub) return false;
|
|
594
|
+
return logoutsOf(config).ended({ sid: id.sid, sub: id.sub, since: (id.iat ?? 0) * 1e3 });
|
|
595
|
+
}
|
|
596
|
+
function oauthRoutes(config) {
|
|
597
|
+
const { resolve, tokenRequest, clientAuth } = tokenClient(config);
|
|
598
|
+
const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
|
|
599
|
+
status,
|
|
600
|
+
headers: { "content-type": "application/json", ...headers }
|
|
601
|
+
});
|
|
602
|
+
const base = (config.basePath ?? "/auth").replace(/\/$/, "");
|
|
603
|
+
const scopes = config.scopes ?? ["openid", "profile", "email"];
|
|
604
|
+
const cookie = createSessionCookie({
|
|
605
|
+
secret: config.cookieSecret,
|
|
606
|
+
name: config.cookieName,
|
|
607
|
+
secure: config.secureCookie
|
|
608
|
+
});
|
|
329
609
|
return async function middleware(request, next) {
|
|
330
610
|
const url = new URL(request.url);
|
|
331
611
|
if (!url.pathname.startsWith(`${base}/`)) return next();
|
|
332
612
|
const route = url.pathname.slice(base.length);
|
|
333
613
|
const session = async () => {
|
|
334
|
-
|
|
614
|
+
const current = await cookie.read(request);
|
|
615
|
+
return current && await endedAtProvider(config, current) ? null : current;
|
|
335
616
|
};
|
|
617
|
+
if (route === "/backchannel-logout") {
|
|
618
|
+
const headers = { "cache-control": "no-store" };
|
|
619
|
+
if (request.method !== "POST") return json({ error: "invalid_request" }, 405, headers);
|
|
620
|
+
try {
|
|
621
|
+
const form = await request.formData();
|
|
622
|
+
const token = form.get("logout_token");
|
|
623
|
+
if (typeof token !== "string" || !token) throw new Error("no logout_token");
|
|
624
|
+
const endpoints = await resolve();
|
|
625
|
+
const issuer = endpoints.issuer ?? config.issuer;
|
|
626
|
+
if (!issuer) throw new Error("no issuer");
|
|
627
|
+
if (!endpoints.jwks) throw new Error("no jwks_uri");
|
|
628
|
+
const jwks = await (await fetch(endpoints.jwks)).json();
|
|
629
|
+
const entry = await logoutTokenVerifier({ issuer, clientId: config.clientId, jwks })(token);
|
|
630
|
+
await logoutsOf(config).add(entry);
|
|
631
|
+
return new Response(null, { status: 200, headers });
|
|
632
|
+
} catch {
|
|
633
|
+
return json({ error: "invalid_request" }, 400, headers);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
336
636
|
if (route === "/login") {
|
|
337
|
-
|
|
637
|
+
let authorization;
|
|
638
|
+
try {
|
|
639
|
+
({ authorization } = await resolve());
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (!(error instanceof OAuthError) || error.code !== "discovery_unreachable") throw error;
|
|
642
|
+
const back = safeReturnTo(url.searchParams.get("returnTo"), {
|
|
643
|
+
fallback: config.defaultReturnTo ?? "/",
|
|
644
|
+
origin: url.origin,
|
|
645
|
+
allowedOrigins: config.allowedReturnOrigins
|
|
646
|
+
});
|
|
647
|
+
const target2 = new URL(back, url.origin);
|
|
648
|
+
target2.searchParams.set("auth_error", "provider_unreachable");
|
|
649
|
+
const location2 = target2.origin === url.origin ? target2.pathname + target2.search : target2.toString();
|
|
650
|
+
return new Response(null, { status: 302, headers: { location: location2 } });
|
|
651
|
+
}
|
|
338
652
|
const { verifier, challenge } = await createPkcePair();
|
|
339
|
-
const
|
|
653
|
+
const retry = url.searchParams.get("retry") === "1";
|
|
654
|
+
const state = createStateValue() + (retry ? RETRY_MARK : "");
|
|
340
655
|
const target = new URL(authorization);
|
|
341
656
|
target.searchParams.set("response_type", "code");
|
|
342
657
|
target.searchParams.set("client_id", config.clientId);
|
|
@@ -345,6 +660,8 @@ function oauthRoutes(config) {
|
|
|
345
660
|
target.searchParams.set("state", state);
|
|
346
661
|
target.searchParams.set("code_challenge", challenge);
|
|
347
662
|
target.searchParams.set("code_challenge_method", "S256");
|
|
663
|
+
const prompt = url.searchParams.get("prompt");
|
|
664
|
+
if (prompt && PROMPTS.has(prompt)) target.searchParams.set("prompt", prompt);
|
|
348
665
|
const pendingCookie = await cookie.write(
|
|
349
666
|
{
|
|
350
667
|
accessToken: "",
|
|
@@ -357,7 +674,8 @@ function oauthRoutes(config) {
|
|
|
357
674
|
fallback: "",
|
|
358
675
|
origin: url.origin,
|
|
359
676
|
allowedOrigins: config.allowedReturnOrigins
|
|
360
|
-
}) || void 0
|
|
677
|
+
}) || void 0,
|
|
678
|
+
silent: prompt === "none" || void 0
|
|
361
679
|
}
|
|
362
680
|
},
|
|
363
681
|
// Ten minutes: long enough to sign in, short enough that an abandoned attempt
|
|
@@ -376,12 +694,43 @@ function oauthRoutes(config) {
|
|
|
376
694
|
const current = await session();
|
|
377
695
|
const pending = current?.pending;
|
|
378
696
|
const error = url.searchParams.get("error");
|
|
379
|
-
if (error)
|
|
697
|
+
if (error) {
|
|
698
|
+
const ours = !!pending && timingSafeEqual(pending.state, url.searchParams.get("state") ?? "");
|
|
699
|
+
let location2 = safeReturnTo(ours ? pending.returnTo ?? config.defaultReturnTo : config.defaultReturnTo, {
|
|
700
|
+
fallback: "/",
|
|
701
|
+
origin: url.origin,
|
|
702
|
+
allowedOrigins: config.allowedReturnOrigins
|
|
703
|
+
});
|
|
704
|
+
const headers = new Headers();
|
|
705
|
+
headers.append("set-cookie", cookie.clear());
|
|
706
|
+
if (ours && pending.silent && SILENT_REFUSALS.has(error)) {
|
|
707
|
+
headers.append("set-cookie", checkedCookie(config, sessionStateFrom(url)).header);
|
|
708
|
+
} else {
|
|
709
|
+
const target = new URL(location2, url.origin);
|
|
710
|
+
target.searchParams.set("auth_error", error);
|
|
711
|
+
location2 = target.origin === url.origin ? target.pathname + target.search : target.toString();
|
|
712
|
+
}
|
|
713
|
+
headers.set("location", location2);
|
|
714
|
+
return new Response(null, { status: 302, headers });
|
|
715
|
+
}
|
|
380
716
|
const code = url.searchParams.get("code");
|
|
381
717
|
const state = url.searchParams.get("state");
|
|
382
718
|
if (!code) return json({ error: "no_code" }, 400);
|
|
383
719
|
if (!pending || !state || !timingSafeEqual(pending.state, state)) {
|
|
384
|
-
|
|
720
|
+
const home = safeReturnTo(config.defaultReturnTo, { fallback: "/", origin: url.origin, allowedOrigins: config.allowedReturnOrigins });
|
|
721
|
+
if (current?.accessToken) return new Response(null, { status: 302, headers: { location: home } });
|
|
722
|
+
if (!state?.endsWith(RETRY_MARK)) {
|
|
723
|
+
return new Response(null, { status: 302, headers: { location: `${base}/login?retry=1` } });
|
|
724
|
+
}
|
|
725
|
+
const target = new URL(home, url.origin);
|
|
726
|
+
target.searchParams.set("auth_error", "state_mismatch");
|
|
727
|
+
return new Response(null, {
|
|
728
|
+
status: 302,
|
|
729
|
+
headers: {
|
|
730
|
+
location: target.origin === url.origin ? target.pathname + target.search : target.toString(),
|
|
731
|
+
"set-cookie": cookie.clear()
|
|
732
|
+
}
|
|
733
|
+
});
|
|
385
734
|
}
|
|
386
735
|
const key = config.dpop ? await createDpopKey(config.dpopAlgorithm, { extractable: true }) : null;
|
|
387
736
|
const payload = await tokenRequest(
|
|
@@ -399,7 +748,8 @@ function oauthRoutes(config) {
|
|
|
399
748
|
idToken: typeof payload.id_token === "string" ? payload.id_token : void 0,
|
|
400
749
|
expiresAt: expiryFrom(payload.expires_in),
|
|
401
750
|
tokenType: typeof payload.token_type === "string" ? payload.token_type : void 0,
|
|
402
|
-
dpop: key ? await exportDpopKey(key) : void 0
|
|
751
|
+
dpop: key ? await exportDpopKey(key) : void 0,
|
|
752
|
+
sessionState: sessionStateFrom(url)
|
|
403
753
|
});
|
|
404
754
|
return new Response(null, {
|
|
405
755
|
status: 302,
|
|
@@ -419,22 +769,9 @@ function oauthRoutes(config) {
|
|
|
419
769
|
if (route === "/refresh") {
|
|
420
770
|
const current = await session();
|
|
421
771
|
if (!current?.refreshToken) return json({ error: "no_session" }, 401);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
key
|
|
426
|
-
);
|
|
427
|
-
const refreshed = await cookie.write({
|
|
428
|
-
accessToken: String(payload.access_token ?? ""),
|
|
429
|
-
// A provider that rotates refresh tokens sends a new one; one that does not
|
|
430
|
-
// expects the old one to keep working.
|
|
431
|
-
refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
|
|
432
|
-
idToken: current.idToken,
|
|
433
|
-
expiresAt: expiryFrom(payload.expires_in),
|
|
434
|
-
tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
|
|
435
|
-
dpop: current.dpop
|
|
436
|
-
});
|
|
437
|
-
return json({ ok: true }, 200, { "set-cookie": refreshed });
|
|
772
|
+
const renewed = await renew(current, tokenRequest);
|
|
773
|
+
if (!renewed) return json({ error: "refresh_refused" }, 401, { "set-cookie": cookie.clear() });
|
|
774
|
+
return json({ ok: true }, 200, { "set-cookie": await cookie.write(renewed) });
|
|
438
775
|
}
|
|
439
776
|
if (route === "/me") {
|
|
440
777
|
const current = await session();
|
|
@@ -446,6 +783,13 @@ function oauthRoutes(config) {
|
|
|
446
783
|
const claims = await response.json();
|
|
447
784
|
return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
|
|
448
785
|
}
|
|
786
|
+
if (route === "/session-check") {
|
|
787
|
+
const { checkSession } = await resolve().catch(() => ({ checkSession: void 0 }));
|
|
788
|
+
const current = await session();
|
|
789
|
+
const checked = cookieValue(request, checkedCookie(config).name);
|
|
790
|
+
const state = current?.accessToken ? current.sessionState : checked !== "1" ? checked : void 0;
|
|
791
|
+
return json({ frame: checkSession ?? null, clientId: config.clientId, state: state ?? null });
|
|
792
|
+
}
|
|
449
793
|
if (route === "/revoke") {
|
|
450
794
|
const current = await session();
|
|
451
795
|
const hint = url.searchParams.get("hint") === "access_token" ? "access_token" : "refresh_token";
|
|
@@ -468,31 +812,33 @@ function oauthRoutes(config) {
|
|
|
468
812
|
const { endSession } = await resolve().catch(() => ({ endSession: void 0 }));
|
|
469
813
|
const { revocation } = await resolve().catch(() => ({ revocation: void 0 }));
|
|
470
814
|
if (revocation && current?.refreshToken) {
|
|
471
|
-
const
|
|
815
|
+
const headers2 = {
|
|
472
816
|
"content-type": "application/x-www-form-urlencoded"
|
|
473
817
|
};
|
|
474
818
|
const form = new URLSearchParams({
|
|
475
819
|
token: current.refreshToken,
|
|
476
820
|
token_type_hint: "refresh_token"
|
|
477
821
|
});
|
|
478
|
-
await applyClientAuth(clientAuth, revocation,
|
|
479
|
-
await fetch(revocation, { method: "POST", headers, body: form.toString() }).catch(
|
|
822
|
+
await applyClientAuth(clientAuth, revocation, headers2, form);
|
|
823
|
+
await fetch(revocation, { method: "POST", headers: headers2, body: form.toString() }).catch(
|
|
480
824
|
() => void 0
|
|
481
825
|
);
|
|
482
826
|
}
|
|
483
827
|
const cleared = cookie.clear();
|
|
828
|
+
const headers = new Headers();
|
|
829
|
+
headers.append("set-cookie", cleared);
|
|
830
|
+
headers.append("set-cookie", checkedCookie(config).header);
|
|
484
831
|
if (endSession && current?.idToken) {
|
|
485
832
|
const target = new URL(endSession);
|
|
486
833
|
target.searchParams.set("id_token_hint", current.idToken);
|
|
487
834
|
if (config.defaultReturnTo) {
|
|
488
835
|
target.searchParams.set("post_logout_redirect_uri", config.defaultReturnTo);
|
|
489
836
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
headers: { location: target.toString(), "set-cookie": cleared }
|
|
493
|
-
});
|
|
837
|
+
headers.set("location", target.toString());
|
|
838
|
+
return new Response(null, { status: 302, headers });
|
|
494
839
|
}
|
|
495
|
-
|
|
840
|
+
headers.set("content-type", "application/json");
|
|
841
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
|
|
496
842
|
}
|
|
497
843
|
return next();
|
|
498
844
|
};
|
|
@@ -503,6 +849,7 @@ function sessionLocals(config) {
|
|
|
503
849
|
name: config.cookieName,
|
|
504
850
|
secure: config.secureCookie
|
|
505
851
|
});
|
|
852
|
+
const { tokenRequest } = tokenClient(config);
|
|
506
853
|
const ttl = config.userCacheMs ?? 3e4;
|
|
507
854
|
const cache2 = /* @__PURE__ */ new Map();
|
|
508
855
|
const remember = (token, user) => {
|
|
@@ -517,21 +864,56 @@ function sessionLocals(config) {
|
|
|
517
864
|
return async function middleware(request, next) {
|
|
518
865
|
const { getRequestLocals } = await import("@fluixi/server");
|
|
519
866
|
const locals = getRequestLocals(request);
|
|
520
|
-
|
|
521
|
-
|
|
867
|
+
let session = await cookie.read(request);
|
|
868
|
+
let setCookie;
|
|
869
|
+
const respond = async (response) => {
|
|
870
|
+
if (!setCookie) return response;
|
|
871
|
+
const copy = new Response(response.body, response);
|
|
872
|
+
copy.headers.append("set-cookie", setCookie);
|
|
873
|
+
return copy;
|
|
874
|
+
};
|
|
875
|
+
const renewOrDrop = async (current2) => {
|
|
876
|
+
const renewed = current2.refreshToken ? await renew(current2, tokenRequest) : null;
|
|
877
|
+
setCookie = renewed ? await cookie.write(renewed) : cookie.clear();
|
|
878
|
+
return renewed;
|
|
879
|
+
};
|
|
880
|
+
if (session?.accessToken && expired(session)) {
|
|
881
|
+
session = await renewOrDrop(session).catch(() => session);
|
|
882
|
+
}
|
|
883
|
+
const signedOut = async () => {
|
|
884
|
+
const redirect = signInRedirect(request, config);
|
|
885
|
+
if (redirect) return respond(redirect);
|
|
522
886
|
locals.auth = { user: null, session: null };
|
|
523
|
-
return next();
|
|
887
|
+
return respond(await next());
|
|
888
|
+
};
|
|
889
|
+
if (!session?.accessToken) return signedOut();
|
|
890
|
+
if (await endedAtProvider(config, session)) {
|
|
891
|
+
setCookie = cookie.clear();
|
|
892
|
+
session = null;
|
|
893
|
+
return signedOut();
|
|
524
894
|
}
|
|
525
|
-
const
|
|
895
|
+
const live = session;
|
|
896
|
+
const fetchAs = (input, init) => sendAs(live, input, init);
|
|
897
|
+
const cached = cache2.get(live.accessToken);
|
|
526
898
|
if (cached && cached.until > Date.now()) {
|
|
527
|
-
locals.auth = { user: cached.user, session: { expiresAt:
|
|
528
|
-
return next();
|
|
899
|
+
locals.auth = { user: cached.user, session: { expiresAt: live.expiresAt }, fetch: fetchAs };
|
|
900
|
+
return respond(await next());
|
|
529
901
|
}
|
|
530
902
|
let user = null;
|
|
903
|
+
let unreachable = false;
|
|
531
904
|
try {
|
|
532
905
|
const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
|
|
533
906
|
if (endpoints?.userinfo) {
|
|
534
|
-
|
|
907
|
+
let response = await withToken(endpoints.userinfo, session);
|
|
908
|
+
if (response.status === 401 && !setCookie) {
|
|
909
|
+
session = await renewOrDrop(session).catch(() => session);
|
|
910
|
+
if (!session) return signedOut();
|
|
911
|
+
response = await withToken(endpoints.userinfo, session);
|
|
912
|
+
}
|
|
913
|
+
if (response.status === 401) {
|
|
914
|
+
setCookie = cookie.clear();
|
|
915
|
+
return signedOut();
|
|
916
|
+
}
|
|
535
917
|
if (response.ok) {
|
|
536
918
|
const claims = await response.json();
|
|
537
919
|
user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
|
|
@@ -539,16 +921,27 @@ function sessionLocals(config) {
|
|
|
539
921
|
}
|
|
540
922
|
}
|
|
541
923
|
} catch {
|
|
924
|
+
unreachable = true;
|
|
542
925
|
}
|
|
543
|
-
|
|
544
|
-
|
|
926
|
+
if (!session) return signedOut();
|
|
927
|
+
const current = session;
|
|
928
|
+
const fetchCurrent = (input, init) => sendAs(current, input, init);
|
|
929
|
+
locals.auth = user ? { user, session: { expiresAt: current.expiresAt }, fetch: fetchCurrent } : { user: null, session: null, ...unreachable ? { unreachable: true } : {} };
|
|
930
|
+
return respond(await next());
|
|
545
931
|
};
|
|
546
932
|
}
|
|
933
|
+
async function requestAuth(request) {
|
|
934
|
+
const { getLocals, getRequestLocals } = await import("@fluixi/server");
|
|
935
|
+
const locals = request ? getRequestLocals(request) : getLocals();
|
|
936
|
+
return locals.auth ?? { user: null, session: null };
|
|
937
|
+
}
|
|
547
938
|
function authServerModule(config) {
|
|
548
939
|
return defineModule({ name: "oauth2-server", middleware: [oauthRoutes(config), sessionLocals(config)] });
|
|
549
940
|
}
|
|
550
941
|
export {
|
|
551
942
|
authServerModule,
|
|
943
|
+
memoryLogouts,
|
|
552
944
|
oauthRoutes,
|
|
945
|
+
requestAuth,
|
|
553
946
|
sessionLocals
|
|
554
947
|
};
|