@atproto/oauth-client 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 (111) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +124 -0
  4. package/dist/constants.d.ts +5 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +8 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/fetch-dpop.d.ts +21 -0
  9. package/dist/fetch-dpop.d.ts.map +1 -0
  10. package/dist/fetch-dpop.js +149 -0
  11. package/dist/fetch-dpop.js.map +1 -0
  12. package/dist/index.d.ts +15 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +35 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/lock.d.ts +2 -0
  17. package/dist/lock.d.ts.map +1 -0
  18. package/dist/lock.js +33 -0
  19. package/dist/lock.js.map +1 -0
  20. package/dist/oauth-agent.d.ts +29 -0
  21. package/dist/oauth-agent.d.ts.map +1 -0
  22. package/dist/oauth-agent.js +138 -0
  23. package/dist/oauth-agent.js.map +1 -0
  24. package/dist/oauth-authorization-server-metadata-resolver.d.ts +15 -0
  25. package/dist/oauth-authorization-server-metadata-resolver.d.ts.map +1 -0
  26. package/dist/oauth-authorization-server-metadata-resolver.js +56 -0
  27. package/dist/oauth-authorization-server-metadata-resolver.js.map +1 -0
  28. package/dist/oauth-callback-error.d.ts +7 -0
  29. package/dist/oauth-callback-error.d.ts.map +1 -0
  30. package/dist/oauth-callback-error.js +28 -0
  31. package/dist/oauth-callback-error.js.map +1 -0
  32. package/dist/oauth-client.d.ts +78 -0
  33. package/dist/oauth-client.d.ts.map +1 -0
  34. package/dist/oauth-client.js +278 -0
  35. package/dist/oauth-client.js.map +1 -0
  36. package/dist/oauth-protected-resource-metadata-resolver.d.ts +15 -0
  37. package/dist/oauth-protected-resource-metadata-resolver.d.ts.map +1 -0
  38. package/dist/oauth-protected-resource-metadata-resolver.js +58 -0
  39. package/dist/oauth-protected-resource-metadata-resolver.js.map +1 -0
  40. package/dist/oauth-resolver-error.d.ts +7 -0
  41. package/dist/oauth-resolver-error.d.ts.map +1 -0
  42. package/dist/oauth-resolver-error.js +17 -0
  43. package/dist/oauth-resolver-error.js.map +1 -0
  44. package/dist/oauth-resolver.d.ts +62 -0
  45. package/dist/oauth-resolver.d.ts.map +1 -0
  46. package/dist/oauth-resolver.js +73 -0
  47. package/dist/oauth-resolver.js.map +1 -0
  48. package/dist/oauth-response-error.d.ts +11 -0
  49. package/dist/oauth-response-error.d.ts.map +1 -0
  50. package/dist/oauth-response-error.js +48 -0
  51. package/dist/oauth-response-error.js.map +1 -0
  52. package/dist/oauth-server-agent.d.ts +51 -0
  53. package/dist/oauth-server-agent.d.ts.map +1 -0
  54. package/dist/oauth-server-agent.js +228 -0
  55. package/dist/oauth-server-agent.js.map +1 -0
  56. package/dist/oauth-server-factory.d.ts +20 -0
  57. package/dist/oauth-server-factory.d.ts.map +1 -0
  58. package/dist/oauth-server-factory.js +53 -0
  59. package/dist/oauth-server-factory.js.map +1 -0
  60. package/dist/refresh-error.d.ts +7 -0
  61. package/dist/refresh-error.d.ts.map +1 -0
  62. package/dist/refresh-error.js +16 -0
  63. package/dist/refresh-error.js.map +1 -0
  64. package/dist/runtime-implementation.d.ts +12 -0
  65. package/dist/runtime-implementation.d.ts.map +1 -0
  66. package/dist/runtime-implementation.js +3 -0
  67. package/dist/runtime-implementation.js.map +1 -0
  68. package/dist/runtime.d.ts +35 -0
  69. package/dist/runtime.d.ts.map +1 -0
  70. package/dist/runtime.js +185 -0
  71. package/dist/runtime.js.map +1 -0
  72. package/dist/session-getter.d.ts +30 -0
  73. package/dist/session-getter.d.ts.map +1 -0
  74. package/dist/session-getter.js +149 -0
  75. package/dist/session-getter.js.map +1 -0
  76. package/dist/types.d.ts +1580 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +8 -0
  79. package/dist/types.js.map +1 -0
  80. package/dist/util.d.ts +9 -0
  81. package/dist/util.d.ts.map +1 -0
  82. package/dist/util.js +35 -0
  83. package/dist/util.js.map +1 -0
  84. package/dist/validate-client-metadata.d.ts +5 -0
  85. package/dist/validate-client-metadata.d.ts.map +1 -0
  86. package/dist/validate-client-metadata.js +46 -0
  87. package/dist/validate-client-metadata.js.map +1 -0
  88. package/package.json +46 -0
  89. package/src/constants.ts +4 -0
  90. package/src/fetch-dpop.ts +235 -0
  91. package/src/index.ts +18 -0
  92. package/src/lock.ts +34 -0
  93. package/src/oauth-agent.ts +150 -0
  94. package/src/oauth-authorization-server-metadata-resolver.ts +98 -0
  95. package/src/oauth-callback-error.ts +16 -0
  96. package/src/oauth-client.ts +440 -0
  97. package/src/oauth-protected-resource-metadata-resolver.ts +102 -0
  98. package/src/oauth-resolver-error.ts +12 -0
  99. package/src/oauth-resolver.ts +111 -0
  100. package/src/oauth-response-error.ts +31 -0
  101. package/src/oauth-server-agent.ts +275 -0
  102. package/src/oauth-server-factory.ts +41 -0
  103. package/src/refresh-error.ts +9 -0
  104. package/src/runtime-implementation.ts +17 -0
  105. package/src/runtime.ts +211 -0
  106. package/src/session-getter.ts +182 -0
  107. package/src/types.ts +26 -0
  108. package/src/util.ts +51 -0
  109. package/src/validate-client-metadata.ts +61 -0
  110. package/tsconfig.build.json +8 -0
  111. package/tsconfig.json +4 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clientMetadataSchema = void 0;
