@atproto/oauth-provider 0.16.3 → 0.17.0-next.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 +42 -0
- package/dist/access-token/access-token-mode.js +2 -5
- package/dist/access-token/access-token-mode.js.map +1 -1
- package/dist/account/account-manager.js +25 -33
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.js +11 -32
- package/dist/account/account-store.js.map +1 -1
- package/dist/account/sign-in-data.js +9 -12
- package/dist/account/sign-in-data.js.map +1 -1
- package/dist/account/sign-up-input.js +14 -17
- package/dist/account/sign-up-input.js.map +1 -1
- package/dist/client/client-auth.js +1 -2
- package/dist/client/client-data.js +1 -2
- package/dist/client/client-id.js +2 -5
- package/dist/client/client-id.js.map +1 -1
- package/dist/client/client-info.js +1 -2
- package/dist/client/client-manager.js +86 -97
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client-store.js +7 -26
- package/dist/client/client-store.js.map +1 -1
- package/dist/client/client-utils.js +10 -14
- package/dist/client/client-utils.js.map +1 -1
- package/dist/client/client.js +43 -53
- package/dist/client/client.js.map +1 -1
- package/dist/constants.js +28 -31
- package/dist/constants.js.map +1 -1
- package/dist/customization/branding.js +8 -11
- package/dist/customization/branding.js.map +1 -1
- package/dist/customization/build-customization-css.js +8 -11
- package/dist/customization/build-customization-css.js.map +1 -1
- package/dist/customization/build-customization-data.js +1 -4
- package/dist/customization/build-customization-data.js.map +1 -1
- package/dist/customization/colors.js +11 -14
- package/dist/customization/colors.js.map +1 -1
- package/dist/customization/customization.js +8 -11
- package/dist/customization/customization.js.map +1 -1
- package/dist/customization/links.js +7 -10
- package/dist/customization/links.js.map +1 -1
- package/dist/device/device-data.js +7 -10
- package/dist/device/device-data.js.map +1 -1
- package/dist/device/device-id.js +11 -16
- package/dist/device/device-id.js.map +1 -1
- package/dist/device/device-manager.js +32 -38
- package/dist/device/device-manager.js.map +1 -1
- package/dist/device/device-store.js +7 -25
- package/dist/device/device-store.js.map +1 -1
- package/dist/device/session-id.js +9 -13
- package/dist/device/session-id.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts +3 -3
- package/dist/dpop/dpop-manager.js +38 -43
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/dpop/dpop-nonce.d.ts +2 -2
- package/dist/dpop/dpop-nonce.d.ts.map +1 -1
- package/dist/dpop/dpop-nonce.js +14 -18
- package/dist/dpop/dpop-nonce.js.map +1 -1
- package/dist/dpop/dpop-proof.js +1 -2
- package/dist/errors/access-denied-error.js +2 -6
- package/dist/errors/access-denied-error.js.map +1 -1
- package/dist/errors/account-selection-required-error.js +2 -6
- package/dist/errors/account-selection-required-error.js.map +1 -1
- package/dist/errors/authorization-error.js +7 -12
- package/dist/errors/authorization-error.js.map +1 -1
- package/dist/errors/consent-required-error.js +2 -6
- package/dist/errors/consent-required-error.js.map +1 -1
- package/dist/errors/error-parser.js +14 -18
- package/dist/errors/error-parser.js.map +1 -1
- package/dist/errors/handle-unavailable-error.js +2 -7
- package/dist/errors/handle-unavailable-error.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js +2 -6
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/errors/invalid-client-error.js +2 -6
- package/dist/errors/invalid-client-error.js.map +1 -1
- package/dist/errors/invalid-client-id-error.js +2 -6
- package/dist/errors/invalid-client-id-error.js.map +1 -1
- package/dist/errors/invalid-client-metadata-error.js +7 -11
- package/dist/errors/invalid-client-metadata-error.js.map +1 -1
- package/dist/errors/invalid-credentials-error.js +2 -7
- package/dist/errors/invalid-credentials-error.js.map +1 -1
- package/dist/errors/invalid-dpop-key-binding-error.js +2 -6
- package/dist/errors/invalid-dpop-key-binding-error.js.map +1 -1
- package/dist/errors/invalid-dpop-proof-error.js +2 -6
- package/dist/errors/invalid-dpop-proof-error.js.map +1 -1
- package/dist/errors/invalid-grant-error.js +2 -6
- package/dist/errors/invalid-grant-error.js.map +1 -1
- package/dist/errors/invalid-invite-code-error.d.ts +1 -1
- package/dist/errors/invalid-invite-code-error.d.ts.map +1 -1
- package/dist/errors/invalid-invite-code-error.js +2 -6
- package/dist/errors/invalid-invite-code-error.js.map +1 -1
- package/dist/errors/invalid-redirect-uri-error.js +2 -6
- package/dist/errors/invalid-redirect-uri-error.js.map +1 -1
- package/dist/errors/invalid-request-error.js +3 -7
- package/dist/errors/invalid-request-error.js.map +1 -1
- package/dist/errors/invalid-scope-error.js +2 -6
- package/dist/errors/invalid-scope-error.js.map +1 -1
- package/dist/errors/invalid-token-error.js +10 -15
- package/dist/errors/invalid-token-error.js.map +1 -1
- package/dist/errors/login-required-error.js +2 -6
- package/dist/errors/login-required-error.js.map +1 -1
- package/dist/errors/oauth-error.js +1 -9
- package/dist/errors/oauth-error.js.map +1 -1
- package/dist/errors/second-authentication-factor-required-error.js +2 -8
- package/dist/errors/second-authentication-factor-required-error.js.map +1 -1
- package/dist/errors/unauthorized-client-error.js +2 -6
- package/dist/errors/unauthorized-client-error.js.map +1 -1
- package/dist/errors/use-dpop-nonce-error.js +4 -8
- package/dist/errors/use-dpop-nonce-error.js.map +1 -1
- package/dist/errors/www-authenticate-error.js +4 -9
- package/dist/errors/www-authenticate-error.js.map +1 -1
- package/dist/index.js +14 -30
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicon-data.js +1 -2
- package/dist/lexicon/lexicon-getter.js +6 -10
- package/dist/lexicon/lexicon-getter.js.map +1 -1
- package/dist/lexicon/lexicon-manager.js +10 -30
- package/dist/lexicon/lexicon-manager.js.map +1 -1
- package/dist/lexicon/lexicon-store.js +5 -10
- package/dist/lexicon/lexicon-store.js.map +1 -1
- package/dist/lib/csp/index.js +3 -8
- package/dist/lib/csp/index.js.map +1 -1
- package/dist/lib/hcaptcha.js +33 -43
- package/dist/lib/hcaptcha.js.map +1 -1
- package/dist/lib/html/build-document.js +19 -24
- package/dist/lib/html/build-document.js.map +1 -1
- package/dist/lib/html/escapers.js +10 -16
- package/dist/lib/html/escapers.js.map +1 -1
- package/dist/lib/html/html.js +1 -5
- package/dist/lib/html/html.js.map +1 -1
- package/dist/lib/html/hydration-data.js +6 -10
- package/dist/lib/html/hydration-data.js.map +1 -1
- package/dist/lib/html/index.js +3 -19
- package/dist/lib/html/index.js.map +1 -1
- package/dist/lib/html/tags.js +14 -23
- package/dist/lib/html/tags.js.map +1 -1
- package/dist/lib/html/util.js +1 -4
- package/dist/lib/html/util.js.map +1 -1
- package/dist/lib/http/accept.d.ts.map +1 -1
- package/dist/lib/http/accept.js +8 -8
- package/dist/lib/http/accept.js.map +1 -1
- package/dist/lib/http/context.js +1 -4
- package/dist/lib/http/context.js.map +1 -1
- package/dist/lib/http/headers.js +1 -4
- package/dist/lib/http/headers.js.map +1 -1
- package/dist/lib/http/index.js +10 -26
- package/dist/lib/http/index.js.map +1 -1
- package/dist/lib/http/method.js +1 -4
- package/dist/lib/http/method.js.map +1 -1
- package/dist/lib/http/middleware.js +11 -17
- package/dist/lib/http/middleware.js.map +1 -1
- package/dist/lib/http/parser.js +13 -20
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/path.js +1 -4
- package/dist/lib/http/path.js.map +1 -1
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +32 -47
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/response.js +14 -27
- package/dist/lib/http/response.js.map +1 -1
- package/dist/lib/http/route.js +9 -12
- package/dist/lib/http/route.js.map +1 -1
- package/dist/lib/http/router.js +8 -13
- package/dist/lib/http/router.js.map +1 -1
- package/dist/lib/http/security-headers.js +10 -15
- package/dist/lib/http/security-headers.js.map +1 -1
- package/dist/lib/http/stream.js +12 -20
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/http/types.js +1 -2
- package/dist/lib/http/url.js +1 -4
- package/dist/lib/http/url.js.map +1 -1
- package/dist/lib/nsid.js +4 -8
- package/dist/lib/nsid.js.map +1 -1
- package/dist/lib/redis.js +4 -7
- package/dist/lib/redis.js.map +1 -1
- package/dist/lib/util/authorization-header.js +11 -15
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/lib/util/cast.js +3 -8
- package/dist/lib/util/cast.js.map +1 -1
- package/dist/lib/util/color.js +23 -32
- package/dist/lib/util/color.js.map +1 -1
- package/dist/lib/util/crypto.js +5 -10
- package/dist/lib/util/crypto.js.map +1 -1
- package/dist/lib/util/date.js +2 -6
- package/dist/lib/util/date.js.map +1 -1
- package/dist/lib/util/error.js +5 -8
- package/dist/lib/util/error.js.map +1 -1
- package/dist/lib/util/function.js +3 -8
- package/dist/lib/util/function.js.map +1 -1
- package/dist/lib/util/locale.js +3 -6
- package/dist/lib/util/locale.js.map +1 -1
- package/dist/lib/util/object.js +1 -4
- package/dist/lib/util/object.js.map +1 -1
- package/dist/lib/util/redirect-uri.js +3 -6
- package/dist/lib/util/redirect-uri.js.map +1 -1
- package/dist/lib/util/time.js +5 -9
- package/dist/lib/util/time.js.map +1 -1
- package/dist/lib/util/type.d.ts.map +1 -1
- package/dist/lib/util/type.js +1 -5
- package/dist/lib/util/type.js.map +1 -1
- package/dist/lib/util/ui8.js +3 -8
- package/dist/lib/util/ui8.js.map +1 -1
- package/dist/lib/util/well-known.js +1 -4
- package/dist/lib/util/well-known.js.map +1 -1
- package/dist/lib/util/zod-error.js +4 -8
- package/dist/lib/util/zod-error.js.map +1 -1
- package/dist/lib/write-form-redirect.js +9 -12
- package/dist/lib/write-form-redirect.js.map +1 -1
- package/dist/lib/write-html.js +12 -15
- package/dist/lib/write-html.js.map +1 -1
- package/dist/metadata/build-metadata.js +9 -12
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-client.js +2 -18
- package/dist/oauth-client.js.map +1 -1
- package/dist/oauth-dpop.js +2 -18
- package/dist/oauth-dpop.js.map +1 -1
- package/dist/oauth-errors.js +24 -42
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.js +8 -15
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-middleware.js +13 -16
- package/dist/oauth-middleware.js.map +1 -1
- package/dist/oauth-provider.js +108 -125
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-store.js +7 -23
- package/dist/oauth-store.js.map +1 -1
- package/dist/oauth-verifier.js +41 -53
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/oidc/sub.js +2 -5
- package/dist/oidc/sub.js.map +1 -1
- package/dist/replay/replay-manager.js +6 -11
- package/dist/replay/replay-manager.js.map +1 -1
- package/dist/replay/replay-store-memory.js +5 -7
- package/dist/replay/replay-store-memory.js.map +1 -1
- package/dist/replay/replay-store-redis.js +3 -8
- package/dist/replay/replay-store-redis.js.map +1 -1
- package/dist/replay/replay-store.js +3 -8
- package/dist/replay/replay-store.js.map +1 -1
- package/dist/request/code.js +10 -15
- package/dist/request/code.js.map +1 -1
- package/dist/request/request-data.js +1 -5
- package/dist/request/request-data.js.map +1 -1
- package/dist/request/request-id.js +9 -13
- package/dist/request/request-id.js.map +1 -1
- package/dist/request/request-manager.js +61 -71
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store.js +9 -27
- package/dist/request/request-store.js.map +1 -1
- package/dist/request/request-uri.js +17 -23
- package/dist/request/request-uri.js.map +1 -1
- package/dist/result/authorization-redirect-parameters.js +1 -2
- package/dist/result/authorization-result-authorize-page.js +1 -2
- package/dist/result/authorization-result-redirect.js +1 -2
- package/dist/router/assets/assets-manifest.d.ts.map +1 -1
- package/dist/router/assets/assets-manifest.js +14 -15
- package/dist/router/assets/assets-manifest.js.map +1 -1
- package/dist/router/assets/assets.d.ts.map +1 -1
- package/dist/router/assets/assets.js +25 -27
- package/dist/router/assets/assets.js.map +1 -1
- package/dist/router/assets/csrf.js +16 -25
- package/dist/router/assets/csrf.js.map +1 -1
- package/dist/router/assets/send-account-page.js +3 -6
- package/dist/router/assets/send-account-page.js.map +1 -1
- package/dist/router/assets/send-authorization-page.js +3 -6
- package/dist/router/assets/send-authorization-page.js.map +1 -1
- package/dist/router/assets/send-cookie-error-page.js +3 -6
- package/dist/router/assets/send-cookie-error-page.js.map +1 -1
- package/dist/router/assets/send-error-page.js +6 -9
- package/dist/router/assets/send-error-page.js.map +1 -1
- package/dist/router/assets/send-redirect.js +12 -20
- package/dist/router/assets/send-redirect.js.map +1 -1
- package/dist/router/create-account-page-middleware.js +11 -14
- package/dist/router/create-account-page-middleware.js.map +1 -1
- package/dist/router/create-api-middleware.js +83 -90
- package/dist/router/create-api-middleware.js.map +1 -1
- package/dist/router/create-authorization-page-middleware.js +43 -46
- package/dist/router/create-authorization-page-middleware.js.map +1 -1
- package/dist/router/create-oauth-middleware.js +31 -34
- package/dist/router/create-oauth-middleware.js.map +1 -1
- package/dist/router/error-handler.js +1 -2
- package/dist/router/middleware-options.js +1 -2
- package/dist/signer/access-token-payload.js +12 -15
- package/dist/signer/access-token-payload.js.map +1 -1
- package/dist/signer/api-token-payload.js +8 -11
- package/dist/signer/api-token-payload.js.map +1 -1
- package/dist/signer/signer.js +11 -17
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/refresh-token.js +10 -15
- package/dist/token/refresh-token.js.map +1 -1
- package/dist/token/token-claims.js +1 -2
- package/dist/token/token-data.js +1 -2
- package/dist/token/token-id.js +10 -15
- package/dist/token/token-id.js.map +1 -1
- package/dist/token/token-manager.js +40 -51
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/token-store.js +7 -25
- package/dist/token/token-store.js.map +1 -1
- package/dist/types/authorization-response-error.js +8 -12
- package/dist/types/authorization-response-error.js.map +1 -1
- package/dist/types/color-hue.js +2 -5
- package/dist/types/color-hue.js.map +1 -1
- package/dist/types/email-otp.js +2 -5
- package/dist/types/email-otp.js.map +1 -1
- package/dist/types/email.js +6 -9
- package/dist/types/email.js.map +1 -1
- package/dist/types/handle.js +6 -9
- package/dist/types/handle.js.map +1 -1
- package/dist/types/invite-code.js +2 -5
- package/dist/types/invite-code.js.map +1 -1
- package/dist/types/par-response-error.js +5 -9
- package/dist/types/par-response-error.js.map +1 -1
- package/dist/types/password.js +3 -6
- package/dist/types/password.js.map +1 -1
- package/dist/types/rgb-color.js +7 -10
- package/dist/types/rgb-color.js.map +1 -1
- package/package.json +20 -22
- package/src/dpop/dpop-nonce.ts +1 -1
- package/src/errors/invalid-invite-code-error.ts +1 -1
- package/src/lib/http/accept.ts +4 -1
- package/src/lib/http/request.ts +4 -1
- package/src/lib/util/type.ts +0 -1
- package/src/router/assets/assets-manifest.ts +3 -1
- package/src/router/assets/assets.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,40 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const client_utils_js_1 = require("./client-utils.js");
|
|
13
|
-
const client_js_1 = require("./client.js");
|
|
14
|
-
const fetchMetadataHandler = (0, pipe_1.pipe)((0, fetch_1.fetchOkProcessor)(),
|
|
1
|
+
import { jwksPubSchema } from '@atproto/jwk';
|
|
2
|
+
import { isLocalHostname, isOAuthClientIdDiscoverable, isOAuthClientIdLoopback, oauthClientMetadataSchema, } from '@atproto/oauth-types';
|
|
3
|
+
import { bindFetch, fetchJsonProcessor, fetchJsonZodProcessor, fetchOkProcessor, } from '@atproto-labs/fetch';
|
|
4
|
+
import { pipe } from '@atproto-labs/pipe';
|
|
5
|
+
import { CachedGetter, } from '@atproto-labs/simple-store';
|
|
6
|
+
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js';
|
|
7
|
+
import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js';
|
|
8
|
+
import { callAsync } from '../lib/util/function.js';
|
|
9
|
+
import { parseDiscoverableClientId, parseRedirectUri } from './client-utils.js';
|
|
10
|
+
import { Client } from './client.js';
|
|
11
|
+
const fetchMetadataHandler = pipe(fetchOkProcessor(),
|
|
15
12
|
// https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html#section-4.1
|
|
16
|
-
|
|
17
|
-
const fetchJwksHandler =
|
|
18
|
-
class ClientManager {
|
|
19
|
-
serverMetadata;
|
|
20
|
-
keyset;
|
|
21
|
-
hooks;
|
|
22
|
-
store;
|
|
23
|
-
loopbackMetadata;
|
|
24
|
-
jwks;
|
|
25
|
-
metadataGetter;
|
|
13
|
+
fetchJsonProcessor('application/json', true), fetchJsonZodProcessor(oauthClientMetadataSchema));
|
|
14
|
+
const fetchJwksHandler = pipe(fetchOkProcessor(), fetchJsonProcessor('application/json', false), fetchJsonZodProcessor(jwksPubSchema));
|
|
15
|
+
export class ClientManager {
|
|
26
16
|
constructor(serverMetadata, keyset, hooks, store, loopbackMetadata = null, safeFetch, clientJwksCache, clientMetadataCache) {
|
|
27
17
|
this.serverMetadata = serverMetadata;
|
|
28
18
|
this.keyset = keyset;
|
|
29
19
|
this.hooks = hooks;
|
|
30
20
|
this.store = store;
|
|
31
21
|
this.loopbackMetadata = loopbackMetadata;
|
|
32
|
-
const fetch =
|
|
33
|
-
this.jwks = new
|
|
22
|
+
const fetch = bindFetch(safeFetch);
|
|
23
|
+
this.jwks = new CachedGetter(async (uri, options) => {
|
|
34
24
|
const jwks = await fetch(buildJsonGetRequest(uri, options)).then(fetchJwksHandler);
|
|
35
25
|
return jwks;
|
|
36
26
|
}, clientJwksCache);
|
|
37
|
-
this.metadataGetter = new
|
|
27
|
+
this.metadataGetter = new CachedGetter(async (uri, options) => {
|
|
38
28
|
const metadata = await fetch(buildJsonGetRequest(uri, options)).then(fetchMetadataHandler);
|
|
39
29
|
// Validate within the getter to avoid caching invalid metadata
|
|
40
30
|
return this.validateClientMetadata(uri, metadata);
|
|
@@ -46,22 +36,22 @@ class ClientManager {
|
|
|
46
36
|
*/
|
|
47
37
|
async getClient(clientId) {
|
|
48
38
|
const metadata = await this.getClientMetadata(clientId).catch((err) => {
|
|
49
|
-
throw
|
|
39
|
+
throw InvalidClientMetadataError.from(err, `Unable to obtain client metadata for "${clientId}"`);
|
|
50
40
|
});
|
|
51
41
|
const jwks = metadata.jwks_uri
|
|
52
42
|
? await this.jwks.get(metadata.jwks_uri).catch((err) => {
|
|
53
|
-
throw
|
|
43
|
+
throw InvalidClientMetadataError.from(err, `Unable to obtain jwks from "${metadata.jwks_uri}" for "${clientId}"`);
|
|
54
44
|
})
|
|
55
45
|
: undefined;
|
|
56
|
-
const partialInfo = await
|
|
46
|
+
const partialInfo = await callAsync(this.hooks.getClientInfo, clientId, {
|
|
57
47
|
metadata,
|
|
58
48
|
jwks,
|
|
59
49
|
}).catch((err) => {
|
|
60
|
-
throw
|
|
50
|
+
throw InvalidClientMetadataError.from(err, `Rejected client information for "${clientId}"`);
|
|
61
51
|
});
|
|
62
52
|
const isFirstParty = partialInfo?.isFirstParty ?? false;
|
|
63
53
|
const isTrusted = partialInfo?.isTrusted ?? isFirstParty;
|
|
64
|
-
return new
|
|
54
|
+
return new Client(clientId, metadata, jwks, { isFirstParty, isTrusted });
|
|
65
55
|
}
|
|
66
56
|
async loadClients(clientIds, { onError = (err) => {
|
|
67
57
|
throw err;
|
|
@@ -72,38 +62,38 @@ class ClientManager {
|
|
|
72
62
|
const clients = await Promise.all(Array.from(uniqueClientIds, async (clientId) => this.getClient(clientId).catch((err) => onError(err, clientId))));
|
|
73
63
|
// Return a map for easy lookups
|
|
74
64
|
return new Map(clients
|
|
75
|
-
.filter((c) => c != null && c instanceof
|
|
65
|
+
.filter((c) => c != null && c instanceof Client)
|
|
76
66
|
.map((c) => [c.id, c]));
|
|
77
67
|
}
|
|
78
68
|
async getClientMetadata(clientId) {
|
|
79
|
-
if (
|
|
69
|
+
if (isOAuthClientIdLoopback(clientId)) {
|
|
80
70
|
return this.getLoopbackClientMetadata(clientId);
|
|
81
71
|
}
|
|
82
|
-
else if (
|
|
72
|
+
else if (isOAuthClientIdDiscoverable(clientId)) {
|
|
83
73
|
return this.getDiscoverableClientMetadata(clientId);
|
|
84
74
|
}
|
|
85
75
|
else if (this.store) {
|
|
86
76
|
return this.getStoredClientMetadata(clientId);
|
|
87
77
|
}
|
|
88
|
-
throw new
|
|
78
|
+
throw new InvalidClientMetadataError(`Invalid client ID "${clientId}"`);
|
|
89
79
|
}
|
|
90
80
|
async getLoopbackClientMetadata(clientId) {
|
|
91
81
|
const { loopbackMetadata } = this;
|
|
92
82
|
if (!loopbackMetadata) {
|
|
93
|
-
throw new
|
|
83
|
+
throw new InvalidClientMetadataError('Loopback clients are not allowed');
|
|
94
84
|
}
|
|
95
|
-
const metadataRaw = await
|
|
96
|
-
throw
|
|
85
|
+
const metadataRaw = await callAsync(loopbackMetadata, clientId).catch((err) => {
|
|
86
|
+
throw InvalidClientMetadataError.from(err, `Invalid loopback client id "${clientId}"`);
|
|
97
87
|
});
|
|
98
|
-
const metadata = await
|
|
88
|
+
const metadata = await oauthClientMetadataSchema
|
|
99
89
|
.parseAsync(metadataRaw)
|
|
100
90
|
.catch((err) => {
|
|
101
|
-
throw
|
|
91
|
+
throw InvalidClientMetadataError.from(err, `Invalid loopback client metadata for "${clientId}"`);
|
|
102
92
|
});
|
|
103
93
|
return this.validateClientMetadata(clientId, metadata);
|
|
104
94
|
}
|
|
105
95
|
async getDiscoverableClientMetadata(clientId) {
|
|
106
|
-
const metadataUrl =
|
|
96
|
+
const metadataUrl = parseDiscoverableClientId(clientId);
|
|
107
97
|
const metadata = await this.metadataGetter.get(metadataUrl.href);
|
|
108
98
|
// Note: we do *not* re-validate the metadata here, as the metadata is
|
|
109
99
|
// validated within the getter. This is to avoid double validation.
|
|
@@ -116,7 +106,7 @@ class ClientManager {
|
|
|
116
106
|
const metadata = await this.store.findClient(clientId);
|
|
117
107
|
return this.validateClientMetadata(clientId, metadata);
|
|
118
108
|
}
|
|
119
|
-
throw new
|
|
109
|
+
throw new InvalidClientMetadataError(`Invalid client ID "${clientId}"`);
|
|
120
110
|
}
|
|
121
111
|
/**
|
|
122
112
|
* This method will ensure that the client metadata is valid w.r.t. the OAuth
|
|
@@ -129,7 +119,7 @@ class ClientManager {
|
|
|
129
119
|
// implementation or the ATPROTO specification. All generic validation rules
|
|
130
120
|
// should be moved to the @atproto/oauth-types package.
|
|
131
121
|
if (metadata.jwks && metadata.jwks_uri) {
|
|
132
|
-
throw new
|
|
122
|
+
throw new InvalidClientMetadataError('jwks_uri and jwks are mutually exclusive');
|
|
133
123
|
}
|
|
134
124
|
// Known OIDC specific parameters
|
|
135
125
|
for (const k of [
|
|
@@ -139,114 +129,114 @@ class ClientManager {
|
|
|
139
129
|
'userinfo_encrypted_response_alg',
|
|
140
130
|
]) {
|
|
141
131
|
if (metadata[k] != null) {
|
|
142
|
-
throw new
|
|
132
|
+
throw new InvalidClientMetadataError(`Unsupported "${k}" parameter`);
|
|
143
133
|
}
|
|
144
134
|
}
|
|
145
135
|
const clientUriUrl = metadata.client_uri
|
|
146
136
|
? new URL(metadata.client_uri)
|
|
147
137
|
: null;
|
|
148
|
-
if (clientUriUrl &&
|
|
149
|
-
throw new
|
|
138
|
+
if (clientUriUrl && isLocalHostname(clientUriUrl.hostname)) {
|
|
139
|
+
throw new InvalidClientMetadataError('client_uri hostname is invalid');
|
|
150
140
|
}
|
|
151
141
|
const scopes = metadata.scope?.split(' ');
|
|
152
142
|
if (!scopes) {
|
|
153
|
-
throw new
|
|
143
|
+
throw new InvalidClientMetadataError('Missing scope property');
|
|
154
144
|
}
|
|
155
145
|
if (!scopes.includes('atproto')) {
|
|
156
|
-
throw new
|
|
146
|
+
throw new InvalidClientMetadataError('Missing "atproto" scope');
|
|
157
147
|
}
|
|
158
148
|
const dupScope = scopes?.find(isDuplicate);
|
|
159
149
|
if (dupScope) {
|
|
160
|
-
throw new
|
|
150
|
+
throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`);
|
|
161
151
|
}
|
|
162
152
|
const dupGrantType = metadata.grant_types.find(isDuplicate);
|
|
163
153
|
if (dupGrantType) {
|
|
164
|
-
throw new
|
|
154
|
+
throw new InvalidClientMetadataError(`Duplicate grant type "${dupGrantType}"`);
|
|
165
155
|
}
|
|
166
156
|
for (const grantType of metadata.grant_types) {
|
|
167
157
|
switch (grantType) {
|
|
168
158
|
case 'implicit':
|
|
169
159
|
// Never allowed (unsafe)
|
|
170
|
-
throw new
|
|
160
|
+
throw new InvalidClientMetadataError(`Grant type "${grantType}" is not allowed`);
|
|
171
161
|
// @TODO Add support (e.g. for first party client)
|
|
172
162
|
// case 'client_credentials':
|
|
173
163
|
// case 'password':
|
|
174
164
|
case 'authorization_code':
|
|
175
165
|
case 'refresh_token':
|
|
176
166
|
if (!this.serverMetadata.grant_types_supported?.includes(grantType)) {
|
|
177
|
-
throw new
|
|
167
|
+
throw new InvalidClientMetadataError(`Unsupported grant type "${grantType}"`);
|
|
178
168
|
}
|
|
179
169
|
break;
|
|
180
170
|
default:
|
|
181
|
-
throw new
|
|
171
|
+
throw new InvalidClientMetadataError(`Grant type "${grantType}" is not supported`);
|
|
182
172
|
}
|
|
183
173
|
}
|
|
184
174
|
if (metadata.client_id && metadata.client_id !== clientId) {
|
|
185
|
-
throw new
|
|
175
|
+
throw new InvalidClientMetadataError('client_id does not match');
|
|
186
176
|
}
|
|
187
177
|
if (metadata.subject_type && metadata.subject_type !== 'public') {
|
|
188
|
-
throw new
|
|
178
|
+
throw new InvalidClientMetadataError('Only "public" subject_type is supported');
|
|
189
179
|
}
|
|
190
180
|
switch (metadata.token_endpoint_auth_method) {
|
|
191
181
|
case 'none':
|
|
192
182
|
if (metadata.token_endpoint_auth_signing_alg) {
|
|
193
|
-
throw new
|
|
183
|
+
throw new InvalidClientMetadataError(`token_endpoint_auth_method "none" must not have token_endpoint_auth_signing_alg`);
|
|
194
184
|
}
|
|
195
185
|
break;
|
|
196
186
|
case 'private_key_jwt':
|
|
197
187
|
if (!metadata.jwks && !metadata.jwks_uri) {
|
|
198
|
-
throw new
|
|
188
|
+
throw new InvalidClientMetadataError(`private_key_jwt auth method requires jwks or jwks_uri`);
|
|
199
189
|
}
|
|
200
190
|
if (metadata.jwks?.keys.length === 0) {
|
|
201
|
-
throw new
|
|
191
|
+
throw new InvalidClientMetadataError(`private_key_jwt auth method requires at least one key in jwks`);
|
|
202
192
|
}
|
|
203
193
|
if (!metadata.token_endpoint_auth_signing_alg) {
|
|
204
|
-
throw new
|
|
194
|
+
throw new InvalidClientMetadataError(`Missing token_endpoint_auth_signing_alg client metadata`);
|
|
205
195
|
}
|
|
206
196
|
break;
|
|
207
197
|
default:
|
|
208
|
-
throw new
|
|
198
|
+
throw new InvalidClientMetadataError(`Unsupported client authentication method "${metadata.token_endpoint_auth_method}". Make sure "token_endpoint_auth_method" is set to one of: "${Client.AUTH_METHODS_SUPPORTED.join('", "')}"`);
|
|
209
199
|
}
|
|
210
200
|
if (metadata.authorization_encrypted_response_enc) {
|
|
211
|
-
throw new
|
|
201
|
+
throw new InvalidClientMetadataError('Encrypted authorization response is not supported');
|
|
212
202
|
}
|
|
213
203
|
if (metadata.tls_client_certificate_bound_access_tokens) {
|
|
214
|
-
throw new
|
|
204
|
+
throw new InvalidClientMetadataError('Mutual-TLS bound access tokens are not supported');
|
|
215
205
|
}
|
|
216
206
|
if (metadata.authorization_encrypted_response_enc &&
|
|
217
207
|
!metadata.authorization_encrypted_response_alg) {
|
|
218
|
-
throw new
|
|
208
|
+
throw new InvalidClientMetadataError('authorization_encrypted_response_enc requires authorization_encrypted_response_alg');
|
|
219
209
|
}
|
|
220
210
|
// ATPROTO spec requires the use of DPoP (OAuth spec defaults to false)
|
|
221
211
|
if (metadata.dpop_bound_access_tokens !== true) {
|
|
222
|
-
throw new
|
|
212
|
+
throw new InvalidClientMetadataError('"dpop_bound_access_tokens" must be true');
|
|
223
213
|
}
|
|
224
214
|
// ATPROTO spec requires the use of PKCE, does not support OIDC
|
|
225
215
|
if (!metadata.response_types.includes('code')) {
|
|
226
|
-
throw new
|
|
216
|
+
throw new InvalidClientMetadataError('response_types must include "code"');
|
|
227
217
|
}
|
|
228
218
|
else if (!metadata.grant_types.includes('authorization_code')) {
|
|
229
219
|
// Consistency check
|
|
230
|
-
throw new
|
|
220
|
+
throw new InvalidClientMetadataError(`The "code" response type requires that "grant_types" contains "authorization_code"`);
|
|
231
221
|
}
|
|
232
222
|
if (metadata.authorization_details_types?.length) {
|
|
233
223
|
const dupAuthDetailsType = metadata.authorization_details_types.find(isDuplicate);
|
|
234
224
|
if (dupAuthDetailsType) {
|
|
235
|
-
throw new
|
|
225
|
+
throw new InvalidClientMetadataError(`Duplicate authorization_details_type "${dupAuthDetailsType}"`);
|
|
236
226
|
}
|
|
237
227
|
const authorizationDetailsTypesSupported = this.serverMetadata.authorization_details_types_supported;
|
|
238
228
|
if (!authorizationDetailsTypesSupported) {
|
|
239
|
-
throw new
|
|
229
|
+
throw new InvalidClientMetadataError('authorization_details_types are not supported');
|
|
240
230
|
}
|
|
241
231
|
for (const type of metadata.authorization_details_types) {
|
|
242
232
|
if (!authorizationDetailsTypesSupported.includes(type)) {
|
|
243
|
-
throw new
|
|
233
|
+
throw new InvalidClientMetadataError(`Unsupported authorization_details_type "${type}"`);
|
|
244
234
|
}
|
|
245
235
|
}
|
|
246
236
|
}
|
|
247
237
|
if (!metadata.redirect_uris?.length) {
|
|
248
238
|
// ATPROTO spec requires that at least one redirect URI is provided
|
|
249
|
-
throw new
|
|
239
|
+
throw new InvalidClientMetadataError('At least one redirect_uri is required');
|
|
250
240
|
}
|
|
251
241
|
if (metadata.application_type === 'native' &&
|
|
252
242
|
metadata.token_endpoint_auth_method !== 'none') {
|
|
@@ -262,7 +252,7 @@ class ClientManager {
|
|
|
262
252
|
// @NOTE We may want to remove this restriction in the future, for example
|
|
263
253
|
// if https://github.com/bluesky-social/proposals/tree/main/0010-client-assertion-backend
|
|
264
254
|
// gets adopted
|
|
265
|
-
throw new
|
|
255
|
+
throw new InvalidClientMetadataError('Native clients must authenticate using "none" method');
|
|
266
256
|
}
|
|
267
257
|
if (metadata.application_type === 'web' &&
|
|
268
258
|
metadata.grant_types.includes('implicit')) {
|
|
@@ -273,20 +263,20 @@ class ClientManager {
|
|
|
273
263
|
// > scheme as redirect_uris; they MUST NOT use localhost as the
|
|
274
264
|
// > hostname.
|
|
275
265
|
for (const redirectUri of metadata.redirect_uris) {
|
|
276
|
-
const url =
|
|
266
|
+
const url = parseRedirectUri(redirectUri);
|
|
277
267
|
if (url.protocol !== 'https:') {
|
|
278
|
-
throw new
|
|
268
|
+
throw new InvalidRedirectUriError(`Web clients must use HTTPS redirect URIs`);
|
|
279
269
|
}
|
|
280
270
|
if (url.hostname === 'localhost') {
|
|
281
|
-
throw new
|
|
271
|
+
throw new InvalidRedirectUriError(`Web clients must not use localhost as the hostname`);
|
|
282
272
|
}
|
|
283
273
|
}
|
|
284
274
|
}
|
|
285
275
|
for (const redirectUri of metadata.redirect_uris) {
|
|
286
|
-
const url =
|
|
276
|
+
const url = parseRedirectUri(redirectUri);
|
|
287
277
|
if (url.username || url.password) {
|
|
288
278
|
// Is this a valid concern? Should we allow credentials in the URI?
|
|
289
|
-
throw new
|
|
279
|
+
throw new InvalidRedirectUriError(`Redirect URI ${url} must not contain credentials`);
|
|
290
280
|
}
|
|
291
281
|
switch (true) {
|
|
292
282
|
// FIRST: Loopback redirect URI exception (only for native apps)
|
|
@@ -301,13 +291,13 @@ class ClientManager {
|
|
|
301
291
|
// > interfaces other than the loopback interface. It is also less
|
|
302
292
|
// > susceptible to client-side firewalls and misconfigured host name
|
|
303
293
|
// > resolution on the user's device.
|
|
304
|
-
throw new
|
|
294
|
+
throw new InvalidRedirectUriError(`Loopback redirect URI ${url} is not allowed (use explicit IPs instead)`);
|
|
305
295
|
}
|
|
306
296
|
case url.hostname === '127.0.0.1':
|
|
307
297
|
case url.hostname === '[::1]': {
|
|
308
298
|
// Only allowed for native apps
|
|
309
299
|
if (metadata.application_type !== 'native') {
|
|
310
|
-
throw new
|
|
300
|
+
throw new InvalidRedirectUriError(`Loopback redirect URIs are only allowed for native apps`);
|
|
311
301
|
}
|
|
312
302
|
if (url.port) {
|
|
313
303
|
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
|
|
@@ -330,7 +320,7 @@ class ClientManager {
|
|
|
330
320
|
// > with the loopback IP literal and whatever port the client is
|
|
331
321
|
// > listening on. That is, "http://127.0.0.1:{port}/{path}" for IPv4,
|
|
332
322
|
// > and "http://[::1]:{port}/{path}" for IPv6.
|
|
333
|
-
throw new
|
|
323
|
+
throw new InvalidRedirectUriError(`Loopback redirect URI ${url} must use HTTP`);
|
|
334
324
|
}
|
|
335
325
|
break;
|
|
336
326
|
}
|
|
@@ -349,11 +339,11 @@ class ClientManager {
|
|
|
349
339
|
// > Authorization Servers MAY reject Redirection URI values using
|
|
350
340
|
// > the http scheme, other than the loopback case for Native
|
|
351
341
|
// > Clients.
|
|
352
|
-
throw new
|
|
342
|
+
throw new InvalidRedirectUriError('Only loopback redirect URIs are allowed to use the "http" scheme');
|
|
353
343
|
}
|
|
354
344
|
case url.protocol === 'https:': {
|
|
355
|
-
if (
|
|
356
|
-
throw new
|
|
345
|
+
if (isLocalHostname(url.hostname)) {
|
|
346
|
+
throw new InvalidRedirectUriError(`Redirect URI "${url}"'s domain name must not be a local hostname`);
|
|
357
347
|
}
|
|
358
348
|
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
|
359
349
|
//
|
|
@@ -389,7 +379,7 @@ class ClientManager {
|
|
|
389
379
|
}
|
|
390
380
|
case isPrivateUseUriScheme(url): {
|
|
391
381
|
if (metadata.application_type !== 'native') {
|
|
392
|
-
throw new
|
|
382
|
+
throw new InvalidRedirectUriError(`Private-Use URI Scheme redirect URI are only allowed for native apps`);
|
|
393
383
|
}
|
|
394
384
|
break;
|
|
395
385
|
}
|
|
@@ -398,13 +388,13 @@ class ClientManager {
|
|
|
398
388
|
//
|
|
399
389
|
// > At a minimum, any private-use URI scheme that doesn't contain a
|
|
400
390
|
// > period character (".") SHOULD be rejected.
|
|
401
|
-
throw new
|
|
391
|
+
throw new InvalidRedirectUriError(`Invalid redirect URI scheme "${url.protocol}"`);
|
|
402
392
|
}
|
|
403
393
|
}
|
|
404
|
-
if (
|
|
394
|
+
if (isOAuthClientIdLoopback(clientId)) {
|
|
405
395
|
return this.validateLoopbackClientMetadata(clientId, metadata);
|
|
406
396
|
}
|
|
407
|
-
else if (
|
|
397
|
+
else if (isOAuthClientIdDiscoverable(clientId)) {
|
|
408
398
|
return this.validateDiscoverableClientMetadata(clientId, metadata);
|
|
409
399
|
}
|
|
410
400
|
else {
|
|
@@ -413,23 +403,23 @@ class ClientManager {
|
|
|
413
403
|
}
|
|
414
404
|
validateLoopbackClientMetadata(clientId, metadata) {
|
|
415
405
|
if (metadata.client_uri) {
|
|
416
|
-
throw new
|
|
406
|
+
throw new InvalidClientMetadataError('client_uri is not allowed for loopback clients');
|
|
417
407
|
}
|
|
418
408
|
if (metadata.application_type !== 'native') {
|
|
419
|
-
throw new
|
|
409
|
+
throw new InvalidClientMetadataError('Loopback clients must have application_type "native"');
|
|
420
410
|
}
|
|
421
411
|
const method = metadata.token_endpoint_auth_method;
|
|
422
412
|
if (method !== 'none') {
|
|
423
|
-
throw new
|
|
413
|
+
throw new InvalidClientMetadataError(`Loopback clients are not allowed to use "token_endpoint_auth_method" ${method}`);
|
|
424
414
|
}
|
|
425
415
|
return metadata;
|
|
426
416
|
}
|
|
427
417
|
validateDiscoverableClientMetadata(clientId, metadata) {
|
|
428
418
|
if (!metadata.client_id) {
|
|
429
419
|
// https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
|
|
430
|
-
throw new
|
|
420
|
+
throw new InvalidClientMetadataError(`client_id is required for discoverable clients`);
|
|
431
421
|
}
|
|
432
|
-
const clientIdUrl =
|
|
422
|
+
const clientIdUrl = parseDiscoverableClientId(clientId);
|
|
433
423
|
if (metadata.client_uri) {
|
|
434
424
|
// https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
|
|
435
425
|
//
|
|
@@ -437,20 +427,20 @@ class ClientManager {
|
|
|
437
427
|
// relaxed in the future.
|
|
438
428
|
const clientUriUrl = new URL(metadata.client_uri);
|
|
439
429
|
if (clientUriUrl.origin !== clientIdUrl.origin) {
|
|
440
|
-
throw new
|
|
430
|
+
throw new InvalidClientMetadataError(`client_uri must have the same origin as the client_id`);
|
|
441
431
|
}
|
|
442
432
|
if (clientIdUrl.pathname !== clientUriUrl.pathname) {
|
|
443
433
|
if (!clientIdUrl.pathname.startsWith(clientUriUrl.pathname.endsWith('/')
|
|
444
434
|
? clientUriUrl.pathname
|
|
445
435
|
: `${clientUriUrl.pathname}/`)) {
|
|
446
|
-
throw new
|
|
436
|
+
throw new InvalidClientMetadataError(`client_uri must be a parent URL of the client_id`);
|
|
447
437
|
}
|
|
448
438
|
}
|
|
449
439
|
}
|
|
450
440
|
for (const redirectUri of metadata.redirect_uris) {
|
|
451
441
|
// @NOTE at this point, all redirect URIs have already been validated by
|
|
452
442
|
// oauthRedirectUriSchema
|
|
453
|
-
const url =
|
|
443
|
+
const url = parseRedirectUri(redirectUri);
|
|
454
444
|
if (isPrivateUseUriScheme(url)) {
|
|
455
445
|
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
|
456
446
|
//
|
|
@@ -475,14 +465,13 @@ class ClientManager {
|
|
|
475
465
|
// > redirect_uri of com.example.app:/callback.
|
|
476
466
|
const protocol = `${reverseDomain(clientIdUrl.hostname)}:`;
|
|
477
467
|
if (url.protocol !== protocol) {
|
|
478
|
-
throw new
|
|
468
|
+
throw new InvalidRedirectUriError(`Private-Use URI Scheme redirect URI, for discoverable client metadata, must be the fully qualified domain name (FQDN) of the client_id, in reverse order (${protocol})`);
|
|
479
469
|
}
|
|
480
470
|
}
|
|
481
471
|
}
|
|
482
472
|
return metadata;
|
|
483
473
|
}
|
|
484
474
|
}
|
|
485
|
-
exports.ClientManager = ClientManager;
|
|
486
475
|
function isDuplicate(value, index, array) {
|
|
487
476
|
return array.includes(value, index + 1);
|
|
488
477
|
}
|