@udibo/oauth2 0.17.0 → 0.17.1

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.
@@ -117,8 +117,9 @@ export declare function exportSigningKeyJwk(key: SigningKey): Promise<JsonWebKey
117
117
  }>;
118
118
  /**
119
119
  * Import a signing key previously exported with {@link exportSigningKeyJwk}.
120
- * A JWK without a `kid` is given a random one, so every instance importing it
121
- * would publish and stamp a different `kid` — persist the exported `kid`.
120
+ * A JWK that sets a `kid` keeps it. One without a `kid` gets its RFC 7638 JWK
121
+ * thumbprint (base64url SHA-256 of the public key), so every instance
122
+ * importing the same key publishes and stamps the same `kid`.
122
123
  */
123
124
  export declare function importSigningKeyJwk(jwk: JsonWebKey & {
124
125
  kid?: string;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { base64urlDecode, base64urlEncode } from "../utils/crypto.js";
15
15
  import { snapshotAuthenticationContext } from "../utils/authentication-context.js";
16
+ import { jwkThumbprint } from "../utils/_jwk-thumbprint.js";
16
17
  /** A {@link SigningKeyProvider} over one fixed key. */
17
18
  export class StaticSigningKeyProvider {
18
19
  #key;
@@ -130,12 +131,13 @@ export async function exportSigningKeyJwk(key) {
130
131
  }
131
132
  /**
132
133
  * Import a signing key previously exported with {@link exportSigningKeyJwk}.
133
- * A JWK without a `kid` is given a random one, so every instance importing it
134
- * would publish and stamp a different `kid` — persist the exported `kid`.
134
+ * A JWK that sets a `kid` keeps it. One without a `kid` gets its RFC 7638 JWK
135
+ * thumbprint (base64url SHA-256 of the public key), so every instance
136
+ * importing the same key publishes and stamps the same `kid`.
135
137
  */
136
138
  export async function importSigningKeyJwk(jwk) {
137
139
  const privateKey = await crypto.subtle.importKey("jwk", jwk, ES256_PARAMS, true, ["sign"]);
138
- const kid = jwk.kid ?? crypto.randomUUID();
140
+ const kid = jwk.kid ?? (await jwkThumbprint(jwk));
139
141
  return {
140
142
  kid,
141
143
  alg: "ES256",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * RFC 7638 JWK thumbprints.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * The RFC 7638 SHA-256 thumbprint of a JWK, base64url-encoded. Only the key
8
+ * type's required public members are hashed, so a private JWK and its public
9
+ * half share a thumbprint. Rejects with a `TypeError` for a key type other
10
+ * than `EC`, `OKP` or `RSA`, or when a required member is missing.
11
+ */
12
+ export declare function jwkThumbprint(jwk: JsonWebKey): Promise<string>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * RFC 7638 JWK thumbprints.
3
+ *
4
+ * @module
5
+ */
6
+ import { encodeBase64Url } from "./_encoding.js";
7
+ const REQUIRED_MEMBERS = {
8
+ EC: ["crv", "kty", "x", "y"],
9
+ OKP: ["crv", "kty", "x"],
10
+ RSA: ["e", "kty", "n"],
11
+ };
12
+ /**
13
+ * The RFC 7638 SHA-256 thumbprint of a JWK, base64url-encoded. Only the key
14
+ * type's required public members are hashed, so a private JWK and its public
15
+ * half share a thumbprint. Rejects with a `TypeError` for a key type other
16
+ * than `EC`, `OKP` or `RSA`, or when a required member is missing.
17
+ */
18
+ export async function jwkThumbprint(jwk) {
19
+ const members = jwk.kty === undefined ? undefined : REQUIRED_MEMBERS[jwk.kty];
20
+ if (!members) {
21
+ throw new TypeError(`Cannot compute a JWK thumbprint for key type ${JSON.stringify(jwk.kty)}`);
22
+ }
23
+ const fields = jwk;
24
+ const canonical = {};
25
+ for (const member of members) {
26
+ const value = fields[member];
27
+ if (typeof value !== "string") {
28
+ throw new TypeError(`Cannot compute a JWK thumbprint without the "${member}" member`);
29
+ }
30
+ canonical[member] = value;
31
+ }
32
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(canonical)));
33
+ return encodeBase64Url(new Uint8Array(digest));
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udibo/oauth2",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
5
5
  "keywords": [
6
6
  "authorization-server",