@fluixi/oauth2 1.0.0-alpha.1 → 1.0.0-alpha.100
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/LICENSE +1 -1
- 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-ONMHX3Z3.mjs → browser-TV25RBLT.mjs} +2 -2
- package/dist/browser.cjs +10 -3
- package/dist/browser.mjs +10 -3
- package/dist/callback.cjs +8 -7
- package/dist/callback.js +1 -1
- package/dist/callback.mjs +3 -2
- package/dist/{chunk-4NSOGPLM.mjs → chunk-2B3I72DV.mjs} +1 -1
- package/dist/chunk-KOP5Z4LB.mjs +108 -0
- package/dist/{chunk-25WE6UWI.mjs → chunk-KTGVSZE6.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-O3HWR7AH.mjs +22 -0
- package/dist/{dpop-LVPWWADA.mjs → dpop-SGWG3K2I.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 +135 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.mjs +138 -13
- package/dist/module.cjs +44 -0
- package/dist/module.d.ts +13 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +22 -0
- package/dist/module.mjs +23 -0
- package/dist/server.cjs +755 -80
- package/dist/server.d.ts +91 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +518 -65
- package/dist/server.mjs +638 -82
- package/dist/service-client.cjs +7 -2
- package/dist/service-client.mjs +9 -4
- package/dist/service.cjs +116 -7
- package/dist/service.d.ts +2 -18
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +48 -5
- package/dist/service.mjs +116 -7
- 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/tokens.cjs +1 -1
- package/dist/tokens.d.ts +2 -2
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +1 -1
- package/dist/tokens.mjs +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +12 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -12
- package/dist/verify.cjs +30 -4
- package/dist/verify.mjs +12 -7
- package/package.json +22 -16
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OAuthError
|
|
3
|
+
} from "./chunk-OGSFNTKA.mjs";
|
|
4
|
+
import "./chunk-7P6ASYW6.mjs";
|
|
5
|
+
|
|
6
|
+
// src/discovery.ts
|
|
7
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8
|
+
function documentUrl(issuer) {
|
|
9
|
+
return `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
10
|
+
}
|
|
11
|
+
async function discover(issuer, fetcher = fetch) {
|
|
12
|
+
const cached = cache.get(issuer);
|
|
13
|
+
if (cached) return cached;
|
|
14
|
+
const pending = (async () => {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetcher(documentUrl(issuer));
|
|
18
|
+
} catch (cause) {
|
|
19
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
20
|
+
throw new OAuthError(
|
|
21
|
+
typeof window === "undefined" ? (
|
|
22
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
23
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
24
|
+
) : `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})`,
|
|
25
|
+
"discovery_unreachable"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new OAuthError(
|
|
30
|
+
`Discovery failed for ${issuer}: ${response.status}`,
|
|
31
|
+
"discovery_failed"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const doc = await response.json();
|
|
35
|
+
if (!doc.authorization_endpoint || !doc.token_endpoint) {
|
|
36
|
+
throw new OAuthError(
|
|
37
|
+
`${issuer} published a discovery document with no authorization or token endpoint.`,
|
|
38
|
+
"discovery_incomplete"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
authorization: doc.authorization_endpoint,
|
|
43
|
+
token: doc.token_endpoint,
|
|
44
|
+
userinfo: doc.userinfo_endpoint,
|
|
45
|
+
endSession: doc.end_session_endpoint,
|
|
46
|
+
jwks: doc.jwks_uri,
|
|
47
|
+
introspection: doc.introspection_endpoint,
|
|
48
|
+
revocation: doc.revocation_endpoint,
|
|
49
|
+
checkSession: doc.check_session_iframe,
|
|
50
|
+
issuer: doc.issuer ?? issuer
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
cache.set(issuer, pending);
|
|
54
|
+
pending.catch(() => cache.delete(issuer));
|
|
55
|
+
return pending;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/verify.ts
|
|
59
|
+
var TokenError = class extends OAuthError {
|
|
60
|
+
constructor(message, code = "invalid_token") {
|
|
61
|
+
super(message, code);
|
|
62
|
+
this.name = "TokenError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
function bearerOf(request) {
|
|
66
|
+
const header = request.headers.get("authorization");
|
|
67
|
+
if (!header) return null;
|
|
68
|
+
const match = header.match(/^(?:Bearer|DPoP)\s+(.+)$/i);
|
|
69
|
+
return match ? match[1].trim() : null;
|
|
70
|
+
}
|
|
71
|
+
async function loadJose() {
|
|
72
|
+
try {
|
|
73
|
+
return await import("./browser-FDUOEUM2.mjs");
|
|
74
|
+
} catch {
|
|
75
|
+
throw new OAuthError(
|
|
76
|
+
"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.",
|
|
77
|
+
"jose_missing"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function createTokenVerifier(options) {
|
|
82
|
+
const issuers = (Array.isArray(options.issuer) ? options.issuer : [options.issuer]).filter(Boolean);
|
|
83
|
+
if (!issuers.length) {
|
|
84
|
+
throw new Error("[fluixi/oauth2] createTokenVerifier needs an issuer to accept tokens from.");
|
|
85
|
+
}
|
|
86
|
+
const discoveryIssuer = issuers[0];
|
|
87
|
+
if (options.audience === void 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"[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."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (options.audience === false && options.authorizedParty === void 0) {
|
|
93
|
+
console.warn(
|
|
94
|
+
"[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."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const algorithms = options.algorithms ?? ["RS256"];
|
|
98
|
+
if (algorithms.some((alg) => !alg || alg.toLowerCase() === "none")) {
|
|
99
|
+
throw new Error("[fluixi/oauth2] `none` is not a signing algorithm and cannot be accepted.");
|
|
100
|
+
}
|
|
101
|
+
let keys = null;
|
|
102
|
+
async function keySet() {
|
|
103
|
+
if (keys) return keys;
|
|
104
|
+
const jose = await loadJose();
|
|
105
|
+
if (options.jwks) {
|
|
106
|
+
keys = jose.createLocalJWKSet(options.jwks);
|
|
107
|
+
return keys;
|
|
108
|
+
}
|
|
109
|
+
let uri = options.jwksUri;
|
|
110
|
+
if (!uri) {
|
|
111
|
+
const endpoints = await discover(discoveryIssuer);
|
|
112
|
+
uri = endpoints.jwks;
|
|
113
|
+
}
|
|
114
|
+
if (!uri) {
|
|
115
|
+
throw new OAuthError(
|
|
116
|
+
`${discoveryIssuer} published no jwks_uri. Give the verifier one directly.`,
|
|
117
|
+
"no_jwks"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
keys = jose.createRemoteJWKSet(new URL(uri));
|
|
121
|
+
return keys;
|
|
122
|
+
}
|
|
123
|
+
const checkProof = async (claims, token, request) => {
|
|
124
|
+
const jose = await loadJose();
|
|
125
|
+
const { boundKeyOf, thumbprintOf } = await import("./dpop-YPGCGL7S.mjs");
|
|
126
|
+
const bound = boundKeyOf(claims);
|
|
127
|
+
if (!bound) {
|
|
128
|
+
throw new TokenError(
|
|
129
|
+
"This token is not bound to a key, and this service requires binding.",
|
|
130
|
+
"token_not_bound"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const proof = request.headers.get("dpop");
|
|
134
|
+
if (!proof) throw new TokenError("The request carried no DPoP proof.", "missing_dpop_proof");
|
|
135
|
+
let payload;
|
|
136
|
+
let header;
|
|
137
|
+
try {
|
|
138
|
+
const result = await jose.jwtVerify(proof, jose.EmbeddedJWK, { typ: "dpop+jwt" });
|
|
139
|
+
payload = result.payload;
|
|
140
|
+
header = result.protectedHeader;
|
|
141
|
+
} catch (cause) {
|
|
142
|
+
throw new TokenError(
|
|
143
|
+
`The DPoP proof did not verify: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
144
|
+
"invalid_dpop_proof"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const presented = await thumbprintOf(header.jwk);
|
|
148
|
+
if (presented !== bound) {
|
|
149
|
+
throw new TokenError(
|
|
150
|
+
"The DPoP proof was signed with a different key from the one this token is bound to.",
|
|
151
|
+
"dpop_key_mismatch"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const url = new URL(request.url);
|
|
155
|
+
url.search = "";
|
|
156
|
+
url.hash = "";
|
|
157
|
+
if (payload.htm !== request.method.toUpperCase()) {
|
|
158
|
+
throw new TokenError("The DPoP proof was made for a different method.", "invalid_dpop_proof");
|
|
159
|
+
}
|
|
160
|
+
if (payload.htu !== url.toString()) {
|
|
161
|
+
throw new TokenError("The DPoP proof was made for a different URL.", "invalid_dpop_proof");
|
|
162
|
+
}
|
|
163
|
+
if (typeof payload.ath === "string") {
|
|
164
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
|
|
165
|
+
let binary = "";
|
|
166
|
+
for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
|
|
167
|
+
const expected = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
168
|
+
if (payload.ath !== expected) {
|
|
169
|
+
throw new TokenError("The DPoP proof was made for a different token.", "invalid_dpop_proof");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const age = Math.abs(Date.now() / 1e3 - payload.iat);
|
|
173
|
+
if (!Number.isFinite(age) || age > (options.dpopProofAgeSec ?? 10)) {
|
|
174
|
+
throw new TokenError("The DPoP proof is too old to accept.", "invalid_dpop_proof");
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const verify = async (token, request) => {
|
|
178
|
+
if (!token) throw new TokenError("No token to verify.", "no_token");
|
|
179
|
+
const jose = await loadJose();
|
|
180
|
+
let payload;
|
|
181
|
+
try {
|
|
182
|
+
({ payload } = await jose.jwtVerify(token, await keySet(), {
|
|
183
|
+
// jose takes a list and accepts any of them, which is what a provider naming
|
|
184
|
+
// itself two ways needs.
|
|
185
|
+
issuer: issuers,
|
|
186
|
+
...options.audience === false ? {} : { audience: options.audience },
|
|
187
|
+
algorithms,
|
|
188
|
+
clockTolerance: options.clockToleranceSec ?? 5
|
|
189
|
+
}));
|
|
190
|
+
} catch (cause) {
|
|
191
|
+
throw new TokenError(
|
|
192
|
+
`The token was not accepted: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (options.authorizedParty !== void 0) {
|
|
196
|
+
const allowed = Array.isArray(options.authorizedParty) ? options.authorizedParty : [options.authorizedParty];
|
|
197
|
+
const issuedTo = payload.azp ?? payload.client_id;
|
|
198
|
+
if (typeof issuedTo !== "string" || !allowed.includes(issuedTo)) {
|
|
199
|
+
throw new TokenError("The token was issued to a different client.", "invalid_azp");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (options.dpop) {
|
|
203
|
+
if (!request) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"[fluixi/oauth2] `dpop: true` needs the request to read its proof. Use `verify.fromRequest(request)` rather than verifying a bare token."
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
await checkProof(payload, token, request);
|
|
209
|
+
}
|
|
210
|
+
if (options.require) {
|
|
211
|
+
const verdict = options.require(payload);
|
|
212
|
+
if (verdict !== true) {
|
|
213
|
+
throw new TokenError(
|
|
214
|
+
typeof verdict === "string" ? verdict : "The token did not meet this service's requirements.",
|
|
215
|
+
"insufficient_claims"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return payload;
|
|
220
|
+
};
|
|
221
|
+
const verifier = verify;
|
|
222
|
+
verifier.bearerOf = bearerOf;
|
|
223
|
+
verifier.fromRequest = async (request) => {
|
|
224
|
+
const token = verifier.bearerOf(request);
|
|
225
|
+
if (!token) {
|
|
226
|
+
throw new TokenError("The request carried no bearer token.", "no_token");
|
|
227
|
+
}
|
|
228
|
+
return verify(token, request);
|
|
229
|
+
};
|
|
230
|
+
return verifier;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/backchannel.ts
|
|
234
|
+
var LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
235
|
+
function memoryLogouts(ttlMs = 14 * 24 * 36e5) {
|
|
236
|
+
const sids = /* @__PURE__ */ new Map();
|
|
237
|
+
const subs = /* @__PURE__ */ new Map();
|
|
238
|
+
const prune = (now) => {
|
|
239
|
+
for (const map of [sids, subs]) for (const [key, at] of map) if (now - at > ttlMs) map.delete(key);
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
add(entry) {
|
|
243
|
+
prune(Date.now());
|
|
244
|
+
if (entry.sid) sids.set(entry.sid, entry.at);
|
|
245
|
+
else if (entry.sub) subs.set(entry.sub, Math.max(entry.at, subs.get(entry.sub) ?? 0));
|
|
246
|
+
},
|
|
247
|
+
ended(session) {
|
|
248
|
+
if (session.sid && sids.has(session.sid)) return true;
|
|
249
|
+
const all = session.sub ? subs.get(session.sub) : void 0;
|
|
250
|
+
return all !== void 0 && session.since <= all;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function idTokenClaims(idToken) {
|
|
255
|
+
if (!idToken) return {};
|
|
256
|
+
try {
|
|
257
|
+
const part = idToken.split(".")[1] ?? "";
|
|
258
|
+
const json = atob(part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "="));
|
|
259
|
+
const claims = JSON.parse(json);
|
|
260
|
+
return {
|
|
261
|
+
sid: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
262
|
+
sub: typeof claims.sub === "string" ? claims.sub : void 0,
|
|
263
|
+
iat: typeof claims.iat === "number" ? claims.iat : void 0
|
|
264
|
+
};
|
|
265
|
+
} catch {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function headerOf(token) {
|
|
270
|
+
try {
|
|
271
|
+
const part = token.split(".")[0] ?? "";
|
|
272
|
+
return JSON.parse(atob(part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "=")));
|
|
273
|
+
} catch {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function logoutTokenVerifier(options) {
|
|
278
|
+
const verify = createTokenVerifier({
|
|
279
|
+
issuer: options.issuer,
|
|
280
|
+
audience: options.clientId,
|
|
281
|
+
jwksUri: options.jwksUri,
|
|
282
|
+
jwks: options.jwks,
|
|
283
|
+
require: (claims) => {
|
|
284
|
+
const events = claims.events;
|
|
285
|
+
if (!events || typeof events !== "object" || !(LOGOUT_EVENT in events)) return "no logout event";
|
|
286
|
+
if ("nonce" in claims) return "a logout token has no nonce";
|
|
287
|
+
if (typeof claims.sid !== "string" && typeof claims.sub !== "string") return "names no session and no subject";
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return async (token) => {
|
|
292
|
+
const typ = headerOf(token).typ;
|
|
293
|
+
if (typ !== void 0 && typ !== "logout+jwt") throw new Error("not a logout token");
|
|
294
|
+
const claims = await verify(token);
|
|
295
|
+
return {
|
|
296
|
+
sid: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
297
|
+
sub: typeof claims.sub === "string" ? claims.sub : void 0,
|
|
298
|
+
at: (typeof claims.iat === "number" ? claims.iat : Math.floor(Date.now() / 1e3)) * 1e3
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
LOGOUT_EVENT,
|
|
304
|
+
idTokenClaims,
|
|
305
|
+
logoutTokenVerifier,
|
|
306
|
+
memoryLogouts
|
|
307
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
import {
|
|
3
3
|
__export
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KTGVSZE6.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,
|
|
@@ -305,8 +307,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
305
307
|
try {
|
|
306
308
|
response = await fetcher(documentUrl(issuer));
|
|
307
309
|
} catch (cause) {
|
|
310
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
308
311
|
throw new OAuthError(
|
|
309
|
-
|
|
312
|
+
typeof window === "undefined" ? (
|
|
313
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
314
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
315
|
+
) : `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})`,
|
|
310
316
|
"discovery_unreachable"
|
|
311
317
|
);
|
|
312
318
|
}
|
|
@@ -331,6 +337,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
331
337
|
jwks: doc.jwks_uri,
|
|
332
338
|
introspection: doc.introspection_endpoint,
|
|
333
339
|
revocation: doc.revocation_endpoint,
|
|
340
|
+
checkSession: doc.check_session_iframe,
|
|
334
341
|
issuer: doc.issuer ?? issuer
|
|
335
342
|
};
|
|
336
343
|
})();
|
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,
|
|
@@ -271,8 +273,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
271
273
|
try {
|
|
272
274
|
response = await fetcher(documentUrl(issuer));
|
|
273
275
|
} catch (cause) {
|
|
276
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
274
277
|
throw new OAuthError(
|
|
275
|
-
|
|
278
|
+
typeof window === "undefined" ? (
|
|
279
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
280
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
281
|
+
) : `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})`,
|
|
276
282
|
"discovery_unreachable"
|
|
277
283
|
);
|
|
278
284
|
}
|
|
@@ -297,6 +303,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
297
303
|
jwks: doc.jwks_uri,
|
|
298
304
|
introspection: doc.introspection_endpoint,
|
|
299
305
|
revocation: doc.revocation_endpoint,
|
|
306
|
+
checkSession: doc.check_session_iframe,
|
|
300
307
|
issuer: doc.issuer ?? issuer
|
|
301
308
|
};
|
|
302
309
|
})();
|
package/dist/callback.cjs
CHANGED
|
@@ -25,15 +25,16 @@ __export(callback_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(callback_exports);
|
|
27
27
|
var import_core = require("@fluixi/core");
|
|
28
|
-
var
|
|
29
|
-
var import_di2 = require("@fluixi/
|
|
28
|
+
var import_signal2 = require("@fluixi/reactive/signal");
|
|
29
|
+
var import_di2 = require("@fluixi/di");
|
|
30
30
|
|
|
31
31
|
// src/tokens.ts
|
|
32
|
-
var import_di = require("@fluixi/
|
|
32
|
+
var import_di = require("@fluixi/di");
|
|
33
33
|
var AuthServiceToken = (0, import_di.injectionToken)("AuthService");
|
|
34
34
|
var AuthClientToken = (0, import_di.injectionToken)("AuthClient");
|
|
35
35
|
|
|
36
36
|
// src/service.ts
|
|
37
|
+
var import_signal = require("@fluixi/reactive/signal");
|
|
37
38
|
var import_session = require("@fluixi/session");
|
|
38
39
|
var import_client = require("@fluixi/auth/client");
|
|
39
40
|
function paramsFromUrl(url) {
|
|
@@ -55,10 +56,10 @@ function reasonFor(cause) {
|
|
|
55
56
|
}
|
|
56
57
|
var spent = /* @__PURE__ */ new Set();
|
|
57
58
|
function useOAuthCallback(options = {}) {
|
|
58
|
-
const stage = (0,
|
|
59
|
-
const error = (0,
|
|
60
|
-
const destination = (0,
|
|
61
|
-
const params = (0,
|
|
59
|
+
const stage = (0, import_signal2.signal)("waiting");
|
|
60
|
+
const error = (0, import_signal2.signal)(null);
|
|
61
|
+
const destination = (0, import_signal2.signal)(null);
|
|
62
|
+
const params = (0, import_signal2.signal)({});
|
|
62
63
|
let lastQuery = null;
|
|
63
64
|
let injected = null;
|
|
64
65
|
let injectionError = null;
|
package/dist/callback.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { onMount } from '@fluixi/core';
|
|
25
25
|
import { signal } from '@fluixi/reactive/signal';
|
|
26
|
-
import { inject } from '@fluixi/
|
|
26
|
+
import { inject } from '@fluixi/di';
|
|
27
27
|
import { AuthServiceToken } from './tokens.js';
|
|
28
28
|
import { paramsFromUrl } from './service.js';
|
|
29
29
|
/** The message for a failure, preferring the provider's own words. */
|
package/dist/callback.mjs
CHANGED
|
@@ -3,14 +3,15 @@ import "./chunk-7P6ASYW6.mjs";
|
|
|
3
3
|
// src/callback.ts
|
|
4
4
|
import { onMount } from "@fluixi/core";
|
|
5
5
|
import { signal } from "@fluixi/reactive/signal";
|
|
6
|
-
import { inject } from "@fluixi/
|
|
6
|
+
import { inject } from "@fluixi/di";
|
|
7
7
|
|
|
8
8
|
// src/tokens.ts
|
|
9
|
-
import { injectionToken } from "@fluixi/
|
|
9
|
+
import { injectionToken } from "@fluixi/di";
|
|
10
10
|
var AuthServiceToken = injectionToken("AuthService");
|
|
11
11
|
var AuthClientToken = injectionToken("AuthClient");
|
|
12
12
|
|
|
13
13
|
// src/service.ts
|
|
14
|
+
import { createSignal, markReactiveScope, openReactiveScope, withReactiveScope } from "@fluixi/reactive/signal";
|
|
14
15
|
import { createSession } from "@fluixi/session";
|
|
15
16
|
import { createAuthHooks } from "@fluixi/auth/client";
|
|
16
17
|
function paramsFromUrl(url) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var OAuthError = class extends Error {
|
|
5
|
+
constructor(message, code, description) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.description = description;
|
|
9
|
+
this.name = "OAuthError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/dpop.ts
|
|
14
|
+
async function loadJose() {
|
|
15
|
+
try {
|
|
16
|
+
return await import("./browser-TV25RBLT.mjs");
|
|
17
|
+
} catch {
|
|
18
|
+
throw new OAuthError(
|
|
19
|
+
"DPoP needs `jose`. Install it: npm i jose. It is an optional peer, so a client not using DPoP does not carry it.",
|
|
20
|
+
"jose_missing"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function createDpopKey(alg = "ES256", options = {}) {
|
|
25
|
+
const jose = await loadJose();
|
|
26
|
+
const { privateKey, publicKey } = await jose.generateKeyPair(alg, {
|
|
27
|
+
extractable: options.extractable ?? false
|
|
28
|
+
});
|
|
29
|
+
const publicJwk = { ...await jose.exportJWK(publicKey) };
|
|
30
|
+
return {
|
|
31
|
+
privateKey,
|
|
32
|
+
publicJwk,
|
|
33
|
+
thumbprint: await jose.calculateJwkThumbprint(publicJwk, "sha256"),
|
|
34
|
+
alg
|
|
35
|
+
};
|
|
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
|
+
}
|
|
53
|
+
async function thumbprintOf(jwk) {
|
|
54
|
+
const jose = await loadJose();
|
|
55
|
+
return jose.calculateJwkThumbprint(jwk, "sha256");
|
|
56
|
+
}
|
|
57
|
+
async function proofFor(key, options) {
|
|
58
|
+
const jose = await loadJose();
|
|
59
|
+
const htu = (() => {
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(options.url);
|
|
62
|
+
url.search = "";
|
|
63
|
+
url.hash = "";
|
|
64
|
+
return url.toString();
|
|
65
|
+
} catch {
|
|
66
|
+
return options.url;
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
const claims = {
|
|
70
|
+
htm: options.method.toUpperCase(),
|
|
71
|
+
htu,
|
|
72
|
+
jti: crypto.randomUUID()
|
|
73
|
+
};
|
|
74
|
+
if (options.accessToken) {
|
|
75
|
+
const digest = await crypto.subtle.digest(
|
|
76
|
+
"SHA-256",
|
|
77
|
+
new TextEncoder().encode(options.accessToken)
|
|
78
|
+
);
|
|
79
|
+
let binary = "";
|
|
80
|
+
for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
|
|
81
|
+
claims.ath = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
82
|
+
}
|
|
83
|
+
if (options.nonce) claims.nonce = options.nonce;
|
|
84
|
+
return new jose.SignJWT(claims).setProtectedHeader({ typ: "dpop+jwt", alg: key.alg, jwk: key.publicJwk }).setIssuedAt().sign(key.privateKey);
|
|
85
|
+
}
|
|
86
|
+
function nonceFrom(response) {
|
|
87
|
+
return response.headers.get("dpop-nonce");
|
|
88
|
+
}
|
|
89
|
+
function wantsNonce(error, response) {
|
|
90
|
+
const code = typeof error === "string" ? error : error?.error;
|
|
91
|
+
return code === "use_dpop_nonce" || !!response && !!nonceFrom(response);
|
|
92
|
+
}
|
|
93
|
+
function boundKeyOf(claims) {
|
|
94
|
+
const cnf = claims.cnf;
|
|
95
|
+
return typeof cnf?.jkt === "string" ? cnf.jkt : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
OAuthError,
|
|
100
|
+
createDpopKey,
|
|
101
|
+
exportDpopKey,
|
|
102
|
+
importDpopKey,
|
|
103
|
+
thumbprintOf,
|
|
104
|
+
proofFor,
|
|
105
|
+
nonceFrom,
|
|
106
|
+
wantsNonce,
|
|
107
|
+
boundKeyOf
|
|
108
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/oauth2 v1.0.0-alpha.
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.100 | (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)
|
package/dist/discovery.cjs
CHANGED
|
@@ -48,8 +48,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
48
48
|
try {
|
|
49
49
|
response = await fetcher(documentUrl(issuer));
|
|
50
50
|
} catch (cause) {
|
|
51
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
51
52
|
throw new OAuthError(
|
|
52
|
-
|
|
53
|
+
typeof window === "undefined" ? (
|
|
54
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
55
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
56
|
+
) : `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})`,
|
|
53
57
|
"discovery_unreachable"
|
|
54
58
|
);
|
|
55
59
|
}
|
|
@@ -74,6 +78,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
74
78
|
jwks: doc.jwks_uri,
|
|
75
79
|
introspection: doc.introspection_endpoint,
|
|
76
80
|
revocation: doc.revocation_endpoint,
|
|
81
|
+
checkSession: doc.check_session_iframe,
|
|
77
82
|
issuer: doc.issuer ?? issuer
|
|
78
83
|
};
|
|
79
84
|
})();
|
package/dist/discovery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAuB5C,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,SAAS,CAAC,CAuDhG;AAED,2CAA2C;AAC3C,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
|
package/dist/discovery.js
CHANGED
|
@@ -18,7 +18,11 @@ export async function discover(issuer, fetcher = fetch) {
|
|
|
18
18
|
// `fetch` reports only "Failed to fetch". Not every provider allows a page to read
|
|
19
19
|
// its discovery document: Keycloak does once an origin is configured, Dex and
|
|
20
20
|
// Google do not. Naming the cause saves a long hunt for a network fault.
|
|
21
|
-
|
|
21
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
22
|
+
throw new OAuthError(typeof window === 'undefined'
|
|
23
|
+
? // On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
24
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
25
|
+
: `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})`, 'discovery_unreachable');
|
|
22
26
|
}
|
|
23
27
|
if (!response.ok) {
|
|
24
28
|
throw new OAuthError(`Discovery failed for ${issuer}: ${response.status}`, 'discovery_failed');
|
|
@@ -35,6 +39,7 @@ export async function discover(issuer, fetcher = fetch) {
|
|
|
35
39
|
jwks: doc.jwks_uri,
|
|
36
40
|
introspection: doc.introspection_endpoint,
|
|
37
41
|
revocation: doc.revocation_endpoint,
|
|
42
|
+
checkSession: doc.check_session_iframe,
|
|
38
43
|
issuer: doc.issuer ?? issuer,
|
|
39
44
|
};
|
|
40
45
|
})();
|
package/dist/discovery.mjs
CHANGED
|
@@ -21,8 +21,12 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
21
21
|
try {
|
|
22
22
|
response = await fetcher(documentUrl(issuer));
|
|
23
23
|
} catch (cause) {
|
|
24
|
+
const why = cause instanceof Error ? cause.message : String(cause);
|
|
24
25
|
throw new OAuthError(
|
|
25
|
-
|
|
26
|
+
typeof window === "undefined" ? (
|
|
27
|
+
// On a server nothing blocks the read: the provider is down, or the address is wrong.
|
|
28
|
+
`Could not reach the provider at ${documentUrl(issuer)}: is it running, and is the issuer right? (${why})`
|
|
29
|
+
) : `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})`,
|
|
26
30
|
"discovery_unreachable"
|
|
27
31
|
);
|
|
28
32
|
}
|
|
@@ -47,6 +51,7 @@ async function discover(issuer, fetcher = fetch) {
|
|
|
47
51
|
jwks: doc.jwks_uri,
|
|
48
52
|
introspection: doc.introspection_endpoint,
|
|
49
53
|
revocation: doc.revocation_endpoint,
|
|
54
|
+
checkSession: doc.check_session_iframe,
|
|
50
55
|
issuer: doc.issuer ?? issuer
|
|
51
56
|
};
|
|
52
57
|
})();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*! @fluixi/oauth2 v1.0.0-alpha.100 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
|
+
import {
|
|
3
|
+
boundKeyOf,
|
|
4
|
+
createDpopKey,
|
|
5
|
+
exportDpopKey,
|
|
6
|
+
importDpopKey,
|
|
7
|
+
nonceFrom,
|
|
8
|
+
proofFor,
|
|
9
|
+
thumbprintOf,
|
|
10
|
+
wantsNonce
|
|
11
|
+
} from "./chunk-KOP5Z4LB.mjs";
|
|
12
|
+
import "./chunk-KTGVSZE6.mjs";
|
|
13
|
+
export {
|
|
14
|
+
boundKeyOf,
|
|
15
|
+
createDpopKey,
|
|
16
|
+
exportDpopKey,
|
|
17
|
+
importDpopKey,
|
|
18
|
+
nonceFrom,
|
|
19
|
+
proofFor,
|
|
20
|
+
thumbprintOf,
|
|
21
|
+
wantsNonce
|
|
22
|
+
};
|