@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/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
- for (const client of options.clients ?? []) this.clients.set(client.clientId, client);
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 = Math.floor(Date.now() / 1e3);
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 signed = await new SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId, typ: "JWT" }).setIssuer(options.issuer ?? this.issuer).setAudience(options.audience ?? clientId).setIssuedAt(now).setNotBefore(now).setExpirationTime(now + (options.expiresIn ?? this.lifetime)).setJti(jti).sign(options.untrusted ? this.untrustedKey : this.signingKey);
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 = Math.floor(Date.now() / 1e3);
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).setProtectedHeader({ alg: "RS256", kid: this.keyId, typ: options.type ?? "logout+jwt" }).sign(this.signingKey);
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.code_challenge_method ?? (query.code_challenge ? "plain" : void 0);
332
- if (method && method !== "S256" && method !== "plain") return answer({ error: "invalid_request", error_description: "Unsupported PKCE method." });
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 = Math.floor(Date.now() / 1e3);
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 { payload } = await jwtVerify(query.id_token_hint, this.verificationKey, { issuer: this.issuer });
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) if (this.clients.get(id)?.backchannelLogoutUri) await this.deliverBackchannelLogout(id, { sub: ended.sub, sid: ended.sid });
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
- let client;
406
- if (authorization?.startsWith("Basic ")) {
407
- const [id, secret] = Buffer.from(authorization.slice(6), "base64").toString("utf8").split(":").map(decodeURIComponent);
408
- client = this.clients.get(id ?? "");
409
- if (!client || !client.clientSecret || client.clientSecret !== secret) return invalid("The client credentials are wrong.");
410
- } else if (form.client_assertion) {
411
- client = this.clients.get(form.client_id ?? "");
412
- if (!client?.publicKey) return invalid("The client has no registered key.");
413
- if (form.client_assertion_type !== "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") return invalid("Wrong assertion type.");
414
- try {
415
- const key = await importJWK(client.publicKey, client.publicKey.alg);
416
- const { payload } = await jwtVerify(form.client_assertion, key, { issuer: client.clientId, subject: client.clientId, audience: this.issuer });
417
- if (!payload.jti) return invalid("The assertion has no jti.");
418
- } catch (error) {
419
- return invalid(`The assertion does not verify: ${error.message}`);
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 && !verifiesChallenge(form.code_verifier, code.codeChallenge, code.codeChallengeMethod ?? "plain"))
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: Math.floor(Date.now() / 1e3), acr: user.acr ?? "1", amr: user.amr ?? ["pwd"] };
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, ...refresh.sid ? { sid: refresh.sid } : {} } },
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
- this.refreshTokens.set(refreshToken, { clientId: client.clientId, sub: user.sub, scope: options.scope, sid, expiresAt: Math.floor(Date.now() / 1e3) + 30 * 24 * 3600 });
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 = Math.floor(Date.now() / 1e3);
641
+ const now = this.nowSeconds();
558
642
  const signIn = Object.fromEntries(Object.entries(idToken.signIn).filter(([, value]) => value !== void 0));
559
- const signed = await new SignJWT({
560
- typ: "ID",
561
- azp: client.clientId,
562
- ...signIn,
563
- ...idToken.nonce ? { nonce: idToken.nonce } : {},
564
- at_hash: halfHash(accessToken),
565
- preferred_username: user.username,
566
- ...user.userInfo
567
- }).setProtectedHeader({ alg: "RS256", kid: this.keyId, typ: "JWT" }).setIssuer(this.issuer).setAudience(client.clientId).setSubject(user.sub).setIssuedAt(now).setExpirationTime(now + this.lifetime).sign(this.signingKey);
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 = Math.floor(Date.now() / 1e3);
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 = Math.floor(Date.now() / 1e3);
634
- const signed = await new SignJWT(claims).setProtectedHeader({ alg: "RS256", kid: this.keyId }).setIssuer(this.issuer).setAudience(issued.clientId).setIssuedAt(now).setExpirationTime(now + this.lifetime).sign(this.signingKey);
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
- return createHash("sha256").update(value, "ascii").digest().subarray(0, 16).toString("base64url");
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 };