@atcute/oauth-crypto 0.1.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.
Files changed (118) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +7 -0
  3. package/dist/client-assertion/create-client-assertion.d.ts +19 -0
  4. package/dist/client-assertion/create-client-assertion.d.ts.map +1 -0
  5. package/dist/client-assertion/create-client-assertion.js +34 -0
  6. package/dist/client-assertion/create-client-assertion.js.map +1 -0
  7. package/dist/client-assertion/generate-key.d.ts +11 -0
  8. package/dist/client-assertion/generate-key.d.ts.map +1 -0
  9. package/dist/client-assertion/generate-key.js +18 -0
  10. package/dist/client-assertion/generate-key.js.map +1 -0
  11. package/dist/client-assertion/index.d.ts +5 -0
  12. package/dist/client-assertion/index.d.ts.map +1 -0
  13. package/dist/client-assertion/index.js +4 -0
  14. package/dist/client-assertion/index.js.map +1 -0
  15. package/dist/client-assertion/keys.d.ts +14 -0
  16. package/dist/client-assertion/keys.d.ts.map +1 -0
  17. package/dist/client-assertion/keys.js +18 -0
  18. package/dist/client-assertion/keys.js.map +1 -0
  19. package/dist/client-assertion/types.d.ts +9 -0
  20. package/dist/client-assertion/types.d.ts.map +1 -0
  21. package/dist/client-assertion/types.js +2 -0
  22. package/dist/client-assertion/types.js.map +1 -0
  23. package/dist/dpop/fetch.d.ts +24 -0
  24. package/dist/dpop/fetch.d.ts.map +1 -0
  25. package/dist/dpop/fetch.js +102 -0
  26. package/dist/dpop/fetch.js.map +1 -0
  27. package/dist/dpop/generate-key.d.ts +9 -0
  28. package/dist/dpop/generate-key.d.ts.map +1 -0
  29. package/dist/dpop/generate-key.js +61 -0
  30. package/dist/dpop/generate-key.js.map +1 -0
  31. package/dist/dpop/index.d.ts +6 -0
  32. package/dist/dpop/index.d.ts.map +1 -0
  33. package/dist/dpop/index.js +5 -0
  34. package/dist/dpop/index.js.map +1 -0
  35. package/dist/dpop/proof.d.ts +9 -0
  36. package/dist/dpop/proof.d.ts.map +1 -0
  37. package/dist/dpop/proof.js +36 -0
  38. package/dist/dpop/proof.js.map +1 -0
  39. package/dist/dpop/types.d.ts +17 -0
  40. package/dist/dpop/types.d.ts.map +1 -0
  41. package/dist/dpop/types.js +2 -0
  42. package/dist/dpop/types.js.map +1 -0
  43. package/dist/dpop/verify.d.ts +42 -0
  44. package/dist/dpop/verify.d.ts.map +1 -0
  45. package/dist/dpop/verify.js +124 -0
  46. package/dist/dpop/verify.js.map +1 -0
  47. package/dist/hash/index.d.ts +3 -0
  48. package/dist/hash/index.d.ts.map +1 -0
  49. package/dist/hash/index.js +3 -0
  50. package/dist/hash/index.js.map +1 -0
  51. package/dist/hash/pkce.d.ts +12 -0
  52. package/dist/hash/pkce.d.ts.map +1 -0
  53. package/dist/hash/pkce.js +14 -0
  54. package/dist/hash/pkce.js.map +1 -0
  55. package/dist/hash/sha256.d.ts +8 -0
  56. package/dist/hash/sha256.d.ts.map +1 -0
  57. package/dist/hash/sha256.js +14 -0
  58. package/dist/hash/sha256.js.map +1 -0
  59. package/dist/index.d.ts +6 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +6 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/internal/crypto.d.ts +7 -0
  64. package/dist/internal/crypto.d.ts.map +1 -0
  65. package/dist/internal/crypto.js +78 -0
  66. package/dist/internal/crypto.js.map +1 -0
  67. package/dist/internal/jwk.d.ts +10 -0
  68. package/dist/internal/jwk.d.ts.map +1 -0
  69. package/dist/internal/jwk.js +121 -0
  70. package/dist/internal/jwk.js.map +1 -0
  71. package/dist/internal/key-cache.d.ts +24 -0
  72. package/dist/internal/key-cache.d.ts.map +1 -0
  73. package/dist/internal/key-cache.js +36 -0
  74. package/dist/internal/key-cache.js.map +1 -0
  75. package/dist/jwk/compute-jkt.d.ts +9 -0
  76. package/dist/jwk/compute-jkt.d.ts.map +1 -0
  77. package/dist/jwk/compute-jkt.js +23 -0
  78. package/dist/jwk/compute-jkt.js.map +1 -0
  79. package/dist/jwk/index.d.ts +5 -0
  80. package/dist/jwk/index.d.ts.map +1 -0
  81. package/dist/jwk/index.js +4 -0
  82. package/dist/jwk/index.js.map +1 -0
  83. package/dist/jwk/keys.d.ts +9 -0
  84. package/dist/jwk/keys.d.ts.map +1 -0
  85. package/dist/jwk/keys.js +13 -0
  86. package/dist/jwk/keys.js.map +1 -0
  87. package/dist/jwk/types.d.ts +37 -0
  88. package/dist/jwk/types.d.ts.map +1 -0
  89. package/dist/jwk/types.js +2 -0
  90. package/dist/jwk/types.js.map +1 -0
  91. package/dist/jwt/index.d.ts +26 -0
  92. package/dist/jwt/index.d.ts.map +1 -0
  93. package/dist/jwt/index.js +56 -0
  94. package/dist/jwt/index.js.map +1 -0
  95. package/lib/client-assertion/create-client-assertion.ts +50 -0
  96. package/lib/client-assertion/generate-key.ts +26 -0
  97. package/lib/client-assertion/index.ts +4 -0
  98. package/lib/client-assertion/keys.ts +26 -0
  99. package/lib/client-assertion/types.ts +9 -0
  100. package/lib/dpop/fetch.ts +140 -0
  101. package/lib/dpop/generate-key.ts +72 -0
  102. package/lib/dpop/index.ts +11 -0
  103. package/lib/dpop/proof.ts +46 -0
  104. package/lib/dpop/types.ts +19 -0
  105. package/lib/dpop/verify.ts +169 -0
  106. package/lib/hash/index.ts +2 -0
  107. package/lib/hash/pkce.ts +18 -0
  108. package/lib/hash/sha256.ts +14 -0
  109. package/lib/index.ts +5 -0
  110. package/lib/internal/crypto.ts +92 -0
  111. package/lib/internal/jwk.ts +157 -0
  112. package/lib/internal/key-cache.ts +51 -0
  113. package/lib/jwk/compute-jkt.ts +27 -0
  114. package/lib/jwk/index.ts +12 -0
  115. package/lib/jwk/keys.ts +15 -0
  116. package/lib/jwk/types.ts +51 -0
  117. package/lib/jwt/index.ts +86 -0
  118. package/package.json +38 -0