4
+ const oauth_types_1 = require("@atproto/oauth-types");
5
+ exports.clientMetadataSchema = oauth_types_1.oauthClientMetadataSchema.extend({
6
+ client_id: oauth_types_1.oauthClientIdSchema.url(),
7
+ });
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAAA,sDAG6B;AAkBhB,QAAA,oBAAoB,GAAG,uCAAyB,CAAC,MAAM,CAAC;IACnE,SAAS,EAAE,iCAAmB,CAAC,GAAG,EAAE;CACrC,CAAC,CAAA"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @todo (?) move to common package
3
+ */
4
+ export declare const withSignal: <T>(options: undefined | {
5
+ signal?: AbortSignal;
6
+ timeout: number;
7
+ }, fn: (signal: AbortSignal) => T | PromiseLike<T>) => Promise<T>;
8
+ export declare function contentMime(headers: Headers): string | undefined;
9
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,UAAU,eAEjB,SAAS,GACT;IACE,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;CAChB,MACD,CAAC,MAAM,EAAE,WAAW,KAAK,CAAC,GAAG,YAAY,CAAC,CAAC,KAC9C,QAAQ,CAAC,CAmCX,CAAA;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEhE"}
package/dist/util.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.contentMime = exports.withSignal = void 0;
4
+ /**
5
+ * @todo (?) move to common package
6
+ */
7
+ const withSignal = async (options, fn) => {
8
+ options?.signal?.throwIfAborted();
9
+ const abortController = new AbortController();
10
+ const { signal } = abortController;
11
+ options?.signal?.addEventListener('abort', (reason) => abortController.abort(reason), { once: true, signal });
12
+ if (options?.timeout != null) {
13
+ const timeoutId = setTimeout((err) => abortController.abort(err), options.timeout, new Error('Timeout'));
14
+ timeoutId.unref?.(); // NodeJS only
15
+ signal.addEventListener('abort', () => clearTimeout(timeoutId), {
16
+ once: true,
17
+ signal,
18
+ });
19
+ }
20
+ try {
21
+ return await fn(signal);
22
+ }
23
+ finally {
24
+ // - Remove listener on incoming signal
25
+ // - Cancel timeout
26
+ // - Cancel pending (async) tasks
27
+ abortController.abort();
28
+ }
29
+ };
30
+ exports.withSignal = withSignal;
31
+ function contentMime(headers) {
32
+ return headers.get('content-type')?.split(';')[0].trim();
33
+ }
34
+ exports.contentMime = contentMime;
35
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAAA;;GAEG;AACI,MAAM,UAAU,GAAG,KAAK,EAC7B,OAKK,EACL,EAA+C,EACnC,EAAE;IACd,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA;IAEjC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA;IAC7C,MAAM,EAAE,MAAM,EAAE,GAAG,eAAe,CAAA;IAElC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAC/B,OAAO,EACP,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,EACzC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CACvB,CAAA;IAED,IAAI,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,UAAU,CAC1B,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,EACnC,OAAO,CAAC,OAAO,EACf,IAAI,KAAK,CAAC,SAAS,CAAC,CACrB,CAAA;QAED,SAAS,CAAC,KAAK,EAAE,EAAE,CAAA,CAAC,cAAc;QAElC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE;YAC9D,IAAI,EAAE,IAAI;YACV,MAAM;SACP,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,MAAM,CAAC,CAAA;IACzB,CAAC;YAAS,CAAC;QACT,uCAAuC;QACvC,mBAAmB;QACnB,iCAAiC;QACjC,eAAe,CAAC,KAAK,EAAE,CAAA;IACzB,CAAC;AACH,CAAC,CAAA;AA3CY,QAAA,UAAU,cA2CtB;AAED,SAAgB,WAAW,CAAC,OAAgB;IAC1C,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAA;AAC3D,CAAC;AAFD,kCAEC"}
@@ -0,0 +1,5 @@
1
+ import { Keyset } from '@atproto/jwk';
2
+ import { OAuthClientMetadataInput } from '@atproto/oauth-types';
3
+ import { ClientMetadata } from './types.js';
4
+ export declare function validateClientMetadata(input: OAuthClientMetadataInput, keyset?: Keyset): ClientMetadata;
5
+ //# sourceMappingURL=validate-client-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-client-metadata.d.ts","sourceRoot":"","sources":["../src/validate-client-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAEL,wBAAwB,EACzB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,cAAc,EAAwB,MAAM,YAAY,CAAA;AAQjE,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,wBAAwB,EAC/B,MAAM,CAAC,EAAE,MAAM,GACd,cAAc,CA2ChB"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateClientMetadata = void 0;
4
+ const oauth_types_1 = require("@atproto/oauth-types");
5
+ const types_js_1 = require("./types.js");
6
+ // Improve bundle size by using concatenation
7
+ const _ENDPOINT_AUTH_METHOD = '_endpoint_auth_method';
8
+ const _ENDPOINT_AUTH_SIGNING_ALG = '_endpoint_auth_signing_alg';
9
+ const TOKEN_ENDPOINT_AUTH_METHOD = `token${_ENDPOINT_AUTH_METHOD}`;
10
+ function validateClientMetadata(input, keyset) {
11
+ const metadata = types_js_1.clientMetadataSchema.parse(input);
12
+ // ATPROTO uses client metadata discovery
13
+ try {
14
+ new URL(metadata.client_id);
15
+ }
16
+ catch (cause) {
17
+ throw new TypeError(`client_id must be a valid URL`, { cause });
18
+ }
19
+ if (!metadata[TOKEN_ENDPOINT_AUTH_METHOD]) {
20
+ throw new TypeError(`${TOKEN_ENDPOINT_AUTH_METHOD} must be provided`);
21
+ }
22
+ for (const endpointName of oauth_types_1.OAUTH_AUTHENTICATED_ENDPOINT_NAMES) {
23
+ const method = metadata[`${endpointName}${_ENDPOINT_AUTH_METHOD}`];
24
+ switch (method) {
25
+ case undefined:
26
+ case 'none':
27
+ if (metadata[`${endpointName}${_ENDPOINT_AUTH_SIGNING_ALG}`]) {
28
+ throw new TypeError(`${endpointName}${_ENDPOINT_AUTH_SIGNING_ALG} must not be provided`);
29
+ }
30
+ break;
31
+ case 'client_secret_jwt':
32
+ if (!keyset) {
33
+ throw new TypeError(`Keyset is required for ${method} method`);
34
+ }
35
+ if (!metadata[`${endpointName}${_ENDPOINT_AUTH_SIGNING_ALG}`]) {
36
+ throw new TypeError(`${endpointName}${_ENDPOINT_AUTH_SIGNING_ALG} must be provided`);
37
+ }
38
+ break;
39
+ default:
40
+ throw new TypeError(`Invalid "${endpointName}${_ENDPOINT_AUTH_METHOD}" value: ${method}`);
41
+ }
42
+ }
43
+ return metadata;
44
+ }
45
+ exports.validateClientMetadata = validateClientMetadata;
46
+ //# sourceMappingURL=validate-client-metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-client-metadata.js","sourceRoot":"","sources":["../src/validate-client-metadata.ts"],"names":[],"mappings":";;;AACA,sDAG6B;AAE7B,yCAAiE;AAEjE,6CAA6C;AAC7C,MAAM,qBAAqB,GAAG,uBAAuB,CAAA;AACrD,MAAM,0BAA0B,GAAG,4BAA4B,CAAA;AAE/D,MAAM,0BAA0B,GAAG,QAAQ,qBAAqB,EAAE,CAAA;AAElE,SAAgB,sBAAsB,CACpC,KAA+B,EAC/B,MAAe;IAEf,MAAM,QAAQ,GAAG,+BAAoB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAElD,yCAAyC;IACzC,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,+BAA+B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACjE,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,GAAG,0BAA0B,mBAAmB,CAAC,CAAA;IACvE,CAAC;IAED,KAAK,MAAM,YAAY,IAAI,gDAAkC,EAAE,CAAC;QAC9D,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,YAAY,GAAG,qBAAqB,EAAE,CAAC,CAAA;QAClE,QAAQ,MAAM,EAAE,CAAC;YACf,KAAK,SAAS,CAAC;YACf,KAAK,MAAM;gBACT,IAAI,QAAQ,CAAC,GAAG,YAAY,GAAG,0BAA0B,EAAE,CAAC,EAAE,CAAC;oBAC7D,MAAM,IAAI,SAAS,CACjB,GAAG,YAAY,GAAG,0BAA0B,uBAAuB,CACpE,CAAA;gBACH,CAAC;gBACD,MAAK;YACP,KAAK,mBAAmB;gBACtB,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,MAAM,IAAI,SAAS,CAAC,0BAA0B,MAAM,SAAS,CAAC,CAAA;gBAChE,CAAC;gBACD,IAAI,CAAC,QAAQ,CAAC,GAAG,YAAY,GAAG,0BAA0B,EAAE,CAAC,EAAE,CAAC;oBAC9D,MAAM,IAAI,SAAS,CACjB,GAAG,YAAY,GAAG,0BAA0B,mBAAmB,CAChE,CAAA;gBACH,CAAC;gBACD,MAAK;YACP;gBACE,MAAM,IAAI,SAAS,CACjB,YAAY,YAAY,GAAG,qBAAqB,YAAY,MAAM,EAAE,CACrE,CAAA;QACL,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AA9CD,wDA8CC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@atproto/oauth-client",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).",
6
+ "keywords": [
7
+ "atproto",
8
+ "oauth",
9
+ "client",
10
+ "isomorphic"
11
+ ],
12
+ "homepage": "https://atproto.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/bluesky-social/atproto",
16
+ "directory": "packages/oauth/oauth-client"
17
+ },
18
+ "type": "commonjs",
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "multiformats": "^9.9.0",
29
+ "zod": "^3.23.8",
30
+ "@atproto-labs/did-resolver": "0.1.0",
31
+ "@atproto-labs/fetch": "0.1.0",
32
+ "@atproto-labs/handle-resolver": "0.1.0",
33
+ "@atproto-labs/identity-resolver": "0.1.0",
34
+ "@atproto-labs/simple-store": "0.1.0",
35
+ "@atproto-labs/simple-store-memory": "0.1.0",
36
+ "@atproto/did": "0.1.0",
37
+ "@atproto/jwk": "0.1.0",
38
+ "@atproto/oauth-types": "0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.3.3"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc --build tsconfig.build.json"
45
+ }
46
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Per ATProto spec (OpenID uses RS256)
3
+ */
4
+ export const FALLBACK_ALG = 'ES256'
@@ -0,0 +1,235 @@
1
+ import { Fetch, FetchContext, cancelBody, peekJson } from '@atproto-labs/fetch'
2
+ import { SimpleStore } from '@atproto-labs/simple-store'
3
+ import { Key } from '@atproto/jwk'
4
+ import { base64url } from 'multiformats/bases/base64'
5
+
6
+ // "undefined" in non https environments or environments without crypto
7
+ const subtle = globalThis.crypto?.subtle as SubtleCrypto | undefined
8
+
9
+ const ReadableStream = globalThis.ReadableStream as
10
+ | typeof globalThis.ReadableStream
11
+ | undefined
12
+
13
+ export type DpopFetchWrapperOptions<C = FetchContext> = {
14
+ key: Key
15
+ iss: string
16
+ nonces: SimpleStore<string, string>
17
+ supportedAlgs?: string[]
18
+ sha256?: (input: string) => Promise<string>
19
+
20
+ /**
21
+ * Is the intended server an authorization server (true) or a resource server
22
+ * (false)? Setting this may allow to avoid parsing the response body to
23
+ * determine the dpop-nonce.
24
+ *
25
+ * @default undefined
26
+ */
27
+ isAuthServer?: boolean
28
+ fetch?: Fetch<C>
29
+ }
30
+
31
+ export function dpopFetchWrapper<C = FetchContext>({
32
+ key,
33
+ iss,
34
+ supportedAlgs,
35
+ nonces,
36
+ sha256 = typeof subtle !== 'undefined' ? subtleSha256 : undefined,
37
+ isAuthServer,
38
+ fetch = globalThis.fetch,
39
+ }: DpopFetchWrapperOptions<C>): Fetch<C> {
40
+ if (!sha256) {
41
+ throw new TypeError(
42
+ `crypto.subtle is not available in this environment. Please provide a sha256 function.`,
43
+ )
44
+ }
45
+
46
+ const alg = negotiateAlg(key, supportedAlgs)
47
+
48
+ return async function (this: C, input, init) {
49
+ if (!key.algorithms.includes(alg)) {
50
+ throw new TypeError(`Key does not support the algorithm ${alg}`)
51
+ }
52
+
53
+ const request: Request =
54
+ init == null && input instanceof Request
55
+ ? input
56
+ : new Request(input, init)
57
+
58
+ const authorizationHeader = request.headers.get('Authorization')
59
+ const ath = authorizationHeader?.startsWith('DPoP ')
60
+ ? await sha256(authorizationHeader.slice(5))
61
+ : undefined
62
+
63
+ const { method, url } = request
64
+ const { origin } = new URL(url)
65
+
66
+ let initNonce: string | undefined
67
+ try {
68
+ initNonce = await nonces.get(origin)
69
+ } catch {
70
+ // Ignore get errors, we will just not send a nonce
71
+ }
72
+
73
+ const initProof = await buildProof(
74
+ key,
75
+ alg,
76
+ iss,
77
+ method,
78
+ url,
79
+ initNonce,
80
+ ath,
81
+ )
82
+ request.headers.set('DPoP', initProof)
83
+
84
+ const initResponse = await fetch.call(this, request)
85
+
86
+ // Make sure the response body is consumed. Either by the caller (when the
87
+ // response is returned), of if an error is thrown (catch block).
88
+
89
+ const nextNonce = initResponse.headers.get('DPoP-Nonce')
90
+ if (!nextNonce || nextNonce === initNonce) {
91
+ // No nonce was returned or it is the same as the one we sent. No need to
92
+ // update the nonce store, or retry the request.
93
+ return initResponse
94
+ }
95
+
96
+ // Store the fresh nonce for future requests
97
+ try {
98
+ await nonces.set(origin, nextNonce)
99
+ } catch {
100
+ // Ignore set errors
101
+ }
102
+
103
+ const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer)
104
+ if (!shouldRetry) {
105
+ // Not a "use_dpop_nonce" error, so there is no need to retry
106
+ return initResponse
107
+ }
108
+
109
+ // If the input stream was already consumed, we cannot retry the request. A
110
+ // solution would be to clone() the request but that would bufferize the
111
+ // entire stream in memory which can lead to memory starvation. Instead, we
112
+ // will return the original response and let the calling code handle retries.
113
+
114
+ if (input === request) {
115
+ // The input request body was consumed. We cannot retry the request.
116
+ return initResponse
117
+ }
118
+
119
+ if (ReadableStream && init?.body instanceof ReadableStream) {
120
+ // The init body was consumed. We cannot retry the request.
121
+ return initResponse
122
+ }
123
+
124
+ // We will now retry the request with the fresh nonce.
125
+
126
+ // The initial response body must be consumed (see cancelBody's doc).
127
+ await cancelBody(initResponse, 'log')
128
+
129
+ const nextProof = await buildProof(
130
+ key,
131
+ alg,
132
+ iss,
133
+ method,
134
+ url,
135
+ nextNonce,
136
+ ath,
137
+ )
138
+ const nextRequest = new Request(input, init)
139
+ nextRequest.headers.set('DPoP', nextProof)
140
+
141
+ return fetch.call(this, nextRequest)
142
+ }
143
+ }
144
+
145
+ async function buildProof(
146
+ key: Key,
147
+ alg: string,
148
+ iss: string,
149
+ htm: string,
150
+ htu: string,
151
+ nonce?: string,
152
+ ath?: string,
153
+ ) {
154
+ if (!key.bareJwk) {
155
+ throw new Error('Only asymmetric keys can be used as DPoP proofs')
156
+ }
157
+
158
+ const now = Math.floor(Date.now() / 1e3)
159
+
160
+ return key.createJwt(
161
+ {
162
+ alg,
163
+ typ: 'dpop+jwt',
164
+ jwk: key.bareJwk,
165
+ },
166
+ {
167
+ iss,
168
+ iat: now,
169
+ exp: now + 10,
170
+ // Any collision will cause the request to be rejected by the server. no biggie.
171
+ jti: Math.random().toString(36).slice(2),
172
+ htm,
173
+ htu,
174
+ nonce,
175
+ ath,
176
+ },
177
+ )
178
+ }
179
+
180
+ async function isUseDpopNonceError(
181
+ response: Response,
182
+ isAuthServer?: boolean,
183
+ ): Promise<boolean> {
184
+ // https://datatracker.ietf.org/doc/html/rfc6750#section-3
185
+ // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
186
+ if (isAuthServer === undefined || isAuthServer === false) {
187
+ if (response.status === 401) {
188
+ const wwwAuth = response.headers.get('WWW-Authenticate')
189
+ if (wwwAuth?.startsWith('DPoP')) {
190
+ return wwwAuth.includes('error="use_dpop_nonce"')
191
+ }
192
+ }
193
+ }
194
+
195
+ // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
196
+ if (isAuthServer === undefined || isAuthServer === true) {
197
+ if (response.status === 400) {
198
+ try {
199
+ const json = await peekJson(response, 10 * 1024)
200
+ return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce'
201
+ } catch {
202
+ // Response too big (to be "use_dpop_nonce" error) or invalid JSON
203
+ return false
204
+ }
205
+ }
206
+ }
207
+
208
+ return false
209
+ }
210
+
211
+ function negotiateAlg(key: Key, supportedAlgs: string[] | undefined): string {
212
+ if (supportedAlgs) {
213
+ // Use order of supportedAlgs as preference
214
+ const alg = supportedAlgs.find((a) => key.algorithms.includes(a))
215
+ if (alg) return alg
216
+ } else {
217
+ const [alg] = key.algorithms
218
+ if (alg) return alg
219
+ }
220
+
221
+ throw new Error('Key does not match any alg supported by the server')
222
+ }
223
+
224
+ async function subtleSha256(input: string): Promise<string> {
225
+ if (subtle == null) {
226
+ throw new Error(
227
+ `crypto.subtle is not available in this environment. Please provide a sha256 function.`,
228
+ )
229
+ }
230
+
231
+ const bytes = new TextEncoder().encode(input)
232
+ const digest = await subtle.digest('SHA-256', bytes)
233
+ const digestBytes = new Uint8Array(digest)
234
+ return base64url.baseEncode(digestBytes)
235
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export {
2
+ FetchError,
3
+ FetchRequestError,
4
+ FetchResponseError,
5
+ } from '@atproto-labs/fetch'
6
+ export * from './oauth-agent.js'
7
+ export * from './oauth-authorization-server-metadata-resolver.js'
8
+ export * from './oauth-callback-error.js'
9
+ export * from './oauth-client.js'
10
+ export * from './oauth-protected-resource-metadata-resolver.js'
11
+ export * from './oauth-resolver-error.js'
12
+ export * from './oauth-response-error.js'
13
+ export * from './oauth-server-agent.js'
14
+ export * from './oauth-server-factory.js'
15
+ export * from './refresh-error.js'
16
+ export * from './runtime-implementation.js'
17
+ export * from './session-getter.js'
18
+ export * from './types.js'
package/src/lock.ts ADDED
@@ -0,0 +1,34 @@
1
+ const locks = new Map<unknown, Promise<void>>()
2
+
3
+ function acquireLocalLock(name: unknown): Promise<() => void> {
4
+ return new Promise((resolveAcquire) => {
5
+ const prev = locks.get(name) ?? Promise.resolve()
6
+ const next = prev.then(() => {
7
+ return new Promise<void>((resolveRelease) => {
8
+ const release = () => {
9
+ // Only delete the lock if it is still the current one
10
+ if (locks.get(name) === next) locks.delete(name)
11
+
12
+ resolveRelease()
13
+ }
14
+
15
+ resolveAcquire(release)
16
+ })
17
+ })
18
+
19
+ locks.set(name, next)
20
+ })
21
+ }
22
+
23
+ export function requestLocalLock<T>(
24
+ name: string,
25
+ fn: () => T | PromiseLike<T>,
26
+ ): Promise<T> {
27
+ return acquireLocalLock(name).then(async (release) => {
28
+ try {
29
+ return await fn()
30
+ } finally {
31
+ release()
32
+ }
33
+ })
34
+ }
@@ -0,0 +1,150 @@
1
+ import { Fetch, bindFetch } from '@atproto-labs/fetch'
2
+ import { JwtPayload, unsafeDecodeJwt } from '@atproto/jwk'
3
+ import { OAuthAuthorizationServerMetadata } from '@atproto/oauth-types'
4
+
5
+ import { dpopFetchWrapper } from './fetch-dpop.js'
6
+ import { OAuthServerAgent, TokenSet } from './oauth-server-agent.js'
7
+ import { SessionGetter } from './session-getter.js'
8
+
9
+ const ReadableStream = globalThis.ReadableStream as
10
+ | typeof globalThis.ReadableStream
11
+ | undefined
12
+
13
+ export class OAuthAgent {
14
+ protected dpopFetch: Fetch<unknown>
15
+
16
+ constructor(
17
+ public readonly server: OAuthServerAgent,
18
+ public readonly sub: string,
19
+ private readonly sessionGetter: SessionGetter,
20
+ fetch: Fetch = globalThis.fetch,
21
+ ) {
22
+ this.dpopFetch = dpopFetchWrapper<void>({
23
+ fetch: bindFetch(fetch),
24
+ iss: server.clientMetadata.client_id,
25
+ key: server.dpopKey,
26
+ supportedAlgs: server.serverMetadata.dpop_signing_alg_values_supported,
27
+ sha256: async (v) => server.runtime.sha256(v),
28
+ nonces: server.dpopNonces,
29
+ isAuthServer: false,
30
+ })
31
+ }
32
+
33
+ get serverMetadata(): Readonly<OAuthAuthorizationServerMetadata> {
34
+ return this.server.serverMetadata
35
+ }
36
+
37
+ public async refreshIfNeeded(): Promise<void> {
38
+ await this.getTokenSet(undefined)
39
+ }
40
+
41
+ /**
42
+ * @param refresh See {@link SessionGetter.getSession}
43
+ */
44
+ protected async getTokenSet(refresh?: boolean): Promise<TokenSet> {
45
+ const { tokenSet } = await this.sessionGetter.getSession(this.sub, refresh)
46
+ return tokenSet
47
+ }
48
+
49
+ async getInfo(): Promise<{
50
+ userinfo?: JwtPayload
51
+ expired?: boolean
52
+ scope?: string
53
+ iss: string
54
+ aud: string
55
+ sub: string
56
+ }> {
57
+ const tokenSet = await this.getTokenSet()
58
+
59
+ return {
60
+ userinfo: tokenSet.id_token
61
+ ? unsafeDecodeJwt(tokenSet.id_token).payload
62
+ : undefined,
63
+ expired:
64
+ tokenSet.expires_at == null
65
+ ? undefined
66
+ : new Date(tokenSet.expires_at).getTime() < Date.now() - 5e3,
67
+ scope: tokenSet.scope,
68
+ iss: tokenSet.iss,
69
+ aud: tokenSet.aud,
70
+ sub: tokenSet.sub,
71
+ }
72
+ }
73
+
74
+ async signOut(): Promise<void> {
75
+ try {
76
+ const { tokenSet } = await this.sessionGetter.getSession(this.sub, false)
77
+ await this.server.revoke(tokenSet.access_token)
78
+ } finally {
79
+ await this.sessionGetter.delStored(this.sub)
80
+ }
81
+ }
82
+
83
+ async request(pathname: string, init?: RequestInit): Promise<Response> {
84
+ // This will try and refresh the token if it is known to be expired
85
+ const tokenSet = await this.getTokenSet(undefined)
86
+
87
+ const initialUrl = new URL(pathname, tokenSet.aud)
88
+ const initialAuth = `${tokenSet.token_type} ${tokenSet.access_token}`
89
+
90
+ const headers = new Headers(init?.headers)
91
+ headers.set('Authorization', initialAuth)
92
+
93
+ const initialResponse = await this.dpopFetch(initialUrl, {
94
+ ...init,
95
+ headers,
96
+ })
97
+
98
+ // If the token is not expired, we don't need to refresh it
99
+ if (!isTokenExpiredResponse(initialResponse)) {
100
+ return initialResponse
101
+ }
102
+
103
+ let tokenSetFresh: TokenSet
104
+ try {
105
+ // "true" here will cause the token to be refreshed
106
+ tokenSetFresh = await this.getTokenSet(true)
107
+ } catch (err) {
108
+ return initialResponse
109
+ }
110
+
111
+ // The stream was already consumed. We cannot retry the request. A solution
112
+ // would be to tee() the input stream but that would bufferize the entire
113
+ // stream in memory which can lead to memory starvation. Instead, we will
114
+ // return the original response and let the calling code handle retries.
115
+ if (ReadableStream && init?.body instanceof ReadableStream) {
116
+ return initialResponse
117
+ }
118
+
119
+ const finalAuth = `${tokenSetFresh.token_type} ${tokenSetFresh.access_token}`
120
+ const finalUrl = new URL(pathname, tokenSetFresh.aud)
121
+
122
+ headers.set('Authorization', finalAuth)
123
+
124
+ const finalResponse = await this.dpopFetch(finalUrl, { ...init, headers })
125
+
126
+ // There is no need to keep the session in the store if the token is expired
127
+ // and there is no way to refresh it.
128
+ if (isTokenExpiredResponse(finalResponse)) {
129
+ // TODO: Is there a "softer" way to handle this, e.g. by marking the
130
+ // session as "expired" and allow the user to trigger a new login?
131
+ await this.sessionGetter.delStored(this.sub)
132
+ }
133
+
134
+ return finalResponse
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3}
140
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no}
141
+ */
142
+ function isTokenExpiredResponse(response: Response) {
143
+ if (response.status !== 401) return false
144
+ const wwwAuth = response.headers.get('WWW-Authenticate')
145
+ return (
146
+ wwwAuth != null &&
147
+ (wwwAuth.startsWith('Bearer ') || wwwAuth.startsWith('DPoP ')) &&
148
+ wwwAuth.includes('error="invalid_token"')
149
+ )
150
+ }