@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.
- package/LICENSE +14 -0
- package/README.md +7 -0
- package/dist/client-assertion/create-client-assertion.d.ts +19 -0
- package/dist/client-assertion/create-client-assertion.d.ts.map +1 -0
- package/dist/client-assertion/create-client-assertion.js +34 -0
- package/dist/client-assertion/create-client-assertion.js.map +1 -0
- package/dist/client-assertion/generate-key.d.ts +11 -0
- package/dist/client-assertion/generate-key.d.ts.map +1 -0
- package/dist/client-assertion/generate-key.js +18 -0
- package/dist/client-assertion/generate-key.js.map +1 -0
- package/dist/client-assertion/index.d.ts +5 -0
- package/dist/client-assertion/index.d.ts.map +1 -0
- package/dist/client-assertion/index.js +4 -0
- package/dist/client-assertion/index.js.map +1 -0
- package/dist/client-assertion/keys.d.ts +14 -0
- package/dist/client-assertion/keys.d.ts.map +1 -0
- package/dist/client-assertion/keys.js +18 -0
- package/dist/client-assertion/keys.js.map +1 -0
- package/dist/client-assertion/types.d.ts +9 -0
- package/dist/client-assertion/types.d.ts.map +1 -0
- package/dist/client-assertion/types.js +2 -0
- package/dist/client-assertion/types.js.map +1 -0
- package/dist/dpop/fetch.d.ts +24 -0
- package/dist/dpop/fetch.d.ts.map +1 -0
- package/dist/dpop/fetch.js +102 -0
- package/dist/dpop/fetch.js.map +1 -0
- package/dist/dpop/generate-key.d.ts +9 -0
- package/dist/dpop/generate-key.d.ts.map +1 -0
- package/dist/dpop/generate-key.js +61 -0
- package/dist/dpop/generate-key.js.map +1 -0
- package/dist/dpop/index.d.ts +6 -0
- package/dist/dpop/index.d.ts.map +1 -0
- package/dist/dpop/index.js +5 -0
- package/dist/dpop/index.js.map +1 -0
- package/dist/dpop/proof.d.ts +9 -0
- package/dist/dpop/proof.d.ts.map +1 -0
- package/dist/dpop/proof.js +36 -0
- package/dist/dpop/proof.js.map +1 -0
- package/dist/dpop/types.d.ts +17 -0
- package/dist/dpop/types.d.ts.map +1 -0
- package/dist/dpop/types.js +2 -0
- package/dist/dpop/types.js.map +1 -0
- package/dist/dpop/verify.d.ts +42 -0
- package/dist/dpop/verify.d.ts.map +1 -0
- package/dist/dpop/verify.js +124 -0
- package/dist/dpop/verify.js.map +1 -0
- package/dist/hash/index.d.ts +3 -0
- package/dist/hash/index.d.ts.map +1 -0
- package/dist/hash/index.js +3 -0
- package/dist/hash/index.js.map +1 -0
- package/dist/hash/pkce.d.ts +12 -0
- package/dist/hash/pkce.d.ts.map +1 -0
- package/dist/hash/pkce.js +14 -0
- package/dist/hash/pkce.js.map +1 -0
- package/dist/hash/sha256.d.ts +8 -0
- package/dist/hash/sha256.d.ts.map +1 -0
- package/dist/hash/sha256.js +14 -0
- package/dist/hash/sha256.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/crypto.d.ts +7 -0
- package/dist/internal/crypto.d.ts.map +1 -0
- package/dist/internal/crypto.js +78 -0
- package/dist/internal/crypto.js.map +1 -0
- package/dist/internal/jwk.d.ts +10 -0
- package/dist/internal/jwk.d.ts.map +1 -0
- package/dist/internal/jwk.js +121 -0
- package/dist/internal/jwk.js.map +1 -0
- package/dist/internal/key-cache.d.ts +24 -0
- package/dist/internal/key-cache.d.ts.map +1 -0
- package/dist/internal/key-cache.js +36 -0
- package/dist/internal/key-cache.js.map +1 -0
- package/dist/jwk/compute-jkt.d.ts +9 -0
- package/dist/jwk/compute-jkt.d.ts.map +1 -0
- package/dist/jwk/compute-jkt.js +23 -0
- package/dist/jwk/compute-jkt.js.map +1 -0
- package/dist/jwk/index.d.ts +5 -0
- package/dist/jwk/index.d.ts.map +1 -0
- package/dist/jwk/index.js +4 -0
- package/dist/jwk/index.js.map +1 -0
- package/dist/jwk/keys.d.ts +9 -0
- package/dist/jwk/keys.d.ts.map +1 -0
- package/dist/jwk/keys.js +13 -0
- package/dist/jwk/keys.js.map +1 -0
- package/dist/jwk/types.d.ts +37 -0
- package/dist/jwk/types.d.ts.map +1 -0
- package/dist/jwk/types.js +2 -0
- package/dist/jwk/types.js.map +1 -0
- package/dist/jwt/index.d.ts +26 -0
- package/dist/jwt/index.d.ts.map +1 -0
- package/dist/jwt/index.js +56 -0
- package/dist/jwt/index.js.map +1 -0
- package/lib/client-assertion/create-client-assertion.ts +50 -0
- package/lib/client-assertion/generate-key.ts +26 -0
- package/lib/client-assertion/index.ts +4 -0
- package/lib/client-assertion/keys.ts +26 -0
- package/lib/client-assertion/types.ts +9 -0
- package/lib/dpop/fetch.ts +140 -0
- package/lib/dpop/generate-key.ts +72 -0
- package/lib/dpop/index.ts +11 -0
- package/lib/dpop/proof.ts +46 -0
- package/lib/dpop/types.ts +19 -0
- package/lib/dpop/verify.ts +169 -0
- package/lib/hash/index.ts +2 -0
- package/lib/hash/pkce.ts +18 -0
- package/lib/hash/sha256.ts +14 -0
- package/lib/index.ts +5 -0
- package/lib/internal/crypto.ts +92 -0
- package/lib/internal/jwk.ts +157 -0
- package/lib/internal/key-cache.ts +51 -0
- package/lib/jwk/compute-jkt.ts +27 -0
- package/lib/jwk/index.ts +12 -0
- package/lib/jwk/keys.ts +15 -0
- package/lib/jwk/types.ts +51 -0
- package/lib/jwt/index.ts +86 -0
- package/package.json +38 -0
package/dist/jwk/keys.js
ADDED
|
@@ -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 @@
|
|
|
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":"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"}
|
|
@@ -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,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,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
|
+
}
|