@@ -0,0 +1,13 @@
1
+ import { exportPkcs8PrivateKey as exportPkcs8 } from '../internal/jwk.js';
2
+ import { getCachedKeyMaterial } from '../internal/key-cache.js';
3
+ /**
4
+ * exports a private JWK to PKCS8 PEM format.
5
+ *
6
+ * @param jwk private JWK to export
7
+ * @returns PKCS8 PEM string
8
+ */
9
+ export const exportPkcs8PrivateKey = async (jwk) => {
10
+ const { cryptoKey } = await getCachedKeyMaterial(jwk);
11
+ return exportPkcs8(cryptoKey);
12
+ };
13
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../lib/jwk/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,IAAI,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,GAAe,EAAmB,EAAE,CAAC;IAChF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACtD,OAAO,WAAW,CAAC,SAAS,CAAC,CAAC;AAAA,CAC9B,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * signing algorithms supported by atproto oauth.
3
+ */
4
+ export type SigningAlgorithm = 'ES256' | 'ES384' | 'ES512' | 'PS256' | 'PS384' | 'PS512' | 'RS256' | 'RS384' | 'RS512';
5
+ export interface EcPublicJwk {
6
+ kty: 'EC';
7
+ crv: 'P-256' | 'P-384' | 'P-521';
8
+ x: string;
9
+ y: string;
10
+ alg?: SigningAlgorithm;
11
+ use?: 'sig';
12
+ kid?: string;
13
+ }
14
+ export interface RsaPublicJwk {
15
+ kty: 'RSA';
16
+ n: string;
17
+ e: string;
18
+ alg?: SigningAlgorithm;
19
+ use?: 'sig';
20
+ kid?: string;
21
+ }
22
+ export type PublicJwk = EcPublicJwk | RsaPublicJwk;
23
+ export interface EcPrivateJwk extends EcPublicJwk {
24
+ alg: SigningAlgorithm;
25
+ d: string;
26
+ }
27
+ export interface RsaPrivateJwk extends RsaPublicJwk {
28
+ alg: SigningAlgorithm;
29
+ d: string;
30
+ p?: string;
31
+ q?: string;
32
+ dp?: string;
33
+ dq?: string;
34
+ qi?: string;
35
+ }
36
+ export type PrivateJwk = EcPrivateJwk | RsaPrivateJwk;
37
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../lib/jwk/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACzB,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,CAAC;AAEX,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,GAAG,CAAC,EAAE,KAAK,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,KAAK,CAAC;IACX,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,GAAG,CAAC,EAAE,KAAK,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY,CAAC;AAEnD,MAAM,WAAW,YAAa,SAAQ,WAAW;IAChD,GAAG,EAAE,gBAAgB,CAAC;IACtB,CAAC,EAAE,MAAM,CAAC;CACV;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IAClD,GAAG,EAAE,gBAAgB,CAAC;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../lib/jwk/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import type { SigningAlgorithm } from '../jwk/types.js';
2
+ /**
3
+ * signs a jwt using webcrypto.
4
+ *
5
+ * @param params signing parameters
6
+ * @returns signed jwt
7
+ */
8
+ export declare const signJwt: (params: {
9
+ header: Record<string, unknown>;
10
+ payload: Record<string, unknown>;
11
+ key: CryptoKey;
12
+ alg: SigningAlgorithm;
13
+ }) => Promise<string>;
14
+ /**
15
+ * verifies a jwt and returns its payload.
16
+ *
17
+ * @param jwt jwt string
18
+ * @param options verification options
19
+ * @returns decoded payload
20
+ */
21
+ export declare const verifyJwt: (jwt: string, options: {
22
+ key: CryptoKey;
23
+ alg: SigningAlgorithm;
24
+ typ?: string | undefined;
25
+ }) => Promise<Record<string, unknown>>;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/jwt/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,OAAO;;;;;qBAqBnB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;sCAkCrB,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { fromBase64Url, toBase64Url } from '@atcute/multibase';
2
+ import { decodeUtf8From, encodeUtf8 } from '@atcute/uint8array';
3
+ import { getSignAlgorithm } from '../internal/crypto.js';
4
+ /**
5
+ * signs a jwt using webcrypto.
6
+ *
7
+ * @param params signing parameters
8
+ * @returns signed jwt
9
+ */
10
+ export const signJwt = async (params) => {
11
+ const { header, payload, key, alg } = params;
12
+ const fullHeader = { ...header, alg };
13
+ const headerSegment = encodeSegment(fullHeader);
14
+ const payloadSegment = encodeSegment(payload);
15
+ const signingInput = `${headerSegment}.${payloadSegment}`;
16
+ const signature = await crypto.subtle.sign(getSignAlgorithm(alg), key, encodeUtf8(signingInput));
17
+ const signatureSegment = toBase64Url(new Uint8Array(signature));
18
+ return `${signingInput}.${signatureSegment}`;
19
+ };
20
+ /**
21
+ * verifies a jwt and returns its payload.
22
+ *
23
+ * @param jwt jwt string
24
+ * @param options verification options
25
+ * @returns decoded payload
26
+ */
27
+ export const verifyJwt = async (jwt, options) => {
28
+ const { key, alg, typ } = options;
29
+ const parts = jwt.split('.');
30
+ if (parts.length !== 3) {
31
+ throw new Error(`invalid jwt format`);
32
+ }
33
+ const header = decodeSegment(parts[0]);
34
+ if (header.alg !== alg) {
35
+ throw new Error(`invalid jwt alg`);
36
+ }
37
+ if (typ && header.typ !== typ) {
38
+ throw new Error(`invalid jwt typ`);
39
+ }
40
+ const payload = decodeSegment(parts[1]);
41
+ const signature = fromBase64Url(parts[2]);
42
+ const signingInput = `${parts[0]}.${parts[1]}`;
43
+ const ok = await crypto.subtle.verify(getSignAlgorithm(alg), key, signature, encodeUtf8(signingInput));
44
+ if (!ok) {
45
+ throw new Error(`invalid jwt signature`);
46
+ }
47
+ return payload;
48
+ };
49
+ const encodeSegment = (value) => {
50
+ return toBase64Url(encodeUtf8(JSON.stringify(value)));
51
+ };
52
+ const decodeSegment = (value) => {
53
+ const bytes = fromBase64Url(value);
54
+ return JSON.parse(decodeUtf8From(bytes));
55
+ };
56
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/jwt/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGzD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,EAAE,MAK7B,EAAmB,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;IAC7C,MAAM,UAAU,GAAG,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC;IACtC,MAAM,aAAa,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,YAAY,GAAG,GAAG,aAAa,IAAI,cAAc,EAAE,CAAC;IAE1D,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CACzC,gBAAgB,CAAC,GAAG,CAAC,EACrB,GAAG,EACH,UAAU,CAAC,YAAY,CAA4B,CACnD,CAAC;IAEF,MAAM,gBAAgB,GAAG,WAAW,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhE,OAAO,GAAG,YAAY,IAAI,gBAAgB,EAAE,CAAC;AAAA,CAC7C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC7B,GAAW,EACX,OAAgE,EAC7B,EAAE,CAAC;IACtC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;IAClC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAA0B,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChE,IAAI,MAAM,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACpC,CAAC;IAED,MAAM,OAAO,GAAG,aAAa,CAA0B,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAE/C,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CACpC,gBAAgB,CAAC,GAAG,CAAC,EACrB,GAAG,EACH,SAAS,EACT,UAAU,CAAC,YAAY,CAA4B,CACnD,CAAC;IAEF,IAAI,CAAC,EAAE,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC1C,CAAC;IAED,OAAO,OAAO,CAAC;AAAA,CACf,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE,CAAC;IACjD,OAAO,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAAA,CACtD,CAAC;AAEF,MAAM,aAAa,GAAG,CAAI,KAAa,EAAK,EAAE,CAAC;IAC9C,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAM,CAAC;AAAA,CAC9C,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ import { getCachedKeyMaterial } from '../internal/key-cache.js';
4
+ import { signJwt } from '../jwt/index.js';
5
+
6
+ import type { ClientAssertionPrivateJwk } from './types.js';
7
+
8
+ export interface CreateClientAssertionOptions {
9
+ /** client id */
10
+ client_id: string;
11
+ /** authorization server issuer */
12
+ aud: string;
13
+ /** JWK thumbprint of the DPoP key to bind to (for CAB pattern) */
14
+ jkt?: string;
15
+ /** client assertion signing key */
16
+ key: ClientAssertionPrivateJwk;
17
+ }
18
+
19
+ /**
20
+ * creates a DPoP-bound client assertion per RFC 7523.
21
+ *
22
+ * @param options creation options
23
+ * @returns signed client assertion JWT
24
+ */
25
+ export const createClientAssertion = async (options: CreateClientAssertionOptions): Promise<string> => {
26
+ const { client_id, aud, jkt, key } = options;
27
+ const { kid, alg } = key;
28
+ const { cryptoKey } = await getCachedKeyMaterial(key);
29
+
30
+ const now = Math.floor(Date.now() / 1000);
31
+ const cnf = jkt ? { jkt } : undefined;
32
+
33
+ return signJwt({
34
+ header: {
35
+ alg,
36
+ kid,
37
+ },
38
+ payload: {
39
+ iss: client_id,
40
+ sub: client_id,
41
+ aud: aud,
42
+ jti: nanoid(24),
43
+ iat: now,
44
+ exp: now + 60,
45
+ cnf,
46
+ },
47
+ key: cryptoKey,
48
+ alg,
49
+ });
50
+ };
@@ -0,0 +1,26 @@
1
+ import { getGenerateAlgorithm } from '../internal/crypto.js';
2
+ import { exportPrivateJwkFromKey } from '../internal/jwk.js';
3
+ import { setCachedKeyMaterial } from '../internal/key-cache.js';
4
+ import type { SigningAlgorithm } from '../jwk/types.js';
5
+
6
+ import type { ClientAssertionPrivateJwk } from './types.js';
7
+
8
+ /**
9
+ * generates a new client assertion private key.
10
+ *
11
+ * @param kid key id to assign
12
+ * @param alg signing algorithm (defaults to es256)
13
+ * @returns client assertion private JWK (with cache pre-warmed)
14
+ */
15
+ export const generateClientAssertionKey = async (
16
+ kid: string,
17
+ alg: SigningAlgorithm = 'ES256',
18
+ ): Promise<ClientAssertionPrivateJwk> => {
19
+ const pair = await crypto.subtle.generateKey(getGenerateAlgorithm(alg), true, ['sign', 'verify']);
20
+ const jwk = (await exportPrivateJwkFromKey(pair.privateKey, alg, kid)) as ClientAssertionPrivateJwk;
21
+
22
+ // pre-populate cache so we don't re-import
23
+ setCachedKeyMaterial(jwk, pair.privateKey);
24
+
25
+ return jwk;
26
+ };
@@ -0,0 +1,4 @@
1
+ export { createClientAssertion } from './create-client-assertion.js';
2
+ export { generateClientAssertionKey } from './generate-key.js';
3
+ export { importClientAssertionPkcs8 } from './keys.js';
4
+ export type { ClientAssertionPrivateJwk } from './types.js';
@@ -0,0 +1,26 @@
1
+ import { exportPrivateJwkFromKey, importPkcs8PrivateKey } from '../internal/jwk.js';
2
+ import { setCachedKeyMaterial } from '../internal/key-cache.js';
3
+ import type { SigningAlgorithm } from '../jwk/types.js';
4
+
5
+ import type { ClientAssertionPrivateJwk } from './types.js';
6
+
7
+ /**
8
+ * imports a client assertion private key from a pkcs8 pem string.
9
+ *
10
+ * @param pem pkcs8 pem string
11
+ * @param options import options (kid + alg)
12
+ * @returns client assertion private JWK (with cache pre-warmed)
13
+ */
14
+ export const importClientAssertionPkcs8 = async (
15
+ pem: string,
16
+ options: { kid: string; alg: SigningAlgorithm },
17
+ ): Promise<ClientAssertionPrivateJwk> => {
18
+ const { kid, alg } = options;
19
+ const cryptoKey = await importPkcs8PrivateKey(pem, alg);
20
+ const jwk = (await exportPrivateJwkFromKey(cryptoKey, alg, kid)) as ClientAssertionPrivateJwk;
21
+
22
+ // pre-populate cache so we don't re-import
23
+ setCachedKeyMaterial(jwk, cryptoKey);
24
+
25
+ return jwk;
26
+ };
@@ -0,0 +1,9 @@
1
+ import type { PrivateJwk, SigningAlgorithm } from '../jwk/types.js';
2
+
3
+ /**
4
+ * private jwk for client assertion signing.
5
+ */
6
+ export type ClientAssertionPrivateJwk = PrivateJwk & {
7
+ alg: SigningAlgorithm;
8
+ kid: string;
9
+ };
@@ -0,0 +1,140 @@
1
+ import { sha256Base64Url } from '../hash/sha256.js';
2
+
3
+ import { createDpopProofSigner } from './proof.js';
4
+ import type { DpopPrivateJwk, DpopNonceCache } from './types.js';
5
+
6
+ export interface CreateDpopFetchOptions {
7
+ /** DPoP private key (JWK with `alg` set) */
8
+ key: DpopPrivateJwk;
9
+ /** nonce store, keyed by origin */
10
+ nonces: DpopNonceCache;
11
+ /** server's supported DPoP signing algorithms */
12
+ supportedAlgs?: readonly string[];
13
+ /**
14
+ * is the target an authorization server (true) or resource server (false)?
15
+ * affects how `use_dpop_nonce` errors are detected.
16
+ */
17
+ isAuthServer?: boolean;
18
+ /** custom fetch implementation */
19
+ fetch?: typeof globalThis.fetch;
20
+ }
21
+
22
+ /**
23
+ * creates a fetch wrapper that adds DPoP proofs to requests.
24
+ *
25
+ * @param options DPoP configuration
26
+ * @returns fetch function with DPoP support
27
+ */
28
+ export const createDpopFetch = (options: CreateDpopFetchOptions): typeof globalThis.fetch => {
29
+ const { key, nonces, supportedAlgs, isAuthServer, fetch = globalThis.fetch } = options;
30
+
31
+ negotiateAlg(key, supportedAlgs);
32
+ const sign = createDpopProofSigner(key);
33
+
34
+ return async (input, init) => {
35
+ const request: Request = init == null && input instanceof Request ? input : new Request(input, init);
36
+
37
+ const authHeader = request.headers.get('Authorization');
38
+ const ath = authHeader?.startsWith('DPoP ') ? await sha256Base64Url(authHeader.slice(5)) : undefined;
39
+
40
+ const { origin } = new URL(request.url);
41
+ const htm = request.method;
42
+ const htu = buildHtu(request.url);
43
+
44
+ let initNonce: string | undefined;
45
+ try {
46
+ initNonce = await nonces.get(origin);
47
+ } catch {
48
+ // ignore get errors
49
+ }
50
+
51
+ const initProof = await sign(htm, htu, initNonce, ath);
52
+ request.headers.set('DPoP', initProof);
53
+
54
+ const initResponse = await fetch(request);
55
+
56
+ const nextNonce = initResponse.headers.get('DPoP-Nonce');
57
+ if (!nextNonce || nextNonce === initNonce) {
58
+ return initResponse;
59
+ }
60
+
61
+ try {
62
+ await nonces.set(origin, nextNonce);
63
+ } catch {
64
+ // ignore set errors
65
+ }
66
+
67
+ const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
68
+ if (!shouldRetry) {
69
+ return initResponse;
70
+ }
71
+
72
+ if (input === request || init?.body instanceof ReadableStream) {
73
+ return initResponse;
74
+ }
75
+
76
+ await initResponse.body?.cancel();
77
+
78
+ const nextProof = await sign(htm, htu, nextNonce, ath);
79
+ const nextRequest = new Request(input, init);
80
+ nextRequest.headers.set('DPoP', nextProof);
81
+
82
+ const retryResponse = await fetch(nextRequest);
83
+
84
+ const retryNonce = retryResponse.headers.get('DPoP-Nonce');
85
+ if (retryNonce && retryNonce !== nextNonce) {
86
+ try {
87
+ await nonces.set(origin, retryNonce);
88
+ } catch {
89
+ // ignore set errors
90
+ }
91
+ }
92
+
93
+ return retryResponse;
94
+ };
95
+ };
96
+
97
+ const buildHtu = (url: string): string => {
98
+ const fragmentIdx = url.indexOf('#');
99
+ const queryIdx = url.indexOf('?');
100
+ const end = fragmentIdx === -1 ? queryIdx : queryIdx === -1 ? fragmentIdx : Math.min(fragmentIdx, queryIdx);
101
+
102
+ return end === -1 ? url : url.slice(0, end);
103
+ };
104
+
105
+ const negotiateAlg = (key: DpopPrivateJwk, supportedAlgs?: readonly string[]): string => {
106
+ const keyAlg = key.alg;
107
+
108
+ if (supportedAlgs?.length) {
109
+ if (supportedAlgs.includes(keyAlg)) {
110
+ return keyAlg;
111
+ }
112
+ throw new Error(`DPoP key algorithm ${keyAlg} not supported by server: ${supportedAlgs.join(', ')}`);
113
+ }
114
+
115
+ return keyAlg;
116
+ };
117
+
118
+ const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean): Promise<boolean> => {
119
+ if (isAuthServer === undefined || isAuthServer === false) {
120
+ if (response.status === 401) {
121
+ const wwwAuth = response.headers.get('WWW-Authenticate');
122
+ if (wwwAuth?.startsWith('DPoP')) {
123
+ return wwwAuth.includes('error="use_dpop_nonce"');
124
+ }
125
+ }
126
+ }
127
+
128
+ if (isAuthServer === undefined || isAuthServer === true) {
129
+ if (response.status === 400) {
130
+ try {
131
+ const json = await response.clone().json();
132
+ return typeof json === 'object' && json?.error === 'use_dpop_nonce';
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+ }
138
+
139
+ return false;
140
+ };
@@ -0,0 +1,72 @@
1
+ import { getGenerateAlgorithm } from '../internal/crypto.js';
2
+ import { exportPrivateJwkFromKey, isSigningAlgorithm } from '../internal/jwk.js';
3
+ import { setCachedKeyMaterial } from '../internal/key-cache.js';
4
+ import type { SigningAlgorithm } from '../jwk/types.js';
5
+
6
+ import type { DpopPrivateJwk } from './types.js';
7
+
8
+ /**
9
+ * preferred algorithm order for DPoP key generation.
10
+ */
11
+ const PREFERRED_ALGORITHMS: readonly SigningAlgorithm[] = [
12
+ 'ES256',
13
+ 'ES384',
14
+ 'ES512',
15
+ 'PS256',
16
+ 'PS384',
17
+ 'PS512',
18
+ 'RS256',
19
+ 'RS384',
20
+ 'RS512',
21
+ ];
22
+
23
+ const sortAlgorithms = (algs: readonly SigningAlgorithm[]): SigningAlgorithm[] => {
24
+ return [...algs].sort((a, b) => {
25
+ const aIdx = PREFERRED_ALGORITHMS.indexOf(a);
26
+ const bIdx = PREFERRED_ALGORITHMS.indexOf(b);
27
+
28
+ if (aIdx === -1 && bIdx === -1) {
29
+ return 0;
30
+ }
31
+ if (aIdx === -1) {
32
+ return 1;
33
+ }
34
+ if (bIdx === -1) {
35
+ return -1;
36
+ }
37
+
38
+ return aIdx - bIdx;
39
+ });
40
+ };
41
+
42
+ /**
43
+ * generates a new DPoP private JWK with `alg` set.
44
+ *
45
+ * @param supportedAlgs server supported algorithms (optional)
46
+ * @returns private JWK (with cache pre-warmed)
47
+ */
48
+ export const generateDpopKey = async (supportedAlgs?: readonly string[]): Promise<DpopPrivateJwk> => {
49
+ const normalized = supportedAlgs?.filter(isSigningAlgorithm) ?? [];
50
+ if (supportedAlgs?.length && normalized.length === 0) {
51
+ throw new Error(`no supported algorithms provided`);
52
+ }
53
+
54
+ const algs: SigningAlgorithm[] = normalized.length ? sortAlgorithms(normalized) : ['ES256'];
55
+ const errors: unknown[] = [];
56
+
57
+ for (const alg of algs) {
58
+ try {
59
+ const pair = await crypto.subtle.generateKey(getGenerateAlgorithm(alg), true, ['sign', 'verify']);
60
+ const jwk = (await exportPrivateJwkFromKey(pair.privateKey, alg)) as DpopPrivateJwk;
61
+
62
+ // pre-populate cache so we don't re-import
63
+ setCachedKeyMaterial(jwk, pair.privateKey);
64
+
65
+ return jwk;
66
+ } catch (err) {
67
+ errors.push(err);
68
+ }
69
+ }
70
+
71
+ throw new AggregateError(errors, `failed to generate DPoP key for any of: ${algs.join(', ')}`);
72
+ };
@@ -0,0 +1,11 @@
1
+ export { createDpopFetch } from './fetch.js';
2
+ export { generateDpopKey } from './generate-key.js';
3
+ export { createDpopProofSigner } from './proof.js';
4
+ export type { DpopNonceCache, DpopPrivateJwk } from './types.js';
5
+ export {
6
+ DpopVerifyError,
7
+ verifyDpopProof,
8
+ type DpopClaims,
9
+ type DpopVerifyOptions,
10
+ type DpopVerifyResult,
11
+ } from './verify.js';
@@ -0,0 +1,46 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ import type { CachedKeyMaterial } from '../internal/key-cache.js';
4
+ import { getCachedKeyMaterial } from '../internal/key-cache.js';
5
+ import { signJwt } from '../jwt/index.js';
6
+
7
+ import type { DpopPrivateJwk } from './types.js';
8
+
9
+ /**
10
+ * creates a DPoP proof signer.
11
+ *
12
+ * @param jwk DPoP private JWK (with `alg` set)
13
+ * @returns signing function for DPoP proofs
14
+ */
15
+ export const createDpopProofSigner = (
16
+ jwk: DpopPrivateJwk,
17
+ ): ((htm: string, htu: string, nonce?: string, ath?: string) => Promise<string>) => {
18
+ const alg = jwk.alg;
19
+
20
+ // lazily resolve key material on first sign
21
+ let materialPromise: Promise<CachedKeyMaterial> | undefined;
22
+
23
+ return async (htm: string, htu: string, nonce?: string, ath?: string) => {
24
+ materialPromise ||= getCachedKeyMaterial(jwk);
25
+ const { cryptoKey, publicJwk } = await materialPromise;
26
+
27
+ const now = Math.floor(Date.now() / 1_000);
28
+
29
+ return signJwt({
30
+ header: {
31
+ typ: 'dpop+jwt',
32
+ jwk: publicJwk,
33
+ },
34
+ payload: {
35
+ htm,
36
+ htu,
37
+ iat: now,
38
+ jti: nanoid(24),
39
+ nonce,
40
+ ath,
41
+ },
42
+ key: cryptoKey,
43
+ alg,
44
+ });
45
+ };
46
+ };
@@ -0,0 +1,19 @@
1
+ import type { PrivateJwk, SigningAlgorithm } from '../jwk/types.js';
2
+
3
+ export type Awaitable<T> = T | Promise<T>;
4
+
5
+ /**
6
+ * private JWK for DPoP proofs.
7
+ */
8
+ export type DpopPrivateJwk = PrivateJwk & {
9
+ alg: SigningAlgorithm;
10
+ kid?: string;
11
+ };
12
+
13
+ /**
14
+ * nonce cache for DPoP fetch.
15
+ */
16
+ export interface DpopNonceCache {
17
+ get(key: string): Awaitable<string | undefined>;
18
+ set(key: string, value: string): Awaitable<void>;
19
+ }