@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.
- 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
|
+
}
|