@atcute/oauth-types 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/README.md +48 -0
- package/dist/build-client-metadata.d.ts +168 -0
- package/dist/build-client-metadata.d.ts.map +1 -0
- package/dist/build-client-metadata.js +53 -0
- package/dist/build-client-metadata.js.map +1 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/atcute-confidential-client-metadata.d.ts +21 -0
- package/dist/schemas/atcute-confidential-client-metadata.d.ts.map +1 -0
- package/dist/schemas/atcute-confidential-client-metadata.js +112 -0
- package/dist/schemas/atcute-confidential-client-metadata.js.map +1 -0
- package/dist/schemas/atproto-authorization-server-metadata.d.ts +55 -0
- package/dist/schemas/atproto-authorization-server-metadata.d.ts.map +1 -0
- package/dist/schemas/atproto-authorization-server-metadata.js +25 -0
- package/dist/schemas/atproto-authorization-server-metadata.js.map +1 -0
- package/dist/schemas/atproto-oauth-scope.d.ts +8 -0
- package/dist/schemas/atproto-oauth-scope.d.ts.map +1 -0
- package/dist/schemas/atproto-oauth-scope.js +12 -0
- package/dist/schemas/atproto-oauth-scope.js.map +1 -0
- package/dist/schemas/atproto-oauth-token-response.d.ts +19 -0
- package/dist/schemas/atproto-oauth-token-response.d.ts.map +1 -0
- package/dist/schemas/atproto-oauth-token-response.js +16 -0
- package/dist/schemas/atproto-oauth-token-response.js.map +1 -0
- package/dist/schemas/atproto-protected-resource-metadata.d.ts +21 -0
- package/dist/schemas/atproto-protected-resource-metadata.d.ts.map +1 -0
- package/dist/schemas/atproto-protected-resource-metadata.js +18 -0
- package/dist/schemas/atproto-protected-resource-metadata.js.map +1 -0
- package/dist/schemas/jwk.d.ts +241 -0
- package/dist/schemas/jwk.d.ts.map +1 -0
- package/dist/schemas/jwk.js +138 -0
- package/dist/schemas/jwk.js.map +1 -0
- package/dist/schemas/jwks.d.ts +242 -0
- package/dist/schemas/jwks.d.ts.map +1 -0
- package/dist/schemas/jwks.js +34 -0
- package/dist/schemas/jwks.js.map +1 -0
- package/dist/schemas/oauth-authorization-details.d.ts +64 -0
- package/dist/schemas/oauth-authorization-details.d.ts.map +1 -0
- package/dist/schemas/oauth-authorization-details.js +37 -0
- package/dist/schemas/oauth-authorization-details.js.map +1 -0
- package/dist/schemas/oauth-authorization-server-metadata.d.ts +96 -0
- package/dist/schemas/oauth-authorization-server-metadata.d.ts.map +1 -0
- package/dist/schemas/oauth-authorization-server-metadata.js +81 -0
- package/dist/schemas/oauth-authorization-server-metadata.js.map +1 -0
- package/dist/schemas/oauth-client-id-discoverable.d.ts +6 -0
- package/dist/schemas/oauth-client-id-discoverable.d.ts.map +1 -0
- package/dist/schemas/oauth-client-id-discoverable.js +43 -0
- package/dist/schemas/oauth-client-id-discoverable.js.map +1 -0
- package/dist/schemas/oauth-client-id.d.ts +5 -0
- package/dist/schemas/oauth-client-id.d.ts.map +1 -0
- package/dist/schemas/oauth-client-id.js +4 -0
- package/dist/schemas/oauth-client-id.js.map +1 -0
- package/dist/schemas/oauth-client-metadata.d.ts +164 -0
- package/dist/schemas/oauth-client-metadata.d.ts.map +1 -0
- package/dist/schemas/oauth-client-metadata.js +74 -0
- package/dist/schemas/oauth-client-metadata.js.map +1 -0
- package/dist/schemas/oauth-code-challenge-method.d.ts +4 -0
- package/dist/schemas/oauth-code-challenge-method.d.ts.map +1 -0
- package/dist/schemas/oauth-code-challenge-method.js +3 -0
- package/dist/schemas/oauth-code-challenge-method.js.map +1 -0
- package/dist/schemas/oauth-endpoint-auth-method.d.ts +4 -0
- package/dist/schemas/oauth-endpoint-auth-method.d.ts.map +1 -0
- package/dist/schemas/oauth-endpoint-auth-method.js +3 -0
- package/dist/schemas/oauth-endpoint-auth-method.js.map +1 -0
- package/dist/schemas/oauth-grant-type.d.ts +4 -0
- package/dist/schemas/oauth-grant-type.d.ts.map +1 -0
- package/dist/schemas/oauth-grant-type.js +4 -0
- package/dist/schemas/oauth-grant-type.js.map +1 -0
- package/dist/schemas/oauth-issuer-identifier.d.ts +4 -0
- package/dist/schemas/oauth-issuer-identifier.d.ts.map +1 -0
- package/dist/schemas/oauth-issuer-identifier.js +21 -0
- package/dist/schemas/oauth-issuer-identifier.js.map +1 -0
- package/dist/schemas/oauth-par-response.d.ts +7 -0
- package/dist/schemas/oauth-par-response.d.ts.map +1 -0
- package/dist/schemas/oauth-par-response.js +7 -0
- package/dist/schemas/oauth-par-response.js.map +1 -0
- package/dist/schemas/oauth-prompt.d.ts +13 -0
- package/dist/schemas/oauth-prompt.d.ts.map +1 -0
- package/dist/schemas/oauth-prompt.js +12 -0
- package/dist/schemas/oauth-prompt.js.map +1 -0
- package/dist/schemas/oauth-protected-resource-metadata.d.ts +66 -0
- package/dist/schemas/oauth-protected-resource-metadata.d.ts.map +1 -0
- package/dist/schemas/oauth-protected-resource-metadata.js +71 -0
- package/dist/schemas/oauth-protected-resource-metadata.js.map +1 -0
- package/dist/schemas/oauth-redirect-uri.d.ts +20 -0
- package/dist/schemas/oauth-redirect-uri.d.ts.map +1 -0
- package/dist/schemas/oauth-redirect-uri.js +32 -0
- package/dist/schemas/oauth-redirect-uri.js.map +1 -0
- package/dist/schemas/oauth-response-mode.d.ts +4 -0
- package/dist/schemas/oauth-response-mode.d.ts.map +1 -0
- package/dist/schemas/oauth-response-mode.js +3 -0
- package/dist/schemas/oauth-response-mode.js.map +1 -0
- package/dist/schemas/oauth-response-type.d.ts +4 -0
- package/dist/schemas/oauth-response-type.d.ts.map +1 -0
- package/dist/schemas/oauth-response-type.js +8 -0
- package/dist/schemas/oauth-response-type.js.map +1 -0
- package/dist/schemas/oauth-scope.d.ts +12 -0
- package/dist/schemas/oauth-scope.d.ts.map +1 -0
- package/dist/schemas/oauth-scope.js +14 -0
- package/dist/schemas/oauth-scope.js.map +1 -0
- package/dist/schemas/oauth-token-response.d.ts +22 -0
- package/dist/schemas/oauth-token-response.d.ts.map +1 -0
- package/dist/schemas/oauth-token-response.js +19 -0
- package/dist/schemas/oauth-token-response.js.map +1 -0
- package/dist/schemas/oauth-token-type.d.ts +5 -0
- package/dist/schemas/oauth-token-type.d.ts.map +1 -0
- package/dist/schemas/oauth-token-type.js +13 -0
- package/dist/schemas/oauth-token-type.js.map +1 -0
- package/dist/schemas/uri.d.ts +18 -0
- package/dist/schemas/uri.d.ts.map +1 -0
- package/dist/schemas/uri.js +81 -0
- package/dist/schemas/uri.js.map +1 -0
- package/dist/schemas/utils.d.ts +32 -0
- package/dist/schemas/utils.d.ts.map +1 -0
- package/dist/schemas/utils.js +94 -0
- package/dist/schemas/utils.js.map +1 -0
- package/dist/scope.d.ts +84 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +102 -0
- package/dist/scope.js.map +1 -0
- package/lib/build-client-metadata.ts +72 -0
- package/lib/constants.ts +5 -0
- package/lib/index.ts +116 -0
- package/lib/schemas/atcute-confidential-client-metadata.ts +139 -0
- package/lib/schemas/atproto-authorization-server-metadata.ts +32 -0
- package/lib/schemas/atproto-oauth-scope.ts +18 -0
- package/lib/schemas/atproto-oauth-token-response.ts +20 -0
- package/lib/schemas/atproto-protected-resource-metadata.ts +24 -0
- package/lib/schemas/jwk.ts +189 -0
- package/lib/schemas/jwks.ts +45 -0
- package/lib/schemas/oauth-authorization-details.ts +43 -0
- package/lib/schemas/oauth-authorization-server-metadata.ts +101 -0
- package/lib/schemas/oauth-client-id-discoverable.ts +53 -0
- package/lib/schemas/oauth-client-id.ts +6 -0
- package/lib/schemas/oauth-client-metadata.ts +83 -0
- package/lib/schemas/oauth-code-challenge-method.ts +5 -0
- package/lib/schemas/oauth-endpoint-auth-method.ts +13 -0
- package/lib/schemas/oauth-grant-type.ts +13 -0
- package/lib/schemas/oauth-issuer-identifier.ts +30 -0
- package/lib/schemas/oauth-par-response.ts +10 -0
- package/lib/schemas/oauth-prompt.ts +20 -0
- package/lib/schemas/oauth-protected-resource-metadata.ts +89 -0
- package/lib/schemas/oauth-redirect-uri.ts +42 -0
- package/lib/schemas/oauth-response-mode.ts +9 -0
- package/lib/schemas/oauth-response-type.ts +17 -0
- package/lib/schemas/oauth-scope.ts +18 -0
- package/lib/schemas/oauth-token-response.ts +22 -0
- package/lib/schemas/oauth-token-type.ts +15 -0
- package/lib/schemas/uri.ts +100 -0
- package/lib/schemas/utils.ts +113 -0
- package/lib/scope.ts +187 -0
- package/package.json +38 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Keyset } from '@atcute/oauth-keyset';
|
|
2
|
+
|
|
3
|
+
import { FALLBACK_ALG } from './constants.js';
|
|
4
|
+
import {
|
|
5
|
+
confidentialClientMetadataSchema,
|
|
6
|
+
type ConfidentialClientMetadata,
|
|
7
|
+
} from './schemas/atcute-confidential-client-metadata.js';
|
|
8
|
+
import type { OAuthClientMetadata } from './schemas/oauth-client-metadata.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* builds an atproto client metadata
|
|
12
|
+
*
|
|
13
|
+
*
|
|
14
|
+
* @param input client metadata
|
|
15
|
+
* @param keyset available keys
|
|
16
|
+
* @returns built client metadata
|
|
17
|
+
*/
|
|
18
|
+
export const buildClientMetadata = (
|
|
19
|
+
input: ConfidentialClientMetadata,
|
|
20
|
+
keyset: Keyset,
|
|
21
|
+
): OAuthClientMetadata => {
|
|
22
|
+
// validate user-facing schema is correct
|
|
23
|
+
const conf = confidentialClientMetadataSchema.parse(input, { mode: 'passthrough' });
|
|
24
|
+
|
|
25
|
+
// build full OAuth client metadata (atproto defaults and requirements)
|
|
26
|
+
const metadata: OAuthClientMetadata = {
|
|
27
|
+
client_id: conf.client_id,
|
|
28
|
+
client_name: conf.client_name,
|
|
29
|
+
client_uri: conf.client_uri,
|
|
30
|
+
policy_uri: conf.policy_uri,
|
|
31
|
+
tos_uri: conf.tos_uri,
|
|
32
|
+
logo_uri: conf.logo_uri,
|
|
33
|
+
redirect_uris: conf.redirect_uris,
|
|
34
|
+
scope: Array.isArray(conf.scope) ? conf.scope.join(' ') : conf.scope,
|
|
35
|
+
|
|
36
|
+
application_type: 'web',
|
|
37
|
+
subject_type: 'public',
|
|
38
|
+
response_types: ['code'],
|
|
39
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
40
|
+
|
|
41
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
42
|
+
token_endpoint_auth_signing_alg: FALLBACK_ALG,
|
|
43
|
+
dpop_bound_access_tokens: true,
|
|
44
|
+
|
|
45
|
+
jwks_uri: conf.jwks_uri,
|
|
46
|
+
jwks: conf.jwks_uri ? undefined : (keyset.publicJwks as OAuthClientMetadata['jwks']),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ensure at least one key supports the fallback algorithm
|
|
50
|
+
const signingKeys = Array.from(keyset);
|
|
51
|
+
if (!signingKeys.some((key) => key.alg === FALLBACK_ALG)) {
|
|
52
|
+
throw new TypeError(`"private_key_jwt" requires at least one "${FALLBACK_ALG}" signing key`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// if jwks provided inline, ensure ALL signing keys are present
|
|
56
|
+
if (metadata.jwks) {
|
|
57
|
+
const jwksKids = new Set(
|
|
58
|
+
metadata.jwks.keys
|
|
59
|
+
.filter((k) => !k.revoked)
|
|
60
|
+
.map((k) => k.kid)
|
|
61
|
+
.filter(Boolean),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
for (const key of signingKeys) {
|
|
65
|
+
if (!jwksKids.has(key.kid)) {
|
|
66
|
+
throw new TypeError(`signing key "${key.kid}" not found in jwks`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return metadata;
|
|
72
|
+
};
|
package/lib/constants.ts
ADDED
package/lib/index.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export { buildClientMetadata } from './build-client-metadata.js';
|
|
2
|
+
export { CLIENT_ASSERTION_TYPE_JWT_BEARER, FALLBACK_ALG } from './constants.js';
|
|
3
|
+
|
|
4
|
+
export * as scope from './scope.js';
|
|
5
|
+
|
|
6
|
+
// schemas
|
|
7
|
+
export {
|
|
8
|
+
confidentialClientMetadataSchema,
|
|
9
|
+
type ConfidentialClientMetadata,
|
|
10
|
+
} from './schemas/atcute-confidential-client-metadata.js';
|
|
11
|
+
export {
|
|
12
|
+
atprotoOAuthScopeSchema,
|
|
13
|
+
ATPROTO_SCOPE_VALUE,
|
|
14
|
+
DEFAULT_ATPROTO_OAUTH_SCOPE,
|
|
15
|
+
type AtprotoOAuthScope,
|
|
16
|
+
} from './schemas/atproto-oauth-scope.js';
|
|
17
|
+
export {
|
|
18
|
+
jwkPubSchema,
|
|
19
|
+
jwkSchema,
|
|
20
|
+
keyUsageSchema,
|
|
21
|
+
publicKeyUsageSchema,
|
|
22
|
+
type Jwk,
|
|
23
|
+
type JwkPub,
|
|
24
|
+
type KeyUsage,
|
|
25
|
+
} from './schemas/jwk.js';
|
|
26
|
+
export { jwksPubSchema, jwksSchema, type Jwks, type JwksPub } from './schemas/jwks.js';
|
|
27
|
+
export { oauthClientIdDiscoverableSchema } from './schemas/oauth-client-id-discoverable.js';
|
|
28
|
+
export { oauthClientIdSchema, type OAuthClientId } from './schemas/oauth-client-id.js';
|
|
29
|
+
export { oauthClientMetadataSchema, type OAuthClientMetadata } from './schemas/oauth-client-metadata.js';
|
|
30
|
+
export {
|
|
31
|
+
oauthEndpointAuthMethodSchema,
|
|
32
|
+
type OAuthEndpointAuthMethod,
|
|
33
|
+
} from './schemas/oauth-endpoint-auth-method.js';
|
|
34
|
+
export { oauthGrantTypeSchema, type OAuthGrantType } from './schemas/oauth-grant-type.js';
|
|
35
|
+
export {
|
|
36
|
+
loopbackRedirectUriSchema,
|
|
37
|
+
oauthRedirectUriSchema,
|
|
38
|
+
type LoopbackRedirectUri,
|
|
39
|
+
type OAuthRedirectUri,
|
|
40
|
+
} from './schemas/oauth-redirect-uri.js';
|
|
41
|
+
export { oauthResponseTypeSchema, type OAuthResponseType } from './schemas/oauth-response-type.js';
|
|
42
|
+
export {
|
|
43
|
+
isOAuthScope,
|
|
44
|
+
OAUTH_SCOPE_REGEXP,
|
|
45
|
+
oauthScopeSchema,
|
|
46
|
+
type OAuthScope,
|
|
47
|
+
} from './schemas/oauth-scope.js';
|
|
48
|
+
export {
|
|
49
|
+
httpsUriSchema,
|
|
50
|
+
loopbackUriSchema,
|
|
51
|
+
nonLocalWebUriSchema,
|
|
52
|
+
privateUseUriSchema,
|
|
53
|
+
urlSchema,
|
|
54
|
+
webUriSchema,
|
|
55
|
+
} from './schemas/uri.js';
|
|
56
|
+
export {
|
|
57
|
+
extractUrlPath,
|
|
58
|
+
isHostnameIP,
|
|
59
|
+
isLastOccurrence,
|
|
60
|
+
isLocalHostname,
|
|
61
|
+
isLoopbackHost,
|
|
62
|
+
isSpaceSeparatedValue,
|
|
63
|
+
} from './schemas/utils.js';
|
|
64
|
+
|
|
65
|
+
// token schemas
|
|
66
|
+
export { oauthTokenTypeSchema, type OAuthTokenType } from './schemas/oauth-token-type.js';
|
|
67
|
+
export { oauthTokenResponseSchema, type OAuthTokenResponse } from './schemas/oauth-token-response.js';
|
|
68
|
+
export {
|
|
69
|
+
atprotoOAuthTokenResponseSchema,
|
|
70
|
+
type AtprotoOAuthTokenResponse,
|
|
71
|
+
} from './schemas/atproto-oauth-token-response.js';
|
|
72
|
+
|
|
73
|
+
// PAR schemas
|
|
74
|
+
export { oauthParResponseSchema, type OAuthParResponse } from './schemas/oauth-par-response.js';
|
|
75
|
+
export {
|
|
76
|
+
oauthCodeChallengeMethodSchema,
|
|
77
|
+
type OAuthCodeChallengeMethod,
|
|
78
|
+
} from './schemas/oauth-code-challenge-method.js';
|
|
79
|
+
export { oauthResponseModeSchema, type OAuthResponseMode } from './schemas/oauth-response-mode.js';
|
|
80
|
+
export { oauthPromptSchema, type OAuthPrompt } from './schemas/oauth-prompt.js';
|
|
81
|
+
|
|
82
|
+
// authorization details
|
|
83
|
+
export {
|
|
84
|
+
oauthAuthorizationDetailSchema,
|
|
85
|
+
oauthAuthorizationDetailsSchema,
|
|
86
|
+
type OAuthAuthorizationDetail,
|
|
87
|
+
type OAuthAuthorizationDetails,
|
|
88
|
+
} from './schemas/oauth-authorization-details.js';
|
|
89
|
+
|
|
90
|
+
// server metadata
|
|
91
|
+
export {
|
|
92
|
+
oauthIssuerIdentifierSchema,
|
|
93
|
+
type OAuthIssuerIdentifier,
|
|
94
|
+
} from './schemas/oauth-issuer-identifier.js';
|
|
95
|
+
export {
|
|
96
|
+
oauthAuthorizationServerMetadataSchema,
|
|
97
|
+
oauthAuthorizationServerMetadataValidator,
|
|
98
|
+
type OAuthAuthorizationServerMetadata,
|
|
99
|
+
} from './schemas/oauth-authorization-server-metadata.js';
|
|
100
|
+
export {
|
|
101
|
+
atprotoAuthorizationServerMetadataValidator,
|
|
102
|
+
type AtprotoAuthorizationServerMetadata,
|
|
103
|
+
} from './schemas/atproto-authorization-server-metadata.js';
|
|
104
|
+
|
|
105
|
+
// protected resource metadata
|
|
106
|
+
export {
|
|
107
|
+
oauthBearerMethodSchema,
|
|
108
|
+
oauthProtectedResourceMetadataSchema,
|
|
109
|
+
oauthProtectedResourceMetadataValidator,
|
|
110
|
+
type OAuthBearerMethod,
|
|
111
|
+
type OAuthProtectedResourceMetadata,
|
|
112
|
+
} from './schemas/oauth-protected-resource-metadata.js';
|
|
113
|
+
export {
|
|
114
|
+
atprotoProtectedResourceMetadataValidator,
|
|
115
|
+
type AtprotoProtectedResourceMetadata,
|
|
116
|
+
} from './schemas/atproto-protected-resource-metadata.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import { atprotoOAuthScopeSchema } from './atproto-oauth-scope.js';
|
|
4
|
+
import { oauthClientIdDiscoverableSchema } from './oauth-client-id-discoverable.js';
|
|
5
|
+
import { httpsUriSchema, nonLocalWebUriSchema, webUriSchema } from './uri.js';
|
|
6
|
+
import { isLocalHostname } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const SINGLE_SCOPE_RE = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
|
|
9
|
+
|
|
10
|
+
const singleScopeSchema = v.string().assert((input) => SINGLE_SCOPE_RE.test(input), `invalid OAuth scope`);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* user-facing client metadata for configuring a confidential OAuth client.
|
|
14
|
+
*
|
|
15
|
+
* this is a lean subset of OAuth client metadata, focused on what you actually provide.
|
|
16
|
+
* the library will fill in atproto-required values like `dpop_bound_access_tokens`,
|
|
17
|
+
* `token_endpoint_auth_method`, and default `grant_types` / `response_types`.
|
|
18
|
+
*/
|
|
19
|
+
export const confidentialClientMetadataSchema = v
|
|
20
|
+
.object({
|
|
21
|
+
/** discoverable https client_id URL (where metadata is hosted) */
|
|
22
|
+
client_id: oauthClientIdDiscoverableSchema,
|
|
23
|
+
|
|
24
|
+
/** redirect URIs for authorization responses (must be https) */
|
|
25
|
+
redirect_uris: v
|
|
26
|
+
.array(httpsUriSchema)
|
|
27
|
+
.assert((arr) => arr.length > 0, `must have at least one redirect URI`)
|
|
28
|
+
.assert((arr) => {
|
|
29
|
+
for (const uri of arr) {
|
|
30
|
+
const url = new URL(uri);
|
|
31
|
+
if (url.username || url.password) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}, `redirect URIs must not contain credentials`),
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OAuth scope - either:
|
|
40
|
+
* - a space-separated string (must include "atproto")
|
|
41
|
+
* - an array of scope strings ('atproto' is added automatically)
|
|
42
|
+
*/
|
|
43
|
+
scope: v.union(
|
|
44
|
+
atprotoOAuthScopeSchema.chain((input) => {
|
|
45
|
+
const scopes = input.split(/\s+/);
|
|
46
|
+
|
|
47
|
+
for (let i = 0, len = scopes.length; i < len; i++) {
|
|
48
|
+
const aka = scopes[i];
|
|
49
|
+
|
|
50
|
+
for (let j = 0; j < i; j++) {
|
|
51
|
+
if (aka === scopes[j]) {
|
|
52
|
+
return v.err(`duplicate "${aka}" scope`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return v.ok(input);
|
|
58
|
+
}),
|
|
59
|
+
v.array(singleScopeSchema).chain((input) => {
|
|
60
|
+
if (!input.includes('atproto')) {
|
|
61
|
+
input = ['atproto', ...input];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (let i = 0, len = input.length; i < len; i++) {
|
|
65
|
+
const aka = input[i];
|
|
66
|
+
|
|
67
|
+
for (let j = 0; j < i; j++) {
|
|
68
|
+
if (aka === input[j]) {
|
|
69
|
+
return v.err(`duplicate "${aka}" scope`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return v.ok(input);
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
|
|
78
|
+
/** optional client homepage */
|
|
79
|
+
client_uri: webUriSchema.optional(),
|
|
80
|
+
/** optional display name */
|
|
81
|
+
client_name: v.string().optional(),
|
|
82
|
+
/** optional policy url */
|
|
83
|
+
policy_uri: nonLocalWebUriSchema.optional(),
|
|
84
|
+
/** optional terms of service url */
|
|
85
|
+
tos_uri: nonLocalWebUriSchema.optional(),
|
|
86
|
+
/** optional logo url */
|
|
87
|
+
logo_uri: nonLocalWebUriSchema.optional(),
|
|
88
|
+
|
|
89
|
+
/** optional JWKS URL; if omitted, the library will inline jwks from the keyset */
|
|
90
|
+
jwks_uri: httpsUriSchema.optional(),
|
|
91
|
+
})
|
|
92
|
+
.chain((input) => {
|
|
93
|
+
const clientIdUrl = new URL(input.client_id);
|
|
94
|
+
if (isLocalHostname(clientIdUrl.hostname)) {
|
|
95
|
+
return v.err({ message: `client_id hostname is invalid`, path: ['client_id'] });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (input.jwks_uri) {
|
|
99
|
+
const jwksUrl = new URL(input.jwks_uri);
|
|
100
|
+
|
|
101
|
+
if (jwksUrl.username || jwksUrl.password) {
|
|
102
|
+
return v.err({ message: `jwks_uri must not contain credentials`, path: ['jwks_uri'] });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isLocalHostname(jwksUrl.hostname)) {
|
|
106
|
+
return v.err({ message: `jwks_uri hostname is invalid`, path: ['jwks_uri'] });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// for discoverable clients, client_uri (if provided) must be same-origin parent of client_id
|
|
111
|
+
if (input.client_uri) {
|
|
112
|
+
const clientUriUrl = new URL(input.client_uri);
|
|
113
|
+
|
|
114
|
+
if (isLocalHostname(clientUriUrl.hostname)) {
|
|
115
|
+
return v.err({ message: `client_uri hostname is invalid`, path: ['client_uri'] });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (clientUriUrl.origin !== clientIdUrl.origin) {
|
|
119
|
+
return v.err({
|
|
120
|
+
message: `client_uri must have the same origin as the client_id`,
|
|
121
|
+
path: ['client_uri'],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (clientIdUrl.pathname !== clientUriUrl.pathname) {
|
|
126
|
+
const prefix = clientUriUrl.pathname.endsWith('/')
|
|
127
|
+
? clientUriUrl.pathname
|
|
128
|
+
: `${clientUriUrl.pathname}/`;
|
|
129
|
+
|
|
130
|
+
if (!clientIdUrl.pathname.startsWith(prefix)) {
|
|
131
|
+
return v.err({ message: `client_uri must be a parent URL of the client_id`, path: ['client_uri'] });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return v.ok(input);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export type ConfidentialClientMetadata = v.Infer<typeof confidentialClientMetadataSchema>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import { oauthAuthorizationServerMetadataValidator } from './oauth-authorization-server-metadata.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AT Protocol authorization server metadata with required fields and assertions.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://atproto.com/specs/oauth}
|
|
9
|
+
*/
|
|
10
|
+
export const atprotoAuthorizationServerMetadataValidator = oauthAuthorizationServerMetadataValidator.chain(
|
|
11
|
+
(data) => {
|
|
12
|
+
// atproto requires client_id_metadata_document support
|
|
13
|
+
if (data.client_id_metadata_document_supported !== true) {
|
|
14
|
+
return v.err({
|
|
15
|
+
message: `atproto requires client_id_metadata_document_supported to be true`,
|
|
16
|
+
path: ['client_id_metadata_document_supported'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// atproto requires PAR
|
|
21
|
+
if (!data.pushed_authorization_request_endpoint) {
|
|
22
|
+
return v.err({
|
|
23
|
+
message: `atproto requires pushed_authorization_request_endpoint to be true`,
|
|
24
|
+
path: ['pushed_authorization_request_endpoint'],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return v.ok(data as typeof data & { pushed_authorization_request_endpoint: string });
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type AtprotoAuthorizationServerMetadata = v.Infer<typeof atprotoAuthorizationServerMetadataValidator>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import { isOAuthScope } from './oauth-scope.js';
|
|
4
|
+
import { isSpaceSeparatedValue } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export const ATPROTO_SCOPE_VALUE = 'atproto';
|
|
7
|
+
|
|
8
|
+
const isAtprotoOAuthScope = (input: string): boolean => {
|
|
9
|
+
return isOAuthScope(input) && isSpaceSeparatedValue(ATPROTO_SCOPE_VALUE, input);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** atproto OAuth scope (must include "atproto") */
|
|
13
|
+
export const atprotoOAuthScopeSchema = v.string().assert(isAtprotoOAuthScope, `invalid atproto OAuth scope`);
|
|
14
|
+
|
|
15
|
+
export type AtprotoOAuthScope = v.Infer<typeof atprotoOAuthScopeSchema>;
|
|
16
|
+
|
|
17
|
+
/** default scope is for reading identity (did) only */
|
|
18
|
+
export const DEFAULT_ATPROTO_OAUTH_SCOPE: AtprotoOAuthScope = ATPROTO_SCOPE_VALUE;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isAtprotoDid } from '@atcute/identity';
|
|
2
|
+
|
|
3
|
+
import * as v from '@badrap/valita';
|
|
4
|
+
|
|
5
|
+
import { atprotoOAuthScopeSchema } from './atproto-oauth-scope.js';
|
|
6
|
+
import { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js';
|
|
7
|
+
|
|
8
|
+
export const atprotoOAuthTokenResponseSchema = v.object({
|
|
9
|
+
access_token: v.string(),
|
|
10
|
+
token_type: v.literal('DPoP'),
|
|
11
|
+
sub: v.string().assert(isAtprotoDid, `must be a did:plc or did:web`),
|
|
12
|
+
scope: atprotoOAuthScopeSchema,
|
|
13
|
+
refresh_token: v.string().optional(),
|
|
14
|
+
expires_in: v.number().optional(),
|
|
15
|
+
// https://datatracker.ietf.org/doc/html/rfc9396#name-enriched-authorization-deta
|
|
16
|
+
authorization_details: oauthAuthorizationDetailsSchema.optional(),
|
|
17
|
+
// OpenID is not compatible with atproto identities
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type AtprotoOAuthTokenResponse = v.Infer<typeof atprotoOAuthTokenResponseSchema>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import { oauthProtectedResourceMetadataValidator } from './oauth-protected-resource-metadata.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AT Protocol protected resource metadata with required fields.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://atproto.com/specs/oauth}
|
|
9
|
+
*/
|
|
10
|
+
export const atprotoProtectedResourceMetadataValidator = oauthProtectedResourceMetadataValidator.chain(
|
|
11
|
+
(data) => {
|
|
12
|
+
// atproto requires exactly one authorization server
|
|
13
|
+
if (data.authorization_servers?.length !== 1) {
|
|
14
|
+
return v.err({
|
|
15
|
+
message: `atproto requires exactly one authorization server`,
|
|
16
|
+
path: ['authorization_servers'],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return v.ok(data as typeof data & { authorization_servers: [string] });
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export type AtprotoProtectedResourceMetadata = v.Infer<typeof atprotoProtectedResourceMetadataValidator>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import { isLastOccurrence } from './utils.js';
|
|
4
|
+
|
|
5
|
+
// key usage constants
|
|
6
|
+
const PUBLIC_KEY_USAGE = ['verify', 'encrypt', 'wrapKey'] as const;
|
|
7
|
+
const PRIVATE_KEY_USAGE = ['sign', 'decrypt', 'unwrapKey', 'deriveKey', 'deriveBits'] as const;
|
|
8
|
+
const KEY_USAGE = [...PRIVATE_KEY_USAGE, ...PUBLIC_KEY_USAGE] as const;
|
|
9
|
+
|
|
10
|
+
type InternalKeyUsage = (typeof KEY_USAGE)[number];
|
|
11
|
+
|
|
12
|
+
const isPublicKeyUsage = (usage: unknown): usage is (typeof PUBLIC_KEY_USAGE)[number] => {
|
|
13
|
+
return (PUBLIC_KEY_USAGE as readonly unknown[]).includes(usage);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isPrivateKeyUsage = (usage: unknown): usage is (typeof PRIVATE_KEY_USAGE)[number] => {
|
|
17
|
+
return (PRIVATE_KEY_USAGE as readonly unknown[]).includes(usage);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isSigKeyUsage = (v: InternalKeyUsage): boolean => v === 'verify';
|
|
21
|
+
const isEncKeyUsage = (v: InternalKeyUsage): boolean => v === 'encrypt' || v === 'wrapKey';
|
|
22
|
+
|
|
23
|
+
export const keyUsageSchema = v.union(
|
|
24
|
+
v.literal('verify'),
|
|
25
|
+
v.literal('encrypt'),
|
|
26
|
+
v.literal('wrapKey'),
|
|
27
|
+
v.literal('sign'),
|
|
28
|
+
v.literal('decrypt'),
|
|
29
|
+
v.literal('unwrapKey'),
|
|
30
|
+
v.literal('deriveKey'),
|
|
31
|
+
v.literal('deriveBits'),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const publicKeyUsageSchema = v.union(v.literal('verify'), v.literal('encrypt'), v.literal('wrapKey'));
|
|
35
|
+
|
|
36
|
+
const jwkBaseSchema = v.object({
|
|
37
|
+
kty: v.string(),
|
|
38
|
+
alg: v.string().optional(),
|
|
39
|
+
kid: v.string().optional(),
|
|
40
|
+
use: v.union(v.literal('sig'), v.literal('enc')).optional(),
|
|
41
|
+
key_ops: v.array(keyUsageSchema).optional(),
|
|
42
|
+
|
|
43
|
+
// X.509
|
|
44
|
+
x5c: v.array(v.string()).optional(),
|
|
45
|
+
x5t: v.string().optional(),
|
|
46
|
+
'x5t#S256': v.string().optional(),
|
|
47
|
+
x5u: v.string().optional(),
|
|
48
|
+
|
|
49
|
+
// WebCrypto
|
|
50
|
+
ext: v.boolean().optional(),
|
|
51
|
+
|
|
52
|
+
// Federation Historical Keys Response
|
|
53
|
+
iat: v.number().optional(),
|
|
54
|
+
exp: v.number().optional(),
|
|
55
|
+
nbf: v.number().optional(),
|
|
56
|
+
revoked: v
|
|
57
|
+
.object({
|
|
58
|
+
revoked_at: v.number(),
|
|
59
|
+
reason: v.string().optional(),
|
|
60
|
+
})
|
|
61
|
+
.optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const jwkRsaKeySchema = jwkBaseSchema.extend({
|
|
65
|
+
kty: v.literal('RSA'),
|
|
66
|
+
alg: v
|
|
67
|
+
.union(
|
|
68
|
+
v.literal('RS256'),
|
|
69
|
+
v.literal('RS384'),
|
|
70
|
+
v.literal('RS512'),
|
|
71
|
+
v.literal('PS256'),
|
|
72
|
+
v.literal('PS384'),
|
|
73
|
+
v.literal('PS512'),
|
|
74
|
+
)
|
|
75
|
+
.optional(),
|
|
76
|
+
n: v.string(),
|
|
77
|
+
e: v.string(),
|
|
78
|
+
d: v.string().optional(),
|
|
79
|
+
p: v.string().optional(),
|
|
80
|
+
q: v.string().optional(),
|
|
81
|
+
dp: v.string().optional(),
|
|
82
|
+
dq: v.string().optional(),
|
|
83
|
+
qi: v.string().optional(),
|
|
84
|
+
oth: v
|
|
85
|
+
.array(
|
|
86
|
+
v.object({
|
|
87
|
+
r: v.string().optional(),
|
|
88
|
+
d: v.string().optional(),
|
|
89
|
+
t: v.string().optional(),
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
.optional(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const jwkEcKeySchema = jwkBaseSchema.extend({
|
|
96
|
+
kty: v.literal('EC'),
|
|
97
|
+
alg: v.union(v.literal('ES256'), v.literal('ES384'), v.literal('ES512')).optional(),
|
|
98
|
+
crv: v.union(v.literal('P-256'), v.literal('P-384'), v.literal('P-521')),
|
|
99
|
+
x: v.string(),
|
|
100
|
+
y: v.string(),
|
|
101
|
+
d: v.string().optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const jwkEcSecp256k1KeySchema = jwkBaseSchema.extend({
|
|
105
|
+
kty: v.literal('EC'),
|
|
106
|
+
alg: v.literal('ES256K').optional(),
|
|
107
|
+
crv: v.literal('secp256k1'),
|
|
108
|
+
x: v.string(),
|
|
109
|
+
y: v.string(),
|
|
110
|
+
d: v.string().optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const jwkOkpKeySchema = jwkBaseSchema.extend({
|
|
114
|
+
kty: v.literal('OKP'),
|
|
115
|
+
alg: v.literal('EdDSA').optional(),
|
|
116
|
+
crv: v.union(v.literal('Ed25519'), v.literal('Ed448')),
|
|
117
|
+
x: v.string(),
|
|
118
|
+
d: v.string().optional(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const jwkSymKeySchema = jwkBaseSchema.extend({
|
|
122
|
+
kty: v.literal('oct'),
|
|
123
|
+
alg: v.union(v.literal('HS256'), v.literal('HS384'), v.literal('HS512')).optional(),
|
|
124
|
+
k: v.string(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const hasPrivateSecret = <J extends object>(jwk: J): boolean => {
|
|
128
|
+
return ('d' in jwk && jwk.d != null) || ('k' in jwk && jwk.k != null);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const isPublicJwk = <J extends object>(jwk: J): boolean => {
|
|
132
|
+
return !hasPrivateSecret(jwk);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** JWK schema for known key types */
|
|
136
|
+
export const jwkSchema = v
|
|
137
|
+
.union(jwkRsaKeySchema, jwkEcKeySchema, jwkEcSecp256k1KeySchema, jwkOkpKeySchema, jwkSymKeySchema)
|
|
138
|
+
.chain((k) => {
|
|
139
|
+
// "use" can only be used with public keys
|
|
140
|
+
if (k.use != null && !isPublicJwk(k)) {
|
|
141
|
+
return v.err({ message: `"use" can only be used with public keys`, path: ['use'] });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// private key usage not allowed for public keys
|
|
145
|
+
if (k.key_ops?.some(isPrivateKeyUsage) && isPublicJwk(k)) {
|
|
146
|
+
return v.err({ message: `private key usage not allowed for public keys`, path: ['key_ops'] });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// key_ops must not contain duplicates
|
|
150
|
+
if (k.key_ops && !k.key_ops.every(isLastOccurrence)) {
|
|
151
|
+
return v.err({ message: `key_ops must not contain duplicates`, path: ['key_ops'] });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// "use" and "key_ops" must be consistent
|
|
155
|
+
if (k.use != null && k.key_ops != null) {
|
|
156
|
+
const consistent =
|
|
157
|
+
(k.use === 'sig' && k.key_ops.every(isSigKeyUsage)) ||
|
|
158
|
+
(k.use === 'enc' && k.key_ops.every(isEncKeyUsage));
|
|
159
|
+
if (!consistent) {
|
|
160
|
+
return v.err({ message: `"key_ops" must be consistent with "use"`, path: ['key_ops'] });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return v.ok(k);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/** public JWK schema (kid required, no private keys) */
|
|
168
|
+
export const jwkPubSchema = jwkSchema.chain((k) => {
|
|
169
|
+
if (k.kid == null) {
|
|
170
|
+
return v.err({ message: `"kid" is required`, path: ['kid'] });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!isPublicJwk(k)) {
|
|
174
|
+
return v.err({ message: `private key not allowed` });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (k.key_ops && !k.key_ops.every(isPublicKeyUsage)) {
|
|
178
|
+
return v.err({
|
|
179
|
+
message: `"key_ops" must not contain private key usage for public keys`,
|
|
180
|
+
path: ['key_ops'],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return v.ok(k);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export type KeyUsage = v.Infer<typeof keyUsageSchema>;
|
|
188
|
+
export type Jwk = v.Infer<typeof jwkSchema>;
|
|
189
|
+
export type JwkPub = v.Infer<typeof jwkPubSchema>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as v from '@badrap/valita';
|
|
2
|
+
|
|
3
|
+
import { jwkPubSchema, jwkSchema, type Jwk, type JwkPub } from './jwk.js';
|
|
4
|
+
|
|
5
|
+
/** JWKS (JSON Web Key Set) */
|
|
6
|
+
export const jwksSchema = v.object({
|
|
7
|
+
keys: v.array(v.unknown()).chain((input, options) => {
|
|
8
|
+
// implementations SHOULD ignore JWKs within a JWK Set that use "kty"
|
|
9
|
+
// values that are not understood, are missing required members, or
|
|
10
|
+
// have values out of the supported ranges.
|
|
11
|
+
const keys: Jwk[] = [];
|
|
12
|
+
|
|
13
|
+
for (const item of input) {
|
|
14
|
+
const result = jwkSchema.try(item, options);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
keys.push(result.value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return v.ok(keys);
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** public JWKS (JSON Web Key Set with only public keys) */
|
|
27
|
+
export const jwksPubSchema = v.object({
|
|
28
|
+
keys: v.array(v.unknown()).chain((input, options) => {
|
|
29
|
+
const keys: JwkPub[] = [];
|
|
30
|
+
|
|
31
|
+
for (const item of input) {
|
|
32
|
+
const result = jwkPubSchema.try(item, options);
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
keys.push(result.value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return v.ok(keys);
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type Jwks = v.Infer<typeof jwksSchema>;
|
|
45
|
+
export type JwksPub = v.Infer<typeof jwksPubSchema>;
|