@hyper.authio/testing 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/index.cjs +171 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -8
- package/dist/index.d.ts +85 -8
- package/dist/index.js +172 -55
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { randomUUID, createHash } from 'crypto';
|
|
3
|
-
import { SignJWT, decodeJwt, generateKeyPair, exportJWK, jwtVerify, importJWK, CompactEncrypt } from 'jose';
|
|
3
|
+
import { SignJWT, decodeJwt, generateKeyPair, exportJWK, jwtVerify, importJWK, decodeProtectedHeader, CompactEncrypt } from 'jose';
|
|
4
4
|
import { AuthioWebhookHeaders, computeWebhookSignatureHeader } from '@hyper.authio/client';
|
|
5
5
|
|
|
6
6
|
// src/fake-authio.ts
|
|
@@ -11,6 +11,8 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
11
11
|
failures = /* @__PURE__ */ new Map();
|
|
12
12
|
/** Every back-channel logout token posted, in order. */
|
|
13
13
|
backchannelDeliveries = [];
|
|
14
|
+
/** Every front-channel logout page loaded, in order. */
|
|
15
|
+
frontchannelDeliveries = [];
|
|
14
16
|
/** What successive device-code polls answer; the last value repeats. Defaults to `["approved"]`. */
|
|
15
17
|
deviceOutcomes = ["approved"];
|
|
16
18
|
/** Overrides the discovery document's issuer, to test the mismatch check. */
|
|
@@ -31,6 +33,7 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
31
33
|
issued = /* @__PURE__ */ new Map();
|
|
32
34
|
devicePolls = /* @__PURE__ */ new Map();
|
|
33
35
|
lifetime;
|
|
36
|
+
clock;
|
|
34
37
|
session;
|
|
35
38
|
server;
|
|
36
39
|
signingKey;
|
|
@@ -43,7 +46,8 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
43
46
|
constructor(options) {
|
|
44
47
|
this.realm = options.realm ?? "fake";
|
|
45
48
|
this.lifetime = options.accessTokenLifetime ?? 300;
|
|
46
|
-
|
|
49
|
+
this.clock = options.clock ?? { now: () => Date.now() };
|
|
50
|
+
for (const client of options.clients ?? []) this.clients.set(client.clientId, checkClient(client));
|
|
47
51
|
for (const user of options.users ?? []) this.users.set(user.username, user);
|
|
48
52
|
for (const pat of options.personalAccessTokens ?? []) this.personalAccessTokens.set(pat.token, pat);
|
|
49
53
|
}
|
|
@@ -73,7 +77,7 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
73
77
|
}
|
|
74
78
|
/** Registers a client. */
|
|
75
79
|
addClient(client) {
|
|
76
|
-
this.clients.set(client.clientId, client);
|
|
80
|
+
this.clients.set(client.clientId, checkClient(client));
|
|
77
81
|
}
|
|
78
82
|
/** Registers a person. */
|
|
79
83
|
addUser(user) {
|
|
@@ -124,7 +128,7 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
124
128
|
* Mints an access token signed by the realm key, recorded so introspection and revocation know it.
|
|
125
129
|
*/
|
|
126
130
|
async issueAccessToken(options = {}) {
|
|
127
|
-
const now =
|
|
131
|
+
const now = this.nowSeconds();
|
|
128
132
|
const clientId = options.clientId ?? "fake-client";
|
|
129
133
|
const jti = randomUUID();
|
|
130
134
|
const payload = {
|
|
@@ -135,7 +139,8 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
135
139
|
...options.sub ? { sub: options.sub } : {},
|
|
136
140
|
...options.claims
|
|
137
141
|
};
|
|
138
|
-
const
|
|
142
|
+
const builder = new SignJWT(payload).setIssuer(options.issuer ?? this.issuer).setAudience(options.audience ?? clientId).setIssuedAt(now).setNotBefore(now).setExpirationTime(now + (options.expiresIn ?? this.lifetime)).setJti(jti);
|
|
143
|
+
const signed = options.untrusted ? await builder.setProtectedHeader({ alg: "RS256", kid: this.keyId, typ: "JWT" }).sign(this.untrustedKey) : await this.sign(builder, this.clients.get(clientId), "JWT", this.clients.get(clientId)?.accessTokenSigningAlgorithm ?? "RS256");
|
|
139
144
|
const token = options.encryptTo ? await encryptTo(signed, options.encryptTo) : signed;
|
|
140
145
|
this.issued.set(token, { claims: decodeJwt(signed), clientId, revoked: false });
|
|
141
146
|
return token;
|
|
@@ -145,7 +150,7 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
145
150
|
* removes one) and `type` the header `typ`, to build invalid ones in tests.
|
|
146
151
|
*/
|
|
147
152
|
async createLogoutToken(options) {
|
|
148
|
-
const now =
|
|
153
|
+
const now = this.nowSeconds();
|
|
149
154
|
const payload = {
|
|
150
155
|
iss: this.issuer,
|
|
151
156
|
aud: options.clientId,
|
|
@@ -158,7 +163,7 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
158
163
|
...options.overrides
|
|
159
164
|
};
|
|
160
165
|
for (const key of Object.keys(payload)) if (payload[key] === void 0) delete payload[key];
|
|
161
|
-
const signed = await new SignJWT(payload)
|
|
166
|
+
const signed = await this.sign(new SignJWT(payload), this.clients.get(options.clientId), options.type ?? "logout+jwt");
|
|
162
167
|
const encryption = this.clients.get(options.clientId)?.encryption;
|
|
163
168
|
return encryption?.logoutTokens ? encryptTo(signed, encryption) : signed;
|
|
164
169
|
}
|
|
@@ -186,6 +191,38 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
186
191
|
this.backchannelDeliveries.push({ clientId, uri: client.backchannelLogoutUri, logoutToken, status });
|
|
187
192
|
return status;
|
|
188
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The front-channel logout URL of a client for a session: its `frontchannelLogoutUri`, with `iss` and `sid` when the
|
|
196
|
+
* client requires the session.
|
|
197
|
+
*/
|
|
198
|
+
frontchannelLogoutUrl(clientId, sid) {
|
|
199
|
+
const client = this.clients.get(clientId);
|
|
200
|
+
if (!client?.frontchannelLogoutUri) throw new Error(`The fake client ${clientId} has no frontchannelLogoutUri.`);
|
|
201
|
+
const url = new URL(client.frontchannelLogoutUri);
|
|
202
|
+
if (client.frontchannelLogoutSessionRequired) {
|
|
203
|
+
url.searchParams.set("iss", this.issuer);
|
|
204
|
+
url.searchParams.set("sid", sid);
|
|
205
|
+
}
|
|
206
|
+
return url.toString();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Loads the client's front-channel logout page, as the browser does in an iframe when Authio ends a session.
|
|
210
|
+
*
|
|
211
|
+
* @returns The HTTP status the client answered, or 0 when it could not be reached.
|
|
212
|
+
*/
|
|
213
|
+
async deliverFrontchannelLogout(clientId, options) {
|
|
214
|
+
const url = this.frontchannelLogoutUrl(clientId, options.sid);
|
|
215
|
+
let status = 0;
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(url, { headers: options.headers, redirect: "manual" });
|
|
218
|
+
status = response.status;
|
|
219
|
+
await response.arrayBuffer();
|
|
220
|
+
} catch {
|
|
221
|
+
status = 0;
|
|
222
|
+
}
|
|
223
|
+
this.frontchannelDeliveries.push({ clientId, url, status });
|
|
224
|
+
return status;
|
|
225
|
+
}
|
|
189
226
|
/** Marks a token revoked, as Authio would after a sign-out: an access token, or a refresh token, which is then forgotten. */
|
|
190
227
|
revokeToken(token) {
|
|
191
228
|
const issued = this.issued.get(token);
|
|
@@ -302,6 +339,8 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
302
339
|
acr_values_supported: ["1", "2", "3"],
|
|
303
340
|
backchannel_logout_supported: true,
|
|
304
341
|
backchannel_logout_session_supported: true,
|
|
342
|
+
frontchannel_logout_supported: true,
|
|
343
|
+
frontchannel_logout_session_supported: true,
|
|
305
344
|
authorization_response_iss_parameter_supported: !this.omitAuthorizationIssuer
|
|
306
345
|
};
|
|
307
346
|
}
|
|
@@ -327,11 +366,28 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
327
366
|
response.writeHead(302, { location: location.toString() });
|
|
328
367
|
response.end();
|
|
329
368
|
};
|
|
369
|
+
if (query.response_mode !== void 0 && query.response_mode !== "query" && query.response_mode !== "web_message")
|
|
370
|
+
return answer({ error: "invalid_request", error_description: `The response_mode '${query.response_mode}' is not supported: Authio answers with query or web_message.` });
|
|
330
371
|
if (query.response_type !== "code") return answer({ error: "unsupported_response_type", error_description: "Only code is supported." });
|
|
331
|
-
const method = query.
|
|
332
|
-
|
|
372
|
+
const method = query.code_challenge ? query.code_challenge_method : void 0;
|
|
373
|
+
const allowedMethods = client.pkceMethods ?? ["S256"];
|
|
374
|
+
if (query.code_challenge) {
|
|
375
|
+
if (!method)
|
|
376
|
+
return answer({ error: "invalid_request", error_description: "code_challenge_method is required with code_challenge: Authio does not assume plain." });
|
|
377
|
+
if (method !== "S256" && method !== "plain") return answer({ error: "invalid_request", error_description: `The PKCE method '${method}' is not supported.` });
|
|
378
|
+
if (!allowedMethods.includes(method))
|
|
379
|
+
return answer({
|
|
380
|
+
error: "invalid_request",
|
|
381
|
+
error_description: `The PKCE method '${method}' is not allowed for the client ${client.clientId}, which allows ${allowedMethods.join(", ")} (codeChallengeMethod).`
|
|
382
|
+
});
|
|
383
|
+
} else if (client.requirePkce ?? true) {
|
|
384
|
+
return answer({
|
|
385
|
+
error: "invalid_request",
|
|
386
|
+
error_description: `The client ${client.clientId} requires PKCE: send code_challenge and code_challenge_method (checks: ["pkce"]).`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
333
389
|
const prompts = (query.prompt ?? "").split(" ").filter(Boolean);
|
|
334
|
-
const now =
|
|
390
|
+
const now = this.nowSeconds();
|
|
335
391
|
const maxAge = query.max_age !== void 0 ? Number(query.max_age) : void 0;
|
|
336
392
|
const tooOld = this.session !== void 0 && maxAge !== void 0 && now - this.session.authTime > maxAge;
|
|
337
393
|
const requestedAcr = (query.acr_values ?? "").split(" ").filter(Boolean).sort().at(-1);
|
|
@@ -373,7 +429,11 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
373
429
|
let clientId = query.client_id;
|
|
374
430
|
if (query.id_token_hint) {
|
|
375
431
|
try {
|
|
376
|
-
const
|
|
432
|
+
const audience = [decodeJwt(query.id_token_hint).aud ?? []].flat()[0];
|
|
433
|
+
const { payload } = await jwtVerify(query.id_token_hint, this.verificationKeyFor(audience ? this.clients.get(audience) : void 0, query.id_token_hint), {
|
|
434
|
+
issuer: this.issuer,
|
|
435
|
+
currentDate: new Date(this.clock.now())
|
|
436
|
+
});
|
|
377
437
|
clientId ??= [payload.aud ?? []].flat()[0];
|
|
378
438
|
} catch {
|
|
379
439
|
return send(response, { status: 400, body: { error: "invalid_request", error_description: "The id_token_hint is not valid." } });
|
|
@@ -386,9 +446,11 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
386
446
|
const ended = this.session;
|
|
387
447
|
this.session = void 0;
|
|
388
448
|
if (ended) this.revokeRefreshTokens({ sid: ended.sid });
|
|
389
|
-
if (ended)
|
|
390
|
-
for (const id of ended.clients)
|
|
391
|
-
|
|
449
|
+
if (ended)
|
|
450
|
+
for (const id of ended.clients) {
|
|
451
|
+
if (this.clients.get(id)?.backchannelLogoutUri) await this.deliverBackchannelLogout(id, { sub: ended.sub, sid: ended.sid });
|
|
452
|
+
if (this.clients.get(id)?.frontchannelLogoutUri) await this.deliverFrontchannelLogout(id, { sid: ended.sid });
|
|
453
|
+
}
|
|
392
454
|
if (!redirect) {
|
|
393
455
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
394
456
|
response.end("<!doctype html><p>You are signed out.</p>");
|
|
@@ -402,28 +464,38 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
402
464
|
async authenticate(recorded, form) {
|
|
403
465
|
const invalid = (description) => ({ status: 401, body: { error: "invalid_client", error_description: description } });
|
|
404
466
|
const authorization = recorded.headers.authorization;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
467
|
+
const basic = authorization?.startsWith("Basic ") ? Buffer.from(authorization.slice(6), "base64").toString("utf8").split(":").map(decodeURIComponent) : void 0;
|
|
468
|
+
const presented = basic ? "client_secret_basic" : form.client_assertion ? "private_key_jwt" : form.client_secret !== void 0 ? "client_secret_post" : "none";
|
|
469
|
+
const client = this.clients.get((basic ? basic[0] : form.client_id) ?? "");
|
|
470
|
+
if (!client) return invalid("Unknown client.");
|
|
471
|
+
if (form.client_assertion && form.client_secret !== void 0) return invalid("A client assertion and a client secret cannot be sent together.");
|
|
472
|
+
const registered = registeredMethodOf(client);
|
|
473
|
+
if (presented !== registered && !(registered === "none" && (presented === "client_secret_basic" || presented === "client_secret_post")))
|
|
474
|
+
return invalid(`The client ${client.clientId} authenticates with ${registered} (tokenEndpointAuthMethod), not ${presented}.`);
|
|
475
|
+
switch (registered) {
|
|
476
|
+
case "client_secret_basic":
|
|
477
|
+
if (!client.clientSecret || client.clientSecret !== basic?.[1]) return invalid("The client credentials are wrong.");
|
|
478
|
+
break;
|
|
479
|
+
case "client_secret_post":
|
|
480
|
+
if (!client.clientSecret || client.clientSecret !== form.client_secret) return invalid("The client credentials are wrong.");
|
|
481
|
+
break;
|
|
482
|
+
case "private_key_jwt": {
|
|
483
|
+
if (!client.publicKey) return invalid("The client has no registered key.");
|
|
484
|
+
if (form.client_assertion_type !== "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") return invalid("Wrong assertion type.");
|
|
485
|
+
try {
|
|
486
|
+
const key = await importJWK(client.publicKey, client.publicKey.alg);
|
|
487
|
+
const { payload } = await jwtVerify(form.client_assertion, key, {
|
|
488
|
+
issuer: client.clientId,
|
|
489
|
+
subject: client.clientId,
|
|
490
|
+
audience: this.issuer,
|
|
491
|
+
currentDate: new Date(this.clock.now())
|
|
492
|
+
});
|
|
493
|
+
if (!payload.jti) return invalid("The assertion has no jti.");
|
|
494
|
+
} catch (error) {
|
|
495
|
+
return invalid(`The assertion does not verify: ${error.message}`);
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
420
498
|
}
|
|
421
|
-
} else if (form.client_secret !== void 0) {
|
|
422
|
-
client = this.clients.get(form.client_id ?? "");
|
|
423
|
-
if (!client || client.clientSecret !== form.client_secret) return invalid("The client credentials are wrong.");
|
|
424
|
-
} else {
|
|
425
|
-
client = this.clients.get(form.client_id ?? "");
|
|
426
|
-
if (!client || client.clientSecret || client.publicKey) return invalid("The client must authenticate.");
|
|
427
499
|
}
|
|
428
500
|
recorded.clientId = client.clientId;
|
|
429
501
|
return client;
|
|
@@ -458,11 +530,12 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
458
530
|
const code = this.codes.get(form.code ?? "");
|
|
459
531
|
if (!code || code.clientId !== client.clientId || code.redirectUri !== form.redirect_uri) return grant("invalid_grant", "The code is not valid.");
|
|
460
532
|
if (code.codeVerifier && code.codeVerifier !== form.code_verifier) return grant("invalid_grant", "PKCE verification failed.");
|
|
461
|
-
if (code.codeChallenge && !
|
|
533
|
+
if (code.codeChallenge && !form.code_verifier) return grant("invalid_grant", "code_verifier is required: the code was issued with a code_challenge.");
|
|
534
|
+
if (code.codeChallenge && !verifiesChallenge(form.code_verifier, code.codeChallenge, code.codeChallengeMethod ?? "S256"))
|
|
462
535
|
return grant("invalid_grant", "PKCE verification failed.");
|
|
463
536
|
this.codes.delete(form.code);
|
|
464
537
|
const user = [...this.users.values()].find((candidate) => candidate.sub === code.sub);
|
|
465
|
-
const signIn = code.sid ? { sid: code.sid, auth_time: code.authTime, acr: code.acr, amr: code.amr } : { auth_time:
|
|
538
|
+
const signIn = code.sid ? { sid: code.sid, auth_time: code.authTime, acr: code.acr, amr: code.amr } : { auth_time: this.nowSeconds(), acr: user.acr ?? "1", amr: user.amr ?? ["pwd"] };
|
|
466
539
|
return this.tokenAnswer(
|
|
467
540
|
client,
|
|
468
541
|
{
|
|
@@ -481,10 +554,13 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
481
554
|
if (!refresh || refresh.clientId !== client.clientId) return grant("invalid_grant", "The refresh token is not valid.");
|
|
482
555
|
this.refreshTokens.delete(form.refresh_token);
|
|
483
556
|
const user = [...this.users.values()].find((candidate) => candidate.sub === refresh.sub);
|
|
557
|
+
const signIn = { ...refresh.sid ? { sid: refresh.sid } : {}, ...refresh.authTime ? { auth_time: refresh.authTime } : {} };
|
|
484
558
|
return this.tokenAnswer(
|
|
485
559
|
client,
|
|
486
|
-
{ sub: refresh.sub, scope: form.scope ?? refresh.scope, claims: { ...client.claims, ...user?.claims, ...
|
|
487
|
-
user
|
|
560
|
+
{ sub: refresh.sub, scope: form.scope ?? refresh.scope, claims: { ...client.claims, ...user?.claims, ...signIn } },
|
|
561
|
+
user,
|
|
562
|
+
void 0,
|
|
563
|
+
client.issueIdTokenOnRefresh ? { signIn } : void 0
|
|
488
564
|
);
|
|
489
565
|
}
|
|
490
566
|
case "urn:ietf:params:oauth:grant-type:token-exchange": {
|
|
@@ -550,21 +626,33 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
550
626
|
if (user) {
|
|
551
627
|
const refreshToken = randomUUID();
|
|
552
628
|
const sid = typeof options.claims?.sid === "string" ? options.claims.sid : void 0;
|
|
553
|
-
|
|
629
|
+
const authTime = typeof options.claims?.auth_time === "number" ? options.claims.auth_time : void 0;
|
|
630
|
+
this.refreshTokens.set(refreshToken, {
|
|
631
|
+
clientId: client.clientId,
|
|
632
|
+
sub: user.sub,
|
|
633
|
+
scope: options.scope,
|
|
634
|
+
sid,
|
|
635
|
+
authTime,
|
|
636
|
+
expiresAt: this.nowSeconds() + 30 * 24 * 3600
|
|
637
|
+
});
|
|
554
638
|
body.refresh_token = refreshToken;
|
|
555
639
|
}
|
|
556
640
|
if (user && idToken && (options.scope ?? "").split(" ").includes("openid")) {
|
|
557
|
-
const now =
|
|
641
|
+
const now = this.nowSeconds();
|
|
558
642
|
const signIn = Object.fromEntries(Object.entries(idToken.signIn).filter(([, value]) => value !== void 0));
|
|
559
|
-
const signed = await
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
643
|
+
const signed = await this.sign(
|
|
644
|
+
new SignJWT({
|
|
645
|
+
typ: "ID",
|
|
646
|
+
azp: client.clientId,
|
|
647
|
+
...signIn,
|
|
648
|
+
...idToken.nonce ? { nonce: idToken.nonce } : {},
|
|
649
|
+
at_hash: halfHash(accessToken, client.signingAlgorithm ?? "RS256"),
|
|
650
|
+
preferred_username: user.username,
|
|
651
|
+
...user.userInfo
|
|
652
|
+
}).setIssuer(this.issuer).setAudience(client.clientId).setSubject(user.sub).setIssuedAt(now).setExpirationTime(now + this.lifetime),
|
|
653
|
+
client,
|
|
654
|
+
"JWT"
|
|
655
|
+
);
|
|
568
656
|
body.id_token = encryption && encryption.idTokens !== false ? await encryptTo(signed, encryption) : signed;
|
|
569
657
|
}
|
|
570
658
|
return { status: 200, body };
|
|
@@ -573,7 +661,7 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
573
661
|
const client = await this.authenticate(recorded, form);
|
|
574
662
|
if ("status" in client) return client;
|
|
575
663
|
const issued = this.issued.get(form.token ?? "");
|
|
576
|
-
const now =
|
|
664
|
+
const now = this.nowSeconds();
|
|
577
665
|
const refresh = this.refreshTokens.get(form.token ?? "");
|
|
578
666
|
if (refresh)
|
|
579
667
|
return refresh.clientId === client.clientId ? {
|
|
@@ -630,13 +718,29 @@ var FakeAuthio = class _FakeAuthio {
|
|
|
630
718
|
const user = [...this.users.values()].find((candidate) => candidate.sub === issued.claims.sub);
|
|
631
719
|
const claims = { sub: issued.claims.sub, ...user?.userInfo };
|
|
632
720
|
if (!this.userInfoAsJwt) return send(response, { status: 200, body: claims });
|
|
633
|
-
const now =
|
|
634
|
-
const signed = await
|
|
721
|
+
const now = this.nowSeconds();
|
|
722
|
+
const signed = await this.sign(
|
|
723
|
+
new SignJWT(claims).setIssuer(this.issuer).setAudience(issued.clientId).setIssuedAt(now).setExpirationTime(now + this.lifetime),
|
|
724
|
+
this.clients.get(issued.clientId),
|
|
725
|
+
void 0
|
|
726
|
+
);
|
|
635
727
|
const encryption = this.clients.get(issued.clientId)?.encryption;
|
|
636
728
|
const jwt = encryption && encryption.userInfo !== false ? await encryptTo(signed, encryption) : signed;
|
|
637
729
|
response.writeHead(200, { "content-type": "application/jwt" });
|
|
638
730
|
response.end(jwt);
|
|
639
731
|
}
|
|
732
|
+
nowSeconds() {
|
|
733
|
+
return Math.floor(this.clock.now() / 1e3);
|
|
734
|
+
}
|
|
735
|
+
sign(builder, client, type, algorithm = client?.signingAlgorithm ?? "RS256") {
|
|
736
|
+
if (algorithm === "RS256") return builder.setProtectedHeader({ alg: "RS256", kid: this.keyId, ...type ? { typ: type } : {} }).sign(this.signingKey);
|
|
737
|
+
if (!client?.clientSecret) throw new Error(`The fake client ${client?.clientId} signs with ${algorithm} and has no clientSecret.`);
|
|
738
|
+
return builder.setProtectedHeader({ alg: algorithm, ...type ? { typ: type } : {} }).sign(new TextEncoder().encode(client.clientSecret));
|
|
739
|
+
}
|
|
740
|
+
verificationKeyFor(client, token) {
|
|
741
|
+
const algorithm = String(decodeProtectedHeader(token).alg ?? "");
|
|
742
|
+
return algorithm.startsWith("HS") && client?.clientSecret ? new TextEncoder().encode(client.clientSecret) : this.verificationKey;
|
|
743
|
+
}
|
|
640
744
|
requireUser(username) {
|
|
641
745
|
const user = this.users.get(username);
|
|
642
746
|
if (!user) throw new Error(`The fake has no user ${username}.`);
|
|
@@ -663,8 +767,21 @@ function verifiesChallenge(verifier, challenge, method) {
|
|
|
663
767
|
if (!verifier) return false;
|
|
664
768
|
return method === "S256" ? createHash("sha256").update(verifier, "ascii").digest("base64url") === challenge : verifier === challenge;
|
|
665
769
|
}
|
|
666
|
-
function halfHash(value) {
|
|
667
|
-
|
|
770
|
+
function halfHash(value, algorithm) {
|
|
771
|
+
const size = algorithm.endsWith("384") ? 384 : algorithm.endsWith("512") ? 512 : 256;
|
|
772
|
+
const digest = createHash(`sha${size}`).update(value, "ascii").digest();
|
|
773
|
+
return digest.subarray(0, digest.length / 2).toString("base64url");
|
|
774
|
+
}
|
|
775
|
+
function checkClient(client) {
|
|
776
|
+
for (const algorithm of [client.signingAlgorithm ?? "RS256", client.accessTokenSigningAlgorithm ?? "RS256"]) {
|
|
777
|
+
const minimum = { HS256: 32, HS384: 48, HS512: 64 }[algorithm];
|
|
778
|
+
if (minimum !== void 0 && Buffer.byteLength(client.clientSecret ?? "", "utf8") < minimum)
|
|
779
|
+
throw new Error(`The fake client ${client.clientId} signs with ${algorithm}, which needs a clientSecret of at least ${minimum} bytes (RFC 7518 \xA73.2).`);
|
|
780
|
+
}
|
|
781
|
+
return client;
|
|
782
|
+
}
|
|
783
|
+
function registeredMethodOf(client) {
|
|
784
|
+
return client.tokenEndpointAuthMethod ?? (client.clientSecret ? "client_secret_basic" : client.publicKey ? "private_key_jwt" : "none");
|
|
668
785
|
}
|
|
669
786
|
function send(response, answer) {
|
|
670
787
|
const headers = { ...answer.headers };
|