@fluixi/oauth2 1.0.0-alpha.3 → 1.0.0-alpha.5
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/{browser-QQDMUXPB.mjs → browser-QTUJRR3J.mjs} +2 -2
- package/dist/browser.cjs +4 -2
- package/dist/browser.mjs +4 -2
- package/dist/{chunk-4GSQS5YG.mjs → chunk-DCBOCQLJ.mjs} +1 -1
- package/dist/{chunk-IZ3BKW3J.mjs → chunk-UDYQMLN5.mjs} +1 -1
- package/dist/{dpop-VQWMNGQD.mjs → dpop-AUWYBMS3.mjs} +26 -6
- package/dist/{dpop-NS7NKOWM.mjs → dpop-YPGCGL7S.mjs} +22 -2
- package/dist/dpop-store.cjs +4 -2
- package/dist/dpop-store.mjs +4 -2
- package/dist/dpop.cjs +22 -2
- package/dist/dpop.d.ts +18 -1
- package/dist/dpop.d.ts.map +1 -1
- package/dist/dpop.js +23 -2
- package/dist/dpop.mjs +22 -2
- package/dist/index.cjs +5 -3
- package/dist/index.mjs +8 -6
- package/dist/server.cjs +133 -25
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +68 -17
- package/dist/server.mjs +135 -27
- package/dist/service-client.cjs +1 -1
- package/dist/service-client.mjs +3 -3
- package/dist/service.cjs +4 -2
- package/dist/service.d.ts +2 -4
- package/dist/service.d.ts.map +1 -1
- package/dist/service.mjs +4 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -12
- package/dist/verify.cjs +24 -3
- package/dist/verify.mjs +6 -6
- package/package.json +15 -15
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAgCA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAgCA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5E;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA8DD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,IA+DjD,SAAS,OAAO,EAChB,MAAM,MAAM,OAAO,CAAC,QAAQ,CAAC,KAC5B,OAAO,CAAC,QAAQ,CAAC,CAmNrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,IA8BnD,SAAS,OAAO,EAChB,MAAM,MAAM,OAAO,CAAC,QAAQ,CAAC,KAC5B,OAAO,CAAC,QAAQ,CAAC,CAwCrB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAErE"}
|
package/dist/server.js
CHANGED
|
@@ -34,7 +34,46 @@ import { applyClientAuth, } from './client-auth.js';
|
|
|
34
34
|
import { discover } from './discovery.js';
|
|
35
35
|
import { createPkcePair, createStateValue, expiryFrom, timingSafeEqual } from './pkce.js';
|
|
36
36
|
import { OAuthError } from './types.js';
|
|
37
|
+
import { createDpopKey, exportDpopKey, importDpopKey, nonceFrom, proofFor } from './dpop.js';
|
|
37
38
|
import { defineModule } from '@fluixi/start/app';
|
|
39
|
+
/**
|
|
40
|
+
* Nonces providers asked for, per origin, shared by the routes and `sessionLocals`. A nonce
|
|
41
|
+
* is the server's to set (RFC 9449 §8, §9), so the latest one it sent is the one to use.
|
|
42
|
+
*/
|
|
43
|
+
const dpopNonces = new Map();
|
|
44
|
+
/** Put a proof for this request on `headers`, with the nonce this origin last asked for. */
|
|
45
|
+
async function prove(headers, key, method, url, accessToken) {
|
|
46
|
+
headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
|
|
47
|
+
}
|
|
48
|
+
/** Record a nonce the server sent. True when it refused the request with one not seen before. */
|
|
49
|
+
function learnNonce(url, response) {
|
|
50
|
+
const offered = nonceFrom(response);
|
|
51
|
+
if (!offered)
|
|
52
|
+
return false;
|
|
53
|
+
const origin = new URL(url).origin;
|
|
54
|
+
const fresh = offered !== dpopNonces.get(origin);
|
|
55
|
+
dpopNonces.set(origin, offered);
|
|
56
|
+
return fresh && !response.ok;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A request carrying the session's access token: the `DPoP` scheme and a proof bound to the
|
|
60
|
+
* token when the provider bound it, `Bearer` otherwise.
|
|
61
|
+
*/
|
|
62
|
+
async function withToken(url, session) {
|
|
63
|
+
const key = session.dpop && session.tokenType?.toLowerCase() === 'dpop' ? await importDpopKey(session.dpop) : null;
|
|
64
|
+
const send = async () => {
|
|
65
|
+
const headers = {
|
|
66
|
+
authorization: `${key ? 'DPoP' : 'Bearer'} ${session.accessToken}`,
|
|
67
|
+
};
|
|
68
|
+
if (key)
|
|
69
|
+
await prove(headers, key, 'GET', url, session.accessToken);
|
|
70
|
+
return fetch(url, { headers });
|
|
71
|
+
};
|
|
72
|
+
let response = await send();
|
|
73
|
+
if (key && learnNonce(url, response))
|
|
74
|
+
response = await send();
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
38
77
|
/**
|
|
39
78
|
* The middleware.
|
|
40
79
|
*
|
|
@@ -73,14 +112,23 @@ export function oauthRoutes(config) {
|
|
|
73
112
|
keyId: config.keyId,
|
|
74
113
|
assertionAlgorithm: config.assertionAlgorithm,
|
|
75
114
|
};
|
|
76
|
-
async function tokenRequest(body) {
|
|
115
|
+
async function tokenRequest(body, key = null) {
|
|
77
116
|
const { token } = await resolve();
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
// Built afresh for a retry: a `private_key_jwt` assertion carries a `jti`, and sending
|
|
118
|
+
// the same one twice can be refused as a replay.
|
|
119
|
+
const attempt = async () => {
|
|
120
|
+
const headers = {
|
|
121
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
122
|
+
};
|
|
123
|
+
const form = new URLSearchParams(body);
|
|
124
|
+
await applyClientAuth(clientAuth, token, headers, form);
|
|
125
|
+
if (key)
|
|
126
|
+
await prove(headers, key, 'POST', token);
|
|
127
|
+
return fetch(token, { method: 'POST', headers, body: form.toString() });
|
|
80
128
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
129
|
+
let response = await attempt();
|
|
130
|
+
if (key && learnNonce(token, response))
|
|
131
|
+
response = await attempt();
|
|
84
132
|
const payload = (await response.json().catch(() => ({})));
|
|
85
133
|
if (!response.ok) {
|
|
86
134
|
throw new OAuthError(String(payload.error ?? 'token_request_failed'), String(payload.error ?? 'token_request_failed'));
|
|
@@ -147,17 +195,23 @@ export function oauthRoutes(config) {
|
|
|
147
195
|
if (!pending || !state || !timingSafeEqual(pending.state, state)) {
|
|
148
196
|
return json({ error: 'state_mismatch' }, 400);
|
|
149
197
|
}
|
|
198
|
+
// The token is bound here, at the exchange, so this is where the session's key is made.
|
|
199
|
+
const key = config.dpop
|
|
200
|
+
? await createDpopKey(config.dpopAlgorithm, { extractable: true })
|
|
201
|
+
: null;
|
|
150
202
|
const payload = await tokenRequest({
|
|
151
203
|
grant_type: 'authorization_code',
|
|
152
204
|
code,
|
|
153
205
|
redirect_uri: config.redirectUri,
|
|
154
206
|
code_verifier: pending.verifier,
|
|
155
|
-
});
|
|
207
|
+
}, key);
|
|
156
208
|
const signedIn = await cookie.write({
|
|
157
209
|
accessToken: String(payload.access_token ?? ''),
|
|
158
210
|
refreshToken: typeof payload.refresh_token === 'string' ? payload.refresh_token : undefined,
|
|
159
211
|
idToken: typeof payload.id_token === 'string' ? payload.id_token : undefined,
|
|
160
212
|
expiresAt: expiryFrom(payload.expires_in),
|
|
213
|
+
tokenType: typeof payload.token_type === 'string' ? payload.token_type : undefined,
|
|
214
|
+
dpop: key ? await exportDpopKey(key) : undefined,
|
|
161
215
|
});
|
|
162
216
|
return new Response(null, {
|
|
163
217
|
status: 302,
|
|
@@ -178,10 +232,9 @@ export function oauthRoutes(config) {
|
|
|
178
232
|
const current = await session();
|
|
179
233
|
if (!current?.refreshToken)
|
|
180
234
|
return json({ error: 'no_session' }, 401);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
235
|
+
// The key that obtained the refresh token, or the provider refuses the refresh.
|
|
236
|
+
const key = current.dpop ? await importDpopKey(current.dpop) : null;
|
|
237
|
+
const payload = await tokenRequest({ grant_type: 'refresh_token', refresh_token: current.refreshToken }, key);
|
|
185
238
|
const refreshed = await cookie.write({
|
|
186
239
|
accessToken: String(payload.access_token ?? ''),
|
|
187
240
|
// A provider that rotates refresh tokens sends a new one; one that does not
|
|
@@ -191,6 +244,8 @@ export function oauthRoutes(config) {
|
|
|
191
244
|
: current.refreshToken,
|
|
192
245
|
idToken: current.idToken,
|
|
193
246
|
expiresAt: expiryFrom(payload.expires_in),
|
|
247
|
+
tokenType: typeof payload.token_type === 'string' ? payload.token_type : current.tokenType,
|
|
248
|
+
dpop: current.dpop,
|
|
194
249
|
});
|
|
195
250
|
return json({ ok: true }, 200, { 'set-cookie': refreshed });
|
|
196
251
|
}
|
|
@@ -201,9 +256,7 @@ export function oauthRoutes(config) {
|
|
|
201
256
|
const { userinfo } = await resolve();
|
|
202
257
|
if (!userinfo)
|
|
203
258
|
return json({ error: 'no_userinfo' }, 501);
|
|
204
|
-
const response = await
|
|
205
|
-
headers: { authorization: `Bearer ${current.accessToken}` },
|
|
206
|
-
});
|
|
259
|
+
const response = await withToken(userinfo, current);
|
|
207
260
|
if (!response.ok)
|
|
208
261
|
return json({ error: 'userinfo_failed' }, response.status);
|
|
209
262
|
const claims = (await response.json());
|
|
@@ -339,9 +392,7 @@ export function sessionLocals(config) {
|
|
|
339
392
|
try {
|
|
340
393
|
const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
|
|
341
394
|
if (endpoints?.userinfo) {
|
|
342
|
-
const response = await
|
|
343
|
-
headers: { authorization: `Bearer ${session.accessToken}` },
|
|
344
|
-
});
|
|
395
|
+
const response = await withToken(endpoints.userinfo, session);
|
|
345
396
|
if (response.ok) {
|
|
346
397
|
const claims = (await response.json());
|
|
347
398
|
user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
|
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
2
|
-
import "./chunk-
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import "./chunk-DCBOCQLJ.mjs";
|
|
3
3
|
|
|
4
4
|
// src/server.ts
|
|
5
5
|
import { createSessionCookie } from "@fluixi/session/server";
|
|
@@ -53,7 +53,7 @@ function resolveMethod(auth) {
|
|
|
53
53
|
}
|
|
54
54
|
async function loadJose() {
|
|
55
55
|
try {
|
|
56
|
-
return await import("./browser-
|
|
56
|
+
return await import("./browser-QTUJRR3J.mjs");
|
|
57
57
|
} catch {
|
|
58
58
|
throw new OAuthError(
|
|
59
59
|
"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.",
|
|
@@ -173,8 +173,106 @@ function expiryFrom(expiresIn, now = Date.now()) {
|
|
|
173
173
|
return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// src/dpop.ts
|
|
177
|
+
async function loadJose2() {
|
|
178
|
+
try {
|
|
179
|
+
return await import("./browser-QTUJRR3J.mjs");
|
|
180
|
+
} catch {
|
|
181
|
+
throw new OAuthError(
|
|
182
|
+
"DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
|
|
183
|
+
"jose_missing"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
188
|
+
const jose = await loadJose2();
|
|
189
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
190
|
+
extractable: options.extractable ?? false
|
|
191
|
+
});
|
|
192
|
+
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
193
|
+
return {
|
|
194
|
+
privateKey,
|
|
195
|
+
publicJwk,
|
|
196
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
197
|
+
alg
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function exportDpopKey(key) {
|
|
201
|
+
const jose = await loadJose2();
|
|
202
|
+
return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
|
|
203
|
+
}
|
|
204
|
+
var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
205
|
+
async function importDpopKey(stored) {
|
|
206
|
+
const jose = await loadJose2();
|
|
207
|
+
const privateKey = await jose.importJWK(stored.jwk, stored.alg);
|
|
208
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
209
|
+
return {
|
|
210
|
+
privateKey,
|
|
211
|
+
publicJwk,
|
|
212
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
213
|
+
alg: stored.alg
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async function proofFor(key, options) {
|
|
217
|
+
const jose = await loadJose2();
|
|
218
|
+
const htu = (() => {
|
|
219
|
+
try {
|
|
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(/=+$/, "");
|
|
241
|
+
}
|
|
242
|
+
if (options.nonce) claims.nonce = options.nonce;
|
|
243
|
+
return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
|
|
244
|
+
}
|
|
245
|
+
function nonceFrom(response) {
|
|
246
|
+
return response.headers.get("dpop-nonce");
|
|
247
|
+
}
|
|
248
|
+
|
|
176
249
|
// src/server.ts
|
|
177
250
|
import { defineModule } from "@fluixi/start/app";
|
|
251
|
+
var dpopNonces = /* @__PURE__ */ new Map();
|
|
252
|
+
async function prove(headers, key, method, url, accessToken) {
|
|
253
|
+
headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
|
|
254
|
+
}
|
|
255
|
+
function learnNonce(url, response) {
|
|
256
|
+
const offered = nonceFrom(response);
|
|
257
|
+
if (!offered) return false;
|
|
258
|
+
const origin = new URL(url).origin;
|
|
259
|
+
const fresh = offered !== dpopNonces.get(origin);
|
|
260
|
+
dpopNonces.set(origin, offered);
|
|
261
|
+
return fresh && !response.ok;
|
|
262
|
+
}
|
|
263
|
+
async function withToken(url, session) {
|
|
264
|
+
const key = session.dpop && session.tokenType?.toLowerCase() === "dpop" ? await importDpopKey(session.dpop) : null;
|
|
265
|
+
const send = async () => {
|
|
266
|
+
const headers = {
|
|
267
|
+
authorization: `${key ? "DPoP" : "Bearer"} ${session.accessToken}`
|
|
268
|
+
};
|
|
269
|
+
if (key) await prove(headers, key, "GET", url, session.accessToken);
|
|
270
|
+
return fetch(url, { headers });
|
|
271
|
+
};
|
|
272
|
+
let response = await send();
|
|
273
|
+
if (key && learnNonce(url, response)) response = await send();
|
|
274
|
+
return response;
|
|
275
|
+
}
|
|
178
276
|
function oauthRoutes(config) {
|
|
179
277
|
const base = (config.basePath ?? "/auth").replace(/\/$/, "");
|
|
180
278
|
const scopes = config.scopes ?? ["openid", "profile", "email"];
|
|
@@ -206,14 +304,19 @@ function oauthRoutes(config) {
|
|
|
206
304
|
keyId: config.keyId,
|
|
207
305
|
assertionAlgorithm: config.assertionAlgorithm
|
|
208
306
|
};
|
|
209
|
-
async function tokenRequest(body) {
|
|
307
|
+
async function tokenRequest(body, key = null) {
|
|
210
308
|
const { token } = await resolve();
|
|
211
|
-
const
|
|
212
|
-
|
|
309
|
+
const attempt = async () => {
|
|
310
|
+
const headers = {
|
|
311
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
312
|
+
};
|
|
313
|
+
const form = new URLSearchParams(body);
|
|
314
|
+
await applyClientAuth(clientAuth, token, headers, form);
|
|
315
|
+
if (key) await prove(headers, key, "POST", token);
|
|
316
|
+
return fetch(token, { method: "POST", headers, body: form.toString() });
|
|
213
317
|
};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const response = await fetch(token, { method: "POST", headers, body: form.toString() });
|
|
318
|
+
let response = await attempt();
|
|
319
|
+
if (key && learnNonce(token, response)) response = await attempt();
|
|
217
320
|
const payload = await response.json().catch(() => ({}));
|
|
218
321
|
if (!response.ok) {
|
|
219
322
|
throw new OAuthError(
|
|
@@ -280,17 +383,23 @@ function oauthRoutes(config) {
|
|
|
280
383
|
if (!pending || !state || !timingSafeEqual(pending.state, state)) {
|
|
281
384
|
return json({ error: "state_mismatch" }, 400);
|
|
282
385
|
}
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
386
|
+
const key = config.dpop ? await createDpopKey(config.dpopAlgorithm, { extractable: true }) : null;
|
|
387
|
+
const payload = await tokenRequest(
|
|
388
|
+
{
|
|
389
|
+
grant_type: "authorization_code",
|
|
390
|
+
code,
|
|
391
|
+
redirect_uri: config.redirectUri,
|
|
392
|
+
code_verifier: pending.verifier
|
|
393
|
+
},
|
|
394
|
+
key
|
|
395
|
+
);
|
|
289
396
|
const signedIn = await cookie.write({
|
|
290
397
|
accessToken: String(payload.access_token ?? ""),
|
|
291
398
|
refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : void 0,
|
|
292
399
|
idToken: typeof payload.id_token === "string" ? payload.id_token : void 0,
|
|
293
|
-
expiresAt: expiryFrom(payload.expires_in)
|
|
400
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
401
|
+
tokenType: typeof payload.token_type === "string" ? payload.token_type : void 0,
|
|
402
|
+
dpop: key ? await exportDpopKey(key) : void 0
|
|
294
403
|
});
|
|
295
404
|
return new Response(null, {
|
|
296
405
|
status: 302,
|
|
@@ -310,17 +419,20 @@ function oauthRoutes(config) {
|
|
|
310
419
|
if (route === "/refresh") {
|
|
311
420
|
const current = await session();
|
|
312
421
|
if (!current?.refreshToken) return json({ error: "no_session" }, 401);
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
refresh_token: current.refreshToken
|
|
316
|
-
|
|
422
|
+
const key = current.dpop ? await importDpopKey(current.dpop) : null;
|
|
423
|
+
const payload = await tokenRequest(
|
|
424
|
+
{ grant_type: "refresh_token", refresh_token: current.refreshToken },
|
|
425
|
+
key
|
|
426
|
+
);
|
|
317
427
|
const refreshed = await cookie.write({
|
|
318
428
|
accessToken: String(payload.access_token ?? ""),
|
|
319
429
|
// A provider that rotates refresh tokens sends a new one; one that does not
|
|
320
430
|
// expects the old one to keep working.
|
|
321
431
|
refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
|
|
322
432
|
idToken: current.idToken,
|
|
323
|
-
expiresAt: expiryFrom(payload.expires_in)
|
|
433
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
434
|
+
tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
|
|
435
|
+
dpop: current.dpop
|
|
324
436
|
});
|
|
325
437
|
return json({ ok: true }, 200, { "set-cookie": refreshed });
|
|
326
438
|
}
|
|
@@ -329,9 +441,7 @@ function oauthRoutes(config) {
|
|
|
329
441
|
if (!current?.accessToken) return json({ error: "no_session" }, 401);
|
|
330
442
|
const { userinfo } = await resolve();
|
|
331
443
|
if (!userinfo) return json({ error: "no_userinfo" }, 501);
|
|
332
|
-
const response = await
|
|
333
|
-
headers: { authorization: `Bearer ${current.accessToken}` }
|
|
334
|
-
});
|
|
444
|
+
const response = await withToken(userinfo, current);
|
|
335
445
|
if (!response.ok) return json({ error: "userinfo_failed" }, response.status);
|
|
336
446
|
const claims = await response.json();
|
|
337
447
|
return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
|
|
@@ -421,9 +531,7 @@ function sessionLocals(config) {
|
|
|
421
531
|
try {
|
|
422
532
|
const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
|
|
423
533
|
if (endpoints?.userinfo) {
|
|
424
|
-
const response = await
|
|
425
|
-
headers: { authorization: `Bearer ${session.accessToken}` }
|
|
426
|
-
});
|
|
534
|
+
const response = await withToken(endpoints.userinfo, session);
|
|
427
535
|
if (response.ok) {
|
|
428
536
|
const claims = await response.json();
|
|
429
537
|
user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
|
package/dist/service-client.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist/service-client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
2
|
-
import "./chunk-
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.5 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import "./chunk-DCBOCQLJ.mjs";
|
|
3
3
|
|
|
4
4
|
// src/types.ts
|
|
5
5
|
var OAuthError = class extends Error {
|
|
@@ -42,7 +42,7 @@ function assertUsable(auth, who) {
|
|
|
42
42
|
}
|
|
43
43
|
async function loadJose() {
|
|
44
44
|
try {
|
|
45
|
-
return await import("./browser-
|
|
45
|
+
return await import("./browser-QTUJRR3J.mjs");
|
|
46
46
|
} catch {
|
|
47
47
|
throw new OAuthError(
|
|
48
48
|
"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.",
|
package/dist/service.cjs
CHANGED
|
@@ -191,9 +191,11 @@ async function loadJose2() {
|
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
async function createDpopKey(alg = "ES256") {
|
|
194
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
195
195
|
const jose = await loadJose2();
|
|
196
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
196
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
197
|
+
extractable: options.extractable ?? false
|
|
198
|
+
});
|
|
197
199
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
198
200
|
return {
|
|
199
201
|
privateKey,
|
package/dist/service.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* else lines up directly: `fetchUser`, `refresh`, `logout` and `getExpiry`.
|
|
14
14
|
*/
|
|
15
15
|
import { type AccessControl, type Session } from '@fluixi/session';
|
|
16
|
-
import { createAuthHooks } from '@fluixi/auth/client';
|
|
16
|
+
import { createAuthHooks, type AuthHooksOptions } from '@fluixi/auth/client';
|
|
17
17
|
import type { AuthClient, AuthConfig, CallbackParams, LoginOptions } from './types.js';
|
|
18
18
|
/** The credential a redirect flow produces. */
|
|
19
19
|
export type AuthCredentials = CallbackParams;
|
|
@@ -55,9 +55,7 @@ export interface AuthServiceOptions<U, P = unknown, Q = (p: P) => boolean> {
|
|
|
55
55
|
* client-side navigation; leave it out and it assigns `location`, which reloads the
|
|
56
56
|
* document and discards the reactive tree.
|
|
57
57
|
*/
|
|
58
|
-
navigate?:
|
|
59
|
-
handler: () => (to: string, options?: unknown) => void;
|
|
60
|
-
};
|
|
58
|
+
navigate?: AuthHooksOptions['navigate'];
|
|
61
59
|
/** Where a signed-out visitor is sent. Default `/login`. */
|
|
62
60
|
loginPath?: string;
|
|
63
61
|
/** Where a signed-in visitor is sent from a guest-only page. Default `/`. */
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/service.mjs
CHANGED
|
@@ -158,9 +158,11 @@ async function loadJose2() {
|
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
-
async function createDpopKey(alg = "ES256") {
|
|
161
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
162
162
|
const jose = await loadJose2();
|
|
163
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
163
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
164
|
+
extractable: options.extractable ?? false
|
|
165
|
+
});
|
|
164
166
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
165
167
|
return {
|
|
166
168
|
privateKey,
|