@fluixi/oauth2 1.0.0-alpha.3 → 1.0.0-alpha.4
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-ZDO2EHPH.mjs} +2 -2
- package/dist/browser.cjs +4 -2
- package/dist/browser.mjs +4 -2
- package/dist/{chunk-4GSQS5YG.mjs → chunk-5NSLG6E3.mjs} +1 -1
- package/dist/{chunk-IZ3BKW3J.mjs → chunk-UEAYG4TW.mjs} +1 -1
- package/dist/{dpop-VQWMNGQD.mjs → dpop-CU42DUAJ.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.mjs +4 -2
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/verify.cjs +24 -3
- package/dist/verify.mjs +6 -6
- package/package.json +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.4 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
import {
|
|
3
3
|
__export
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-5NSLG6E3.mjs";
|
|
5
5
|
|
|
6
6
|
// ../../node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/browser/runtime/webcrypto.js
|
|
7
7
|
var webcrypto_default = crypto;
|
package/dist/browser.cjs
CHANGED
|
@@ -165,9 +165,11 @@ async function loadJose2() {
|
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
-
async function createDpopKey(alg = "ES256") {
|
|
168
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
169
169
|
const jose = await loadJose2();
|
|
170
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
170
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
171
|
+
extractable: options.extractable ?? false
|
|
172
|
+
});
|
|
171
173
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
172
174
|
return {
|
|
173
175
|
privateKey,
|
package/dist/browser.mjs
CHANGED
|
@@ -131,9 +131,11 @@ async function loadJose2() {
|
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
async function createDpopKey(alg = "ES256") {
|
|
134
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
135
135
|
const jose = await loadJose2();
|
|
136
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
136
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
137
|
+
extractable: options.extractable ?? false
|
|
138
|
+
});
|
|
137
139
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
138
140
|
return {
|
|
139
141
|
privateKey,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.4 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __export = (target, all) => {
|
|
4
4
|
for (var name in all)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.4 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
import {
|
|
3
3
|
OAuthError
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-UEAYG4TW.mjs";
|
|
5
|
+
import "./chunk-5NSLG6E3.mjs";
|
|
6
6
|
|
|
7
7
|
// src/dpop.ts
|
|
8
8
|
async function loadJose() {
|
|
9
9
|
try {
|
|
10
|
-
return await import("./browser-
|
|
10
|
+
return await import("./browser-ZDO2EHPH.mjs");
|
|
11
11
|
} catch {
|
|
12
12
|
throw new OAuthError(
|
|
13
13
|
"DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
|
|
@@ -15,9 +15,11 @@ async function loadJose() {
|
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
async function createDpopKey(alg = "ES256") {
|
|
18
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
19
19
|
const jose = await loadJose();
|
|
20
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
20
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
21
|
+
extractable: options.extractable ?? false
|
|
22
|
+
});
|
|
21
23
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
22
24
|
return {
|
|
23
25
|
privateKey,
|
|
@@ -26,6 +28,22 @@ async function createDpopKey(alg = "ES256") {
|
|
|
26
28
|
alg
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
async function exportDpopKey(key) {
|
|
32
|
+
const jose = await loadJose();
|
|
33
|
+
return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
|
|
34
|
+
}
|
|
35
|
+
var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
36
|
+
async function importDpopKey(stored) {
|
|
37
|
+
const jose = await loadJose();
|
|
38
|
+
const privateKey = await jose.importJWK(stored.jwk, stored.alg);
|
|
39
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
40
|
+
return {
|
|
41
|
+
privateKey,
|
|
42
|
+
publicJwk,
|
|
43
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
44
|
+
alg: stored.alg
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
async function thumbprintOf(jwk) {
|
|
30
48
|
const jose = await loadJose();
|
|
31
49
|
return jose.calculateJwkThumbprint(jwk, "sha256");
|
|
@@ -73,6 +91,8 @@ function boundKeyOf(claims) {
|
|
|
73
91
|
export {
|
|
74
92
|
boundKeyOf,
|
|
75
93
|
createDpopKey,
|
|
94
|
+
exportDpopKey,
|
|
95
|
+
importDpopKey,
|
|
76
96
|
nonceFrom,
|
|
77
97
|
proofFor,
|
|
78
98
|
thumbprintOf,
|
|
@@ -14,9 +14,11 @@ async function loadJose() {
|
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
async function createDpopKey(alg = "ES256") {
|
|
17
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
18
18
|
const jose = await loadJose();
|
|
19
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
19
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
20
|
+
extractable: options.extractable ?? false
|
|
21
|
+
});
|
|
20
22
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
21
23
|
return {
|
|
22
24
|
privateKey,
|
|
@@ -25,6 +27,22 @@ async function createDpopKey(alg = "ES256") {
|
|
|
25
27
|
alg
|
|
26
28
|
};
|
|
27
29
|
}
|
|
30
|
+
async function exportDpopKey(key) {
|
|
31
|
+
const jose = await loadJose();
|
|
32
|
+
return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
|
|
33
|
+
}
|
|
34
|
+
var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
35
|
+
async function importDpopKey(stored) {
|
|
36
|
+
const jose = await loadJose();
|
|
37
|
+
const privateKey = await jose.importJWK(stored.jwk, stored.alg);
|
|
38
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
39
|
+
return {
|
|
40
|
+
privateKey,
|
|
41
|
+
publicJwk,
|
|
42
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
43
|
+
alg: stored.alg
|
|
44
|
+
};
|
|
45
|
+
}
|
|
28
46
|
async function thumbprintOf(jwk) {
|
|
29
47
|
const jose = await loadJose();
|
|
30
48
|
return jose.calculateJwkThumbprint(jwk, "sha256");
|
|
@@ -72,6 +90,8 @@ function boundKeyOf(claims) {
|
|
|
72
90
|
export {
|
|
73
91
|
boundKeyOf,
|
|
74
92
|
createDpopKey,
|
|
93
|
+
exportDpopKey,
|
|
94
|
+
importDpopKey,
|
|
75
95
|
nonceFrom,
|
|
76
96
|
proofFor,
|
|
77
97
|
thumbprintOf,
|
package/dist/dpop-store.cjs
CHANGED
|
@@ -56,9 +56,11 @@ async function loadJose() {
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
async function createDpopKey(alg = "ES256") {
|
|
59
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
60
60
|
const jose = await loadJose();
|
|
61
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
61
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
62
|
+
extractable: options.extractable ?? false
|
|
63
|
+
});
|
|
62
64
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
63
65
|
return {
|
|
64
66
|
privateKey,
|
package/dist/dpop-store.mjs
CHANGED
|
@@ -21,9 +21,11 @@ async function loadJose() {
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
async function createDpopKey(alg = "ES256") {
|
|
24
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
25
25
|
const jose = await loadJose();
|
|
26
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
26
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
27
|
+
extractable: options.extractable ?? false
|
|
28
|
+
});
|
|
27
29
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
28
30
|
return {
|
|
29
31
|
privateKey,
|
package/dist/dpop.cjs
CHANGED
|
@@ -32,6 +32,8 @@ var dpop_exports = {};
|
|
|
32
32
|
__export(dpop_exports, {
|
|
33
33
|
boundKeyOf: () => boundKeyOf,
|
|
34
34
|
createDpopKey: () => createDpopKey,
|
|
35
|
+
exportDpopKey: () => exportDpopKey,
|
|
36
|
+
importDpopKey: () => importDpopKey,
|
|
35
37
|
nonceFrom: () => nonceFrom,
|
|
36
38
|
proofFor: () => proofFor,
|
|
37
39
|
thumbprintOf: () => thumbprintOf,
|
|
@@ -60,9 +62,11 @@ async function loadJose() {
|
|
|
60
62
|
);
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
|
-
async function createDpopKey(alg = "ES256") {
|
|
65
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
64
66
|
const jose = await loadJose();
|
|
65
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
67
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
68
|
+
extractable: options.extractable ?? false
|
|
69
|
+
});
|
|
66
70
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
67
71
|
return {
|
|
68
72
|
privateKey,
|
|
@@ -71,6 +75,22 @@ async function createDpopKey(alg = "ES256") {
|
|
|
71
75
|
alg
|
|
72
76
|
};
|
|
73
77
|
}
|
|
78
|
+
async function exportDpopKey(key) {
|
|
79
|
+
const jose = await loadJose();
|
|
80
|
+
return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
|
|
81
|
+
}
|
|
82
|
+
var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
83
|
+
async function importDpopKey(stored) {
|
|
84
|
+
const jose = await loadJose();
|
|
85
|
+
const privateKey = await jose.importJWK(stored.jwk, stored.alg);
|
|
86
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
87
|
+
return {
|
|
88
|
+
privateKey,
|
|
89
|
+
publicJwk,
|
|
90
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
91
|
+
alg: stored.alg
|
|
92
|
+
};
|
|
93
|
+
}
|
|
74
94
|
async function thumbprintOf(jwk) {
|
|
75
95
|
const jose = await loadJose();
|
|
76
96
|
return jose.calculateJwkThumbprint(jwk, "sha256");
|
package/dist/dpop.d.ts
CHANGED
|
@@ -25,7 +25,24 @@ export interface DpopKey {
|
|
|
25
25
|
* attacker who can run script can use the key while they are there, and cannot take it
|
|
26
26
|
* away to use later.
|
|
27
27
|
*/
|
|
28
|
-
export declare function createDpopKey(alg?: string
|
|
28
|
+
export declare function createDpopKey(alg?: string, options?: {
|
|
29
|
+
extractable?: boolean;
|
|
30
|
+
}): Promise<DpopKey>;
|
|
31
|
+
/**
|
|
32
|
+
* A key as a server session stores it: the private JWK and its algorithm.
|
|
33
|
+
*
|
|
34
|
+
* Only for a key held by a server, inside an encrypted httpOnly cookie. There it is bytes
|
|
35
|
+
* no page can read, and the session has nowhere else to keep it between requests. A
|
|
36
|
+
* browser key stays non-extractable in IndexedDB instead.
|
|
37
|
+
*/
|
|
38
|
+
export interface StoredDpopKey {
|
|
39
|
+
jwk: Record<string, unknown>;
|
|
40
|
+
alg: string;
|
|
41
|
+
}
|
|
42
|
+
/** Export a key made with `extractable: true`, for a server session to store. */
|
|
43
|
+
export declare function exportDpopKey(key: DpopKey): Promise<StoredDpopKey>;
|
|
44
|
+
/** Rebuild a key a server session stored. The thumbprint comes out the same, which is the point. */
|
|
45
|
+
export declare function importDpopKey(stored: StoredDpopKey): Promise<DpopKey>;
|
|
29
46
|
/** The thumbprint of a public JWK, for comparing against a token's `cnf.jkt`. */
|
|
30
47
|
export declare function thumbprintOf(jwk: Record<string, unknown>): Promise<string>;
|
|
31
48
|
export interface ProofOptions {
|
package/dist/dpop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../src/dpop.ts"],"names":[],"mappings":"AAkBA,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,iEAAiE;IACjE,UAAU,EAAE,SAAS,CAAC;IACtB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,GAAG,EAAE,MAAM,CAAC;CACb;AAcD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,
|
|
1
|
+
{"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../src/dpop.ts"],"names":[],"mappings":"AAkBA,6EAA6E;AAC7E,MAAM,WAAW,OAAO;IACtB,iEAAiE;IACjE,UAAU,EAAE,SAAS,CAAC;IACtB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,GAAG,EAAE,MAAM,CAAC;CACb;AAcD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,GAAG,SAAU,EACb,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,OAAO,CAAC,OAAO,CAAC,CAalB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAGxE;AAID,oGAAoG;AACpG,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAW3E;AAED,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhF;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAsCnF;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,IAAI,CAE3D;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAGvE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAGzE"}
|
package/dist/dpop.js
CHANGED
|
@@ -35,9 +35,11 @@ async function loadJose() {
|
|
|
35
35
|
* attacker who can run script can use the key while they are there, and cannot take it
|
|
36
36
|
* away to use later.
|
|
37
37
|
*/
|
|
38
|
-
export async function createDpopKey(alg = 'ES256') {
|
|
38
|
+
export async function createDpopKey(alg = 'ES256', options = {}) {
|
|
39
39
|
const jose = await loadJose();
|
|
40
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
40
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
41
|
+
extractable: options.extractable ?? false,
|
|
42
|
+
});
|
|
41
43
|
const publicJwk = { ...(await jose.exportJWK(publicKey)) };
|
|
42
44
|
return {
|
|
43
45
|
privateKey: privateKey,
|
|
@@ -46,6 +48,25 @@ export async function createDpopKey(alg = 'ES256') {
|
|
|
46
48
|
alg,
|
|
47
49
|
};
|
|
48
50
|
}
|
|
51
|
+
/** Export a key made with `extractable: true`, for a server session to store. */
|
|
52
|
+
export async function exportDpopKey(key) {
|
|
53
|
+
const jose = await loadJose();
|
|
54
|
+
return { jwk: { ...(await jose.exportJWK(key.privateKey)) }, alg: key.alg };
|
|
55
|
+
}
|
|
56
|
+
const PRIVATE_MEMBERS = new Set(['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth']);
|
|
57
|
+
/** Rebuild a key a server session stored. The thumbprint comes out the same, which is the point. */
|
|
58
|
+
export async function importDpopKey(stored) {
|
|
59
|
+
const jose = await loadJose();
|
|
60
|
+
const privateKey = (await jose.importJWK(stored.jwk, stored.alg));
|
|
61
|
+
// The public half is the private JWK without its private members (RFC 7518 §6.2.2, §6.3.2).
|
|
62
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
63
|
+
return {
|
|
64
|
+
privateKey,
|
|
65
|
+
publicJwk,
|
|
66
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, 'sha256'),
|
|
67
|
+
alg: stored.alg,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
49
70
|
/** The thumbprint of a public JWK, for comparing against a token's `cnf.jkt`. */
|
|
50
71
|
export async function thumbprintOf(jwk) {
|
|
51
72
|
const jose = await loadJose();
|
package/dist/dpop.mjs
CHANGED
|
@@ -21,9 +21,11 @@ async function loadJose() {
|
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
async function createDpopKey(alg = "ES256") {
|
|
24
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
25
25
|
const jose = await loadJose();
|
|
26
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
26
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
27
|
+
extractable: options.extractable ?? false
|
|
28
|
+
});
|
|
27
29
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
28
30
|
return {
|
|
29
31
|
privateKey,
|
|
@@ -32,6 +34,22 @@ async function createDpopKey(alg = "ES256") {
|
|
|
32
34
|
alg
|
|
33
35
|
};
|
|
34
36
|
}
|
|
37
|
+
async function exportDpopKey(key) {
|
|
38
|
+
const jose = await loadJose();
|
|
39
|
+
return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
|
|
40
|
+
}
|
|
41
|
+
var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
42
|
+
async function importDpopKey(stored) {
|
|
43
|
+
const jose = await loadJose();
|
|
44
|
+
const privateKey = await jose.importJWK(stored.jwk, stored.alg);
|
|
45
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
46
|
+
return {
|
|
47
|
+
privateKey,
|
|
48
|
+
publicJwk,
|
|
49
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
50
|
+
alg: stored.alg
|
|
51
|
+
};
|
|
52
|
+
}
|
|
35
53
|
async function thumbprintOf(jwk) {
|
|
36
54
|
const jose = await loadJose();
|
|
37
55
|
return jose.calculateJwkThumbprint(jwk, "sha256");
|
|
@@ -79,6 +97,8 @@ function boundKeyOf(claims) {
|
|
|
79
97
|
export {
|
|
80
98
|
boundKeyOf,
|
|
81
99
|
createDpopKey,
|
|
100
|
+
exportDpopKey,
|
|
101
|
+
importDpopKey,
|
|
82
102
|
nonceFrom,
|
|
83
103
|
proofFor,
|
|
84
104
|
thumbprintOf,
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.4 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -213,9 +213,11 @@ async function loadJose2() {
|
|
|
213
213
|
);
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
-
async function createDpopKey(alg = "ES256") {
|
|
216
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
217
217
|
const jose = await loadJose2();
|
|
218
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
218
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
219
|
+
extractable: options.extractable ?? false
|
|
220
|
+
});
|
|
219
221
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
220
222
|
return {
|
|
221
223
|
privateKey,
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
2
|
-
import "./chunk-
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.4 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import "./chunk-5NSLG6E3.mjs";
|
|
3
3
|
|
|
4
4
|
// src/service.ts
|
|
5
5
|
import { createSession } from "@fluixi/session";
|
|
@@ -111,7 +111,7 @@ function resolveMethod(auth) {
|
|
|
111
111
|
}
|
|
112
112
|
async function loadJose() {
|
|
113
113
|
try {
|
|
114
|
-
return await import("./browser-
|
|
114
|
+
return await import("./browser-ZDO2EHPH.mjs");
|
|
115
115
|
} catch {
|
|
116
116
|
throw new OAuthError(
|
|
117
117
|
"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.",
|
|
@@ -151,7 +151,7 @@ async function applyClientAuth(auth, endpoint, headers, form) {
|
|
|
151
151
|
// src/dpop.ts
|
|
152
152
|
async function loadJose2() {
|
|
153
153
|
try {
|
|
154
|
-
return await import("./browser-
|
|
154
|
+
return await import("./browser-ZDO2EHPH.mjs");
|
|
155
155
|
} catch {
|
|
156
156
|
throw new OAuthError(
|
|
157
157
|
"DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
|
|
@@ -159,9 +159,11 @@ async function loadJose2() {
|
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
async function createDpopKey(alg = "ES256") {
|
|
162
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
163
163
|
const jose = await loadJose2();
|
|
164
|
-
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
164
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
165
|
+
extractable: options.extractable ?? false
|
|
166
|
+
});
|
|
165
167
|
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
166
168
|
return {
|
|
167
169
|
privateKey,
|
package/dist/server.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.4 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -207,8 +207,106 @@ function expiryFrom(expiresIn, now = Date.now()) {
|
|
|
207
207
|
return Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// src/dpop.ts
|
|
211
|
+
async function loadJose2() {
|
|
212
|
+
try {
|
|
213
|
+
return await import("jose");
|
|
214
|
+
} catch {
|
|
215
|
+
throw new OAuthError(
|
|
216
|
+
"DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
|
|
217
|
+
"jose_missing"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
222
|
+
const jose = await loadJose2();
|
|
223
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
224
|
+
extractable: options.extractable ?? false
|
|
225
|
+
});
|
|
226
|
+
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
227
|
+
return {
|
|
228
|
+
privateKey,
|
|
229
|
+
publicJwk,
|
|
230
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
231
|
+
alg
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function exportDpopKey(key) {
|
|
235
|
+
const jose = await loadJose2();
|
|
236
|
+
return { jwk: { ...await jose.exportJWK(key.privateKey) }, alg: key.alg };
|
|
237
|
+
}
|
|
238
|
+
var PRIVATE_MEMBERS = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth"]);
|
|
239
|
+
async function importDpopKey(stored) {
|
|
240
|
+
const jose = await loadJose2();
|
|
241
|
+
const privateKey = await jose.importJWK(stored.jwk, stored.alg);
|
|
242
|
+
const publicJwk = Object.fromEntries(Object.entries(stored.jwk).filter(([k]) => !PRIVATE_MEMBERS.has(k)));
|
|
243
|
+
return {
|
|
244
|
+
privateKey,
|
|
245
|
+
publicJwk,
|
|
246
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
247
|
+
alg: stored.alg
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function proofFor(key, options) {
|
|
251
|
+
const jose = await loadJose2();
|
|
252
|
+
const htu = (() => {
|
|
253
|
+
try {
|
|
254
|
+
const url = new URL(options.url);
|
|
255
|
+
url.search = "";
|
|
256
|
+
url.hash = "";
|
|
257
|
+
return url.toString();
|
|
258
|
+
} catch {
|
|
259
|
+
return options.url;
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
const claims = {
|
|
263
|
+
htm: options.method.toUpperCase(),
|
|
264
|
+
htu,
|
|
265
|
+
jti: crypto.randomUUID()
|
|
266
|
+
};
|
|
267
|
+
if (options.accessToken) {
|
|
268
|
+
const digest = await crypto.subtle.digest(
|
|
269
|
+
"SHA-256",
|
|
270
|
+
new TextEncoder().encode(options.accessToken)
|
|
271
|
+
);
|
|
272
|
+
let binary = "";
|
|
273
|
+
for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
|
|
274
|
+
claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
275
|
+
}
|
|
276
|
+
if (options.nonce) claims.nonce = options.nonce;
|
|
277
|
+
return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
|
|
278
|
+
}
|
|
279
|
+
function nonceFrom(response) {
|
|
280
|
+
return response.headers.get("dpop-nonce");
|
|
281
|
+
}
|
|
282
|
+
|
|
210
283
|
// src/server.ts
|
|
211
284
|
var import_app = require("@fluixi/start/app");
|
|
285
|
+
var dpopNonces = /* @__PURE__ */ new Map();
|
|
286
|
+
async function prove(headers, key, method, url, accessToken) {
|
|
287
|
+
headers.dpop = await proofFor(key, { method, url, accessToken, nonce: dpopNonces.get(new URL(url).origin) });
|
|
288
|
+
}
|
|
289
|
+
function learnNonce(url, response) {
|
|
290
|
+
const offered = nonceFrom(response);
|
|
291
|
+
if (!offered) return false;
|
|
292
|
+
const origin = new URL(url).origin;
|
|
293
|
+
const fresh = offered !== dpopNonces.get(origin);
|
|
294
|
+
dpopNonces.set(origin, offered);
|
|
295
|
+
return fresh && !response.ok;
|
|
296
|
+
}
|
|
297
|
+
async function withToken(url, session) {
|
|
298
|
+
const key = session.dpop && session.tokenType?.toLowerCase() === "dpop" ? await importDpopKey(session.dpop) : null;
|
|
299
|
+
const send = async () => {
|
|
300
|
+
const headers = {
|
|
301
|
+
authorization: `${key ? "DPoP" : "Bearer"} ${session.accessToken}`
|
|
302
|
+
};
|
|
303
|
+
if (key) await prove(headers, key, "GET", url, session.accessToken);
|
|
304
|
+
return fetch(url, { headers });
|
|
305
|
+
};
|
|
306
|
+
let response = await send();
|
|
307
|
+
if (key && learnNonce(url, response)) response = await send();
|
|
308
|
+
return response;
|
|
309
|
+
}
|
|
212
310
|
function oauthRoutes(config) {
|
|
213
311
|
const base = (config.basePath ?? "/auth").replace(/\/$/, "");
|
|
214
312
|
const scopes = config.scopes ?? ["openid", "profile", "email"];
|
|
@@ -240,14 +338,19 @@ function oauthRoutes(config) {
|
|
|
240
338
|
keyId: config.keyId,
|
|
241
339
|
assertionAlgorithm: config.assertionAlgorithm
|
|
242
340
|
};
|
|
243
|
-
async function tokenRequest(body) {
|
|
341
|
+
async function tokenRequest(body, key = null) {
|
|
244
342
|
const { token } = await resolve();
|
|
245
|
-
const
|
|
246
|
-
|
|
343
|
+
const attempt = async () => {
|
|
344
|
+
const headers = {
|
|
345
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
346
|
+
};
|
|
347
|
+
const form = new URLSearchParams(body);
|
|
348
|
+
await applyClientAuth(clientAuth, token, headers, form);
|
|
349
|
+
if (key) await prove(headers, key, "POST", token);
|
|
350
|
+
return fetch(token, { method: "POST", headers, body: form.toString() });
|
|
247
351
|
};
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const response = await fetch(token, { method: "POST", headers, body: form.toString() });
|
|
352
|
+
let response = await attempt();
|
|
353
|
+
if (key && learnNonce(token, response)) response = await attempt();
|
|
251
354
|
const payload = await response.json().catch(() => ({}));
|
|
252
355
|
if (!response.ok) {
|
|
253
356
|
throw new OAuthError(
|
|
@@ -314,17 +417,23 @@ function oauthRoutes(config) {
|
|
|
314
417
|
if (!pending || !state || !timingSafeEqual(pending.state, state)) {
|
|
315
418
|
return json({ error: "state_mismatch" }, 400);
|
|
316
419
|
}
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
420
|
+
const key = config.dpop ? await createDpopKey(config.dpopAlgorithm, { extractable: true }) : null;
|
|
421
|
+
const payload = await tokenRequest(
|
|
422
|
+
{
|
|
423
|
+
grant_type: "authorization_code",
|
|
424
|
+
code,
|
|
425
|
+
redirect_uri: config.redirectUri,
|
|
426
|
+
code_verifier: pending.verifier
|
|
427
|
+
},
|
|
428
|
+
key
|
|
429
|
+
);
|
|
323
430
|
const signedIn = await cookie.write({
|
|
324
431
|
accessToken: String(payload.access_token ?? ""),
|
|
325
432
|
refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : void 0,
|
|
326
433
|
idToken: typeof payload.id_token === "string" ? payload.id_token : void 0,
|
|
327
|
-
expiresAt: expiryFrom(payload.expires_in)
|
|
434
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
435
|
+
tokenType: typeof payload.token_type === "string" ? payload.token_type : void 0,
|
|
436
|
+
dpop: key ? await exportDpopKey(key) : void 0
|
|
328
437
|
});
|
|
329
438
|
return new Response(null, {
|
|
330
439
|
status: 302,
|
|
@@ -344,17 +453,20 @@ function oauthRoutes(config) {
|
|
|
344
453
|
if (route === "/refresh") {
|
|
345
454
|
const current = await session();
|
|
346
455
|
if (!current?.refreshToken) return json({ error: "no_session" }, 401);
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
refresh_token: current.refreshToken
|
|
350
|
-
|
|
456
|
+
const key = current.dpop ? await importDpopKey(current.dpop) : null;
|
|
457
|
+
const payload = await tokenRequest(
|
|
458
|
+
{ grant_type: "refresh_token", refresh_token: current.refreshToken },
|
|
459
|
+
key
|
|
460
|
+
);
|
|
351
461
|
const refreshed = await cookie.write({
|
|
352
462
|
accessToken: String(payload.access_token ?? ""),
|
|
353
463
|
// A provider that rotates refresh tokens sends a new one; one that does not
|
|
354
464
|
// expects the old one to keep working.
|
|
355
465
|
refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : current.refreshToken,
|
|
356
466
|
idToken: current.idToken,
|
|
357
|
-
expiresAt: expiryFrom(payload.expires_in)
|
|
467
|
+
expiresAt: expiryFrom(payload.expires_in),
|
|
468
|
+
tokenType: typeof payload.token_type === "string" ? payload.token_type : current.tokenType,
|
|
469
|
+
dpop: current.dpop
|
|
358
470
|
});
|
|
359
471
|
return json({ ok: true }, 200, { "set-cookie": refreshed });
|
|
360
472
|
}
|
|
@@ -363,9 +475,7 @@ function oauthRoutes(config) {
|
|
|
363
475
|
if (!current?.accessToken) return json({ error: "no_session" }, 401);
|
|
364
476
|
const { userinfo } = await resolve();
|
|
365
477
|
if (!userinfo) return json({ error: "no_userinfo" }, 501);
|
|
366
|
-
const response = await
|
|
367
|
-
headers: { authorization: `Bearer ${current.accessToken}` }
|
|
368
|
-
});
|
|
478
|
+
const response = await withToken(userinfo, current);
|
|
369
479
|
if (!response.ok) return json({ error: "userinfo_failed" }, response.status);
|
|
370
480
|
const claims = await response.json();
|
|
371
481
|
return json(config.mapUser ? config.mapUser(claims, current.accessToken) : claims);
|
|
@@ -455,9 +565,7 @@ function sessionLocals(config) {
|
|
|
455
565
|
try {
|
|
456
566
|
const endpoints = config.endpoints ?? (config.issuer ? await discover(config.issuer) : null);
|
|
457
567
|
if (endpoints?.userinfo) {
|
|
458
|
-
const response = await
|
|
459
|
-
headers: { authorization: `Bearer ${session.accessToken}` }
|
|
460
|
-
});
|
|
568
|
+
const response = await withToken(endpoints.userinfo, session);
|
|
461
569
|
if (response.ok) {
|
|
462
570
|
const claims = await response.json();
|
|
463
571
|
user = config.mapUser ? config.mapUser(claims, session.accessToken) : claims;
|
package/dist/server.d.ts
CHANGED
|
@@ -53,6 +53,17 @@ export interface OAuthRoutesConfig {
|
|
|
53
53
|
userCacheMs?: number;
|
|
54
54
|
/** Map the provider's userinfo onto the app's own shape. */
|
|
55
55
|
mapUser?: (claims: Record<string, unknown>, accessToken: string) => unknown;
|
|
56
|
+
/**
|
|
57
|
+
* Bind tokens to a key with DPoP (RFC 9449), which a provider may require: an Okta org
|
|
58
|
+
* that mandates it refuses a server-mode client without it.
|
|
59
|
+
*
|
|
60
|
+
* The key is made per session at the code exchange and kept in the encrypted session
|
|
61
|
+
* cookie, since a refresh token is bound to the key that obtained it and a refresh with
|
|
62
|
+
* another key is refused. Needs `jose`.
|
|
63
|
+
*/
|
|
64
|
+
dpop?: boolean;
|
|
65
|
+
/** Default `ES256`. Must be in the provider's `dpop_signing_alg_values_supported`. */
|
|
66
|
+
dpopAlgorithm?: string;
|
|
56
67
|
}
|
|
57
68
|
/**
|
|
58
69
|
* The middleware.
|