@atproto/oauth-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +20 -0
- package/LICENSE.txt +7 -0
- package/README.md +124 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/fetch-dpop.d.ts +21 -0
- package/dist/fetch-dpop.d.ts.map +1 -0
- package/dist/fetch-dpop.js +149 -0
- package/dist/fetch-dpop.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +2 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +33 -0
- package/dist/lock.js.map +1 -0
- package/dist/oauth-agent.d.ts +29 -0
- package/dist/oauth-agent.d.ts.map +1 -0
- package/dist/oauth-agent.js +138 -0
- package/dist/oauth-agent.js.map +1 -0
- package/dist/oauth-authorization-server-metadata-resolver.d.ts +15 -0
- package/dist/oauth-authorization-server-metadata-resolver.d.ts.map +1 -0
- package/dist/oauth-authorization-server-metadata-resolver.js +56 -0
- package/dist/oauth-authorization-server-metadata-resolver.js.map +1 -0
- package/dist/oauth-callback-error.d.ts +7 -0
- package/dist/oauth-callback-error.d.ts.map +1 -0
- package/dist/oauth-callback-error.js +28 -0
- package/dist/oauth-callback-error.js.map +1 -0
- package/dist/oauth-client.d.ts +78 -0
- package/dist/oauth-client.d.ts.map +1 -0
- package/dist/oauth-client.js +278 -0
- package/dist/oauth-client.js.map +1 -0
- package/dist/oauth-protected-resource-metadata-resolver.d.ts +15 -0
- package/dist/oauth-protected-resource-metadata-resolver.d.ts.map +1 -0
- package/dist/oauth-protected-resource-metadata-resolver.js +58 -0
- package/dist/oauth-protected-resource-metadata-resolver.js.map +1 -0
- package/dist/oauth-resolver-error.d.ts +7 -0
- package/dist/oauth-resolver-error.d.ts.map +1 -0
- package/dist/oauth-resolver-error.js +17 -0
- package/dist/oauth-resolver-error.js.map +1 -0
- package/dist/oauth-resolver.d.ts +62 -0
- package/dist/oauth-resolver.d.ts.map +1 -0
- package/dist/oauth-resolver.js +73 -0
- package/dist/oauth-resolver.js.map +1 -0
- package/dist/oauth-response-error.d.ts +11 -0
- package/dist/oauth-response-error.d.ts.map +1 -0
- package/dist/oauth-response-error.js +48 -0
- package/dist/oauth-response-error.js.map +1 -0
- package/dist/oauth-server-agent.d.ts +51 -0
- package/dist/oauth-server-agent.d.ts.map +1 -0
- package/dist/oauth-server-agent.js +228 -0
- package/dist/oauth-server-agent.js.map +1 -0
- package/dist/oauth-server-factory.d.ts +20 -0
- package/dist/oauth-server-factory.d.ts.map +1 -0
- package/dist/oauth-server-factory.js +53 -0
- package/dist/oauth-server-factory.js.map +1 -0
- package/dist/refresh-error.d.ts +7 -0
- package/dist/refresh-error.d.ts.map +1 -0
- package/dist/refresh-error.js +16 -0
- package/dist/refresh-error.js.map +1 -0
- package/dist/runtime-implementation.d.ts +12 -0
- package/dist/runtime-implementation.d.ts.map +1 -0
- package/dist/runtime-implementation.js +3 -0
- package/dist/runtime-implementation.js.map +1 -0
- package/dist/runtime.d.ts +35 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +185 -0
- package/dist/runtime.js.map +1 -0
- package/dist/session-getter.d.ts +30 -0
- package/dist/session-getter.d.ts.map +1 -0
- package/dist/session-getter.js +149 -0
- package/dist/session-getter.js.map +1 -0
- package/dist/types.d.ts +1580 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +9 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +35 -0
- package/dist/util.js.map +1 -0
- package/dist/validate-client-metadata.d.ts +5 -0
- package/dist/validate-client-metadata.d.ts.map +1 -0
- package/dist/validate-client-metadata.js +46 -0
- package/dist/validate-client-metadata.js.map +1 -0
- package/package.json +46 -0
- package/src/constants.ts +4 -0
- package/src/fetch-dpop.ts +235 -0
- package/src/index.ts +18 -0
- package/src/lock.ts +34 -0
- package/src/oauth-agent.ts +150 -0
- package/src/oauth-authorization-server-metadata-resolver.ts +98 -0
- package/src/oauth-callback-error.ts +16 -0
- package/src/oauth-client.ts +440 -0
- package/src/oauth-protected-resource-metadata-resolver.ts +102 -0
- package/src/oauth-resolver-error.ts +12 -0
- package/src/oauth-resolver.ts +111 -0
- package/src/oauth-response-error.ts +31 -0
- package/src/oauth-server-agent.ts +275 -0
- package/src/oauth-server-factory.ts +41 -0
- package/src/refresh-error.ts +9 -0
- package/src/runtime-implementation.ts +17 -0
- package/src/runtime.ts +211 -0
- package/src/session-getter.ts +182 -0
- package/src/types.ts +26 -0
- package/src/util.ts +51 -0
- package/src/validate-client-metadata.ts +61 -0
- package/tsconfig.build.json +8 -0
- 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
|
package/dist/util.js.map
ADDED
@@ -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
|
+
}
|
package/src/constants.ts
ADDED
@@ -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
|
+
}
|