@atproto/oauth-types 0.7.2 → 0.7.4
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 +24 -0
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +11 -7
- package/src/atproto-loopback-client-id.ts +0 -78
- package/src/atproto-loopback-client-metadata.ts +0 -47
- package/src/atproto-loopback-client-redirect-uris.ts +0 -4
- package/src/atproto-oauth-scope.ts +0 -34
- package/src/atproto-oauth-token-response.ts +0 -16
- package/src/constants.ts +0 -2
- package/src/index.ts +0 -50
- package/src/oauth-access-token.ts +0 -4
- package/src/oauth-authorization-code-grant-token-request.ts +0 -19
- package/src/oauth-authorization-details.ts +0 -48
- package/src/oauth-authorization-request-jar.ts +0 -16
- package/src/oauth-authorization-request-par.ts +0 -12
- package/src/oauth-authorization-request-parameters.ts +0 -100
- package/src/oauth-authorization-request-query.ts +0 -21
- package/src/oauth-authorization-request-uri.ts +0 -10
- package/src/oauth-authorization-response-error.ts +0 -25
- package/src/oauth-authorization-server-metadata.ts +0 -120
- package/src/oauth-client-credentials-grant-token-request.ts +0 -9
- package/src/oauth-client-credentials.ts +0 -53
- package/src/oauth-client-id-discoverable.ts +0 -130
- package/src/oauth-client-id-loopback.ts +0 -164
- package/src/oauth-client-id.ts +0 -4
- package/src/oauth-client-metadata.ts +0 -80
- package/src/oauth-code-challenge-method.ts +0 -3
- package/src/oauth-endpoint-auth-method.ts +0 -13
- package/src/oauth-endpoint-name.ts +0 -8
- package/src/oauth-grant-type.ts +0 -13
- package/src/oauth-introspection-response.ts +0 -23
- package/src/oauth-issuer-identifier.ts +0 -47
- package/src/oauth-par-response.ts +0 -8
- package/src/oauth-password-grant-token-request.ts +0 -11
- package/src/oauth-prompt-mode.ts +0 -18
- package/src/oauth-protected-resource-metadata.ts +0 -94
- package/src/oauth-redirect-uri.ts +0 -73
- package/src/oauth-refresh-token-grant-token-request.ts +0 -11
- package/src/oauth-refresh-token.ts +0 -4
- package/src/oauth-request-uri.ts +0 -5
- package/src/oauth-response-mode.ts +0 -9
- package/src/oauth-response-type.ts +0 -17
- package/src/oauth-scope.ts +0 -21
- package/src/oauth-token-identification.ts +0 -12
- package/src/oauth-token-request.ts +0 -14
- package/src/oauth-token-response.ts +0 -29
- package/src/oauth-token-type.ts +0 -15
- package/src/oidc-authorization-error-response.ts +0 -29
- package/src/oidc-claims-parameter.ts +0 -40
- package/src/oidc-claims-properties.ts +0 -11
- package/src/oidc-entity-type.ts +0 -5
- package/src/oidc-userinfo.ts +0 -15
- package/src/uri.ts +0 -221
- package/src/util.ts +0 -170
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'
|
|
3
|
-
import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
|
|
4
|
-
import { oauthPromptModeSchema } from './oauth-prompt-mode.js'
|
|
5
|
-
import { webUriSchema } from './uri.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
|
|
9
|
-
* @note we do not enforce https: scheme in URIs to support development
|
|
10
|
-
* environments. Make sure to validate the URIs before using it in a production
|
|
11
|
-
* environment.
|
|
12
|
-
*/
|
|
13
|
-
export const oauthAuthorizationServerMetadataSchema = z.object({
|
|
14
|
-
issuer: oauthIssuerIdentifierSchema,
|
|
15
|
-
|
|
16
|
-
claims_supported: z.array(z.string()).optional(),
|
|
17
|
-
claims_locales_supported: z.array(z.string()).optional(),
|
|
18
|
-
claims_parameter_supported: z.boolean().optional(),
|
|
19
|
-
request_parameter_supported: z.boolean().optional(),
|
|
20
|
-
request_uri_parameter_supported: z.boolean().optional(),
|
|
21
|
-
require_request_uri_registration: z.boolean().optional(),
|
|
22
|
-
scopes_supported: z.array(z.string()).optional(),
|
|
23
|
-
subject_types_supported: z.array(z.string()).optional(),
|
|
24
|
-
response_types_supported: z.array(z.string()).optional(),
|
|
25
|
-
response_modes_supported: z.array(z.string()).optional(),
|
|
26
|
-
grant_types_supported: z.array(z.string()).optional(),
|
|
27
|
-
code_challenge_methods_supported: z
|
|
28
|
-
.array(oauthCodeChallengeMethodSchema)
|
|
29
|
-
.min(1)
|
|
30
|
-
.optional(),
|
|
31
|
-
ui_locales_supported: z.array(z.string()).optional(),
|
|
32
|
-
id_token_signing_alg_values_supported: z.array(z.string()).optional(),
|
|
33
|
-
display_values_supported: z.array(z.string()).optional(),
|
|
34
|
-
request_object_signing_alg_values_supported: z.array(z.string()).optional(),
|
|
35
|
-
authorization_response_iss_parameter_supported: z.boolean().optional(),
|
|
36
|
-
authorization_details_types_supported: z.array(z.string()).optional(),
|
|
37
|
-
request_object_encryption_alg_values_supported: z
|
|
38
|
-
.array(z.string())
|
|
39
|
-
.optional(),
|
|
40
|
-
request_object_encryption_enc_values_supported: z
|
|
41
|
-
.array(z.string())
|
|
42
|
-
.optional(),
|
|
43
|
-
|
|
44
|
-
jwks_uri: webUriSchema.optional(),
|
|
45
|
-
|
|
46
|
-
authorization_endpoint: webUriSchema, // .optional(),
|
|
47
|
-
|
|
48
|
-
token_endpoint: webUriSchema, // .optional(),
|
|
49
|
-
// https://www.rfc-editor.org/rfc/rfc8414.html#section-2
|
|
50
|
-
token_endpoint_auth_methods_supported: z
|
|
51
|
-
.array(z.string())
|
|
52
|
-
// > If omitted, the default is "client_secret_basic" [...].
|
|
53
|
-
.default(['client_secret_basic']),
|
|
54
|
-
token_endpoint_auth_signing_alg_values_supported: z
|
|
55
|
-
.array(z.string())
|
|
56
|
-
.optional(),
|
|
57
|
-
|
|
58
|
-
revocation_endpoint: webUriSchema.optional(),
|
|
59
|
-
introspection_endpoint: webUriSchema.optional(),
|
|
60
|
-
pushed_authorization_request_endpoint: webUriSchema.optional(),
|
|
61
|
-
|
|
62
|
-
require_pushed_authorization_requests: z.boolean().optional(),
|
|
63
|
-
|
|
64
|
-
userinfo_endpoint: webUriSchema.optional(),
|
|
65
|
-
end_session_endpoint: webUriSchema.optional(),
|
|
66
|
-
registration_endpoint: webUriSchema.optional(),
|
|
67
|
-
|
|
68
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#section-5.1
|
|
69
|
-
dpop_signing_alg_values_supported: z.array(z.string()).optional(),
|
|
70
|
-
|
|
71
|
-
// https://www.rfc-editor.org/rfc/rfc9728.html#section-4
|
|
72
|
-
protected_resources: z.array(webUriSchema).optional(),
|
|
73
|
-
|
|
74
|
-
// https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
|
|
75
|
-
client_id_metadata_document_supported: z.boolean().optional(),
|
|
76
|
-
|
|
77
|
-
// https://openid.net/specs/openid-connect-prompt-create-1_0.html#section-4.2
|
|
78
|
-
prompt_values_supported: z.array(oauthPromptModeSchema).optional(),
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
export type OAuthAuthorizationServerMetadata = z.infer<
|
|
82
|
-
typeof oauthAuthorizationServerMetadataSchema
|
|
83
|
-
>
|
|
84
|
-
|
|
85
|
-
export const oauthAuthorizationServerMetadataValidator =
|
|
86
|
-
oauthAuthorizationServerMetadataSchema
|
|
87
|
-
.superRefine((data, ctx) => {
|
|
88
|
-
if (
|
|
89
|
-
data.require_pushed_authorization_requests &&
|
|
90
|
-
!data.pushed_authorization_request_endpoint
|
|
91
|
-
) {
|
|
92
|
-
ctx.addIssue({
|
|
93
|
-
code: z.ZodIssueCode.custom,
|
|
94
|
-
message:
|
|
95
|
-
'"pushed_authorization_request_endpoint" required when "require_pushed_authorization_requests" is true',
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
.superRefine((data, ctx) => {
|
|
100
|
-
if (data.response_types_supported) {
|
|
101
|
-
if (!data.response_types_supported.includes('code')) {
|
|
102
|
-
ctx.addIssue({
|
|
103
|
-
code: z.ZodIssueCode.custom,
|
|
104
|
-
message: 'Response type "code" is required',
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
.superRefine((data, ctx) => {
|
|
110
|
-
if (
|
|
111
|
-
data.token_endpoint_auth_signing_alg_values_supported?.includes('none')
|
|
112
|
-
) {
|
|
113
|
-
// https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
|
|
114
|
-
// > The value `none` MUST NOT be used.
|
|
115
|
-
ctx.addIssue({
|
|
116
|
-
code: z.ZodIssueCode.custom,
|
|
117
|
-
message: 'Client authentication method "none" is not allowed',
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
})
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const oauthClientCredentialsGrantTokenRequestSchema = z.object({
|
|
4
|
-
grant_type: z.literal('client_credentials'),
|
|
5
|
-
})
|
|
6
|
-
|
|
7
|
-
export type OAuthClientCredentialsGrantTokenRequest = z.infer<
|
|
8
|
-
typeof oauthClientCredentialsGrantTokenRequestSchema
|
|
9
|
-
>
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { signedJwtSchema } from '@atproto/jwk'
|
|
3
|
-
import { CLIENT_ASSERTION_TYPE_JWT_BEARER } from './constants.js'
|
|
4
|
-
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
5
|
-
|
|
6
|
-
export const oauthClientCredentialsJwtBearerSchema = z.object({
|
|
7
|
-
client_id: oauthClientIdSchema,
|
|
8
|
-
client_assertion_type: z.literal(CLIENT_ASSERTION_TYPE_JWT_BEARER),
|
|
9
|
-
/**
|
|
10
|
-
* - "sub" the subject MUST be the "client_id" of the OAuth client
|
|
11
|
-
* - "iat" is required and MUST be less than one minute
|
|
12
|
-
* - "aud" must containing a value that identifies the authorization server
|
|
13
|
-
* - The JWT MAY contain a "jti" (JWT ID) claim that provides a unique identifier for the token.
|
|
14
|
-
* - Note that the authorization server may reject JWTs with an "exp" claim value that is unreasonably far in the future.
|
|
15
|
-
*
|
|
16
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}
|
|
17
|
-
*/
|
|
18
|
-
client_assertion: signedJwtSchema,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
export type OAuthClientCredentialsJwtBearer = z.infer<
|
|
22
|
-
typeof oauthClientCredentialsJwtBearerSchema
|
|
23
|
-
>
|
|
24
|
-
|
|
25
|
-
export const oauthClientCredentialsSecretPostSchema = z.object({
|
|
26
|
-
client_id: oauthClientIdSchema,
|
|
27
|
-
client_secret: z.string(),
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
export type OAuthClientCredentialsSecretPost = z.infer<
|
|
31
|
-
typeof oauthClientCredentialsSecretPostSchema
|
|
32
|
-
>
|
|
33
|
-
|
|
34
|
-
export const oauthClientCredentialsNoneSchema = z.object({
|
|
35
|
-
client_id: oauthClientIdSchema,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
export type OAuthClientCredentialsNone = z.infer<
|
|
39
|
-
typeof oauthClientCredentialsNoneSchema
|
|
40
|
-
>
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
export const oauthClientCredentialsSchema = z.union([
|
|
45
|
-
oauthClientCredentialsJwtBearerSchema,
|
|
46
|
-
oauthClientCredentialsSecretPostSchema,
|
|
47
|
-
// Must be last since it is less specific
|
|
48
|
-
oauthClientCredentialsNoneSchema,
|
|
49
|
-
])
|
|
50
|
-
|
|
51
|
-
export type OAuthClientCredentials = z.infer<
|
|
52
|
-
typeof oauthClientCredentialsSchema
|
|
53
|
-
>
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { TypeOf, z } from 'zod'
|
|
2
|
-
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
3
|
-
import { httpsUriSchema } from './uri.js'
|
|
4
|
-
import { extractUrlPath, isHostnameIP } from './util.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @see {@link https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html}
|
|
8
|
-
*/
|
|
9
|
-
export const oauthClientIdDiscoverableSchema = z
|
|
10
|
-
.intersection(oauthClientIdSchema, httpsUriSchema)
|
|
11
|
-
.superRefine((value, ctx): value is `https://${string}/${string}` => {
|
|
12
|
-
const url = new URL(value)
|
|
13
|
-
|
|
14
|
-
if (url.username || url.password) {
|
|
15
|
-
ctx.addIssue({
|
|
16
|
-
code: z.ZodIssueCode.custom,
|
|
17
|
-
message: 'ClientID must not contain credentials',
|
|
18
|
-
})
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (url.hash) {
|
|
23
|
-
ctx.addIssue({
|
|
24
|
-
code: z.ZodIssueCode.custom,
|
|
25
|
-
message: 'ClientID must not contain a fragment',
|
|
26
|
-
})
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (url.pathname === '/') {
|
|
31
|
-
ctx.addIssue({
|
|
32
|
-
code: z.ZodIssueCode.custom,
|
|
33
|
-
message:
|
|
34
|
-
'ClientID must contain a path component (e.g. "/client-metadata.json")',
|
|
35
|
-
})
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (url.pathname.endsWith('/')) {
|
|
40
|
-
ctx.addIssue({
|
|
41
|
-
code: z.ZodIssueCode.custom,
|
|
42
|
-
message: 'ClientID path must not end with a trailing slash',
|
|
43
|
-
})
|
|
44
|
-
return false
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (isHostnameIP(url.hostname)) {
|
|
48
|
-
ctx.addIssue({
|
|
49
|
-
code: z.ZodIssueCode.custom,
|
|
50
|
-
message: 'ClientID hostname must not be an IP address',
|
|
51
|
-
})
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// URL constructor normalizes the URL, so we extract the path manually to
|
|
56
|
-
// avoid normalization, then compare it to the normalized path to ensure
|
|
57
|
-
// that the URL does not contain path traversal or other unexpected characters
|
|
58
|
-
if (extractUrlPath(value) !== url.pathname) {
|
|
59
|
-
ctx.addIssue({
|
|
60
|
-
code: z.ZodIssueCode.custom,
|
|
61
|
-
message: `ClientID must be in canonical form ("${url.href}", got "${value}")`,
|
|
62
|
-
})
|
|
63
|
-
return false
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return true
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
export type OAuthClientIdDiscoverable = TypeOf<
|
|
70
|
-
typeof oauthClientIdDiscoverableSchema
|
|
71
|
-
>
|
|
72
|
-
|
|
73
|
-
export function isOAuthClientIdDiscoverable(
|
|
74
|
-
clientId: string,
|
|
75
|
-
): clientId is OAuthClientIdDiscoverable {
|
|
76
|
-
return oauthClientIdDiscoverableSchema.safeParse(clientId).success
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const conventionalOAuthClientIdSchema =
|
|
80
|
-
oauthClientIdDiscoverableSchema.superRefine(
|
|
81
|
-
(value, ctx): value is `https://${string}/oauth-client-metadata.json` => {
|
|
82
|
-
const url = new URL(value)
|
|
83
|
-
|
|
84
|
-
if (url.port) {
|
|
85
|
-
ctx.addIssue({
|
|
86
|
-
code: z.ZodIssueCode.custom,
|
|
87
|
-
message: 'ClientID must not contain a port',
|
|
88
|
-
})
|
|
89
|
-
return false
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (url.search) {
|
|
93
|
-
ctx.addIssue({
|
|
94
|
-
code: z.ZodIssueCode.custom,
|
|
95
|
-
message: 'ClientID must not contain a query string',
|
|
96
|
-
})
|
|
97
|
-
return false
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (url.pathname !== '/oauth-client-metadata.json') {
|
|
101
|
-
ctx.addIssue({
|
|
102
|
-
code: z.ZodIssueCode.custom,
|
|
103
|
-
message: 'ClientID must be "/oauth-client-metadata.json"',
|
|
104
|
-
})
|
|
105
|
-
return false
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return true
|
|
109
|
-
},
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
export type ConventionalOAuthClientId = TypeOf<
|
|
113
|
-
typeof conventionalOAuthClientIdSchema
|
|
114
|
-
>
|
|
115
|
-
|
|
116
|
-
export function isConventionalOAuthClientId(
|
|
117
|
-
clientId: string,
|
|
118
|
-
): clientId is ConventionalOAuthClientId {
|
|
119
|
-
return conventionalOAuthClientIdSchema.safeParse(clientId).success
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function assertOAuthDiscoverableClientId(
|
|
123
|
-
value: string,
|
|
124
|
-
): asserts value is OAuthClientIdDiscoverable {
|
|
125
|
-
void oauthClientIdDiscoverableSchema.parse(value)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function parseOAuthDiscoverableClientId(clientId: string): URL {
|
|
129
|
-
return new URL(oauthClientIdDiscoverableSchema.parse(clientId))
|
|
130
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
2
|
-
import {
|
|
3
|
-
OAuthLoopbackRedirectURI,
|
|
4
|
-
oauthLoopbackClientRedirectUriSchema,
|
|
5
|
-
} from './oauth-redirect-uri.js'
|
|
6
|
-
import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
|
|
7
|
-
|
|
8
|
-
export const LOOPBACK_CLIENT_ID_ORIGIN = 'http://localhost'
|
|
9
|
-
|
|
10
|
-
// @NOTE This is not actually based on a standard, but rather a convention
|
|
11
|
-
// established by Bluesky in the Atproto specs and implementation. As such, and
|
|
12
|
-
// in order to respect the convention from this package, these should be
|
|
13
|
-
// prefixed with "Atproto" instead of "OAuth". For legacy reasons, we keep the
|
|
14
|
-
// current names, but we should rename them in a future major release, unless
|
|
15
|
-
// loopback client ids have since then been standardized.
|
|
16
|
-
|
|
17
|
-
export type OAuthClientIdLoopback =
|
|
18
|
-
`http://localhost${'' | `/`}${'' | `?${string}`}`
|
|
19
|
-
|
|
20
|
-
export type OAuthLoopbackClientIdParams = {
|
|
21
|
-
scope?: OAuthScope
|
|
22
|
-
redirect_uris?: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const oauthClientIdLoopbackSchema = oauthClientIdSchema.superRefine(
|
|
26
|
-
(input, ctx): input is OAuthClientIdLoopback => {
|
|
27
|
-
const result = safeParseOAuthLoopbackClientId(input)
|
|
28
|
-
if (!result.success) {
|
|
29
|
-
ctx.addIssue({ code: 'custom', message: result.message })
|
|
30
|
-
}
|
|
31
|
-
return result.success
|
|
32
|
-
},
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
export function assertOAuthLoopbackClientId(
|
|
36
|
-
input: string,
|
|
37
|
-
): asserts input is OAuthClientIdLoopback {
|
|
38
|
-
void parseOAuthLoopbackClientId(input)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function isOAuthClientIdLoopback<T extends string>(
|
|
42
|
-
input: T,
|
|
43
|
-
): input is T & OAuthClientIdLoopback {
|
|
44
|
-
return safeParseOAuthLoopbackClientId(input).success
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function asOAuthClientIdLoopback<T extends string>(input: T) {
|
|
48
|
-
assertOAuthLoopbackClientId(input)
|
|
49
|
-
return input
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function parseOAuthLoopbackClientId(
|
|
53
|
-
input: string,
|
|
54
|
-
): OAuthLoopbackClientIdParams {
|
|
55
|
-
const result = safeParseOAuthLoopbackClientId(input)
|
|
56
|
-
if (result.success) return result.value
|
|
57
|
-
|
|
58
|
-
throw new TypeError(`Invalid loopback client ID: ${result.message}`)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Similar to Zod's {@link SafeParseReturnType} but uses a simple "message"
|
|
63
|
-
* string instead of an "error" Error object.
|
|
64
|
-
*/
|
|
65
|
-
type LightParseReturnType<T> =
|
|
66
|
-
| { success: true; value: T }
|
|
67
|
-
| { success: false; message: string }
|
|
68
|
-
|
|
69
|
-
export function safeParseOAuthLoopbackClientId(
|
|
70
|
-
input: string,
|
|
71
|
-
): LightParseReturnType<OAuthLoopbackClientIdParams> {
|
|
72
|
-
// @NOTE Not using "new URL" to ensure input indeed matches the type
|
|
73
|
-
// OAuthClientIdLoopback
|
|
74
|
-
|
|
75
|
-
if (!input.startsWith(LOOPBACK_CLIENT_ID_ORIGIN)) {
|
|
76
|
-
return {
|
|
77
|
-
success: false,
|
|
78
|
-
message: `Value must start with "${LOOPBACK_CLIENT_ID_ORIGIN}"`,
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (input.includes('#', LOOPBACK_CLIENT_ID_ORIGIN.length)) {
|
|
83
|
-
return {
|
|
84
|
-
success: false,
|
|
85
|
-
message: 'Value must not contain a hash component',
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Since we don't allow a path component (except for a single "/") the query
|
|
90
|
-
// string starts after the origin (+ 1 if there is a "/")
|
|
91
|
-
const queryStringIdx =
|
|
92
|
-
input.length > LOOPBACK_CLIENT_ID_ORIGIN.length &&
|
|
93
|
-
input.charCodeAt(LOOPBACK_CLIENT_ID_ORIGIN.length) === 0x2f /* '/' */
|
|
94
|
-
? LOOPBACK_CLIENT_ID_ORIGIN.length + 1
|
|
95
|
-
: LOOPBACK_CLIENT_ID_ORIGIN.length
|
|
96
|
-
|
|
97
|
-
// Since we determined the position of the query string based on the origin
|
|
98
|
-
// length (instead of looking for a "?"), we need to make sure the query
|
|
99
|
-
// string position (if any) indeed starts with a "?".
|
|
100
|
-
if (
|
|
101
|
-
input.length !== queryStringIdx &&
|
|
102
|
-
input.charCodeAt(queryStringIdx) !== 0x3f /* '?' */
|
|
103
|
-
) {
|
|
104
|
-
return {
|
|
105
|
-
success: false,
|
|
106
|
-
message: 'Value must not contain a path component',
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const queryString = input.slice(queryStringIdx + 1)
|
|
111
|
-
return safeParseOAuthLoopbackClientIdQueryString(queryString)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function safeParseOAuthLoopbackClientIdQueryString(
|
|
115
|
-
input: string | Iterable<[key: string, value: string]>,
|
|
116
|
-
): LightParseReturnType<OAuthLoopbackClientIdParams> {
|
|
117
|
-
// Parse query params
|
|
118
|
-
const params: OAuthLoopbackClientIdParams = {}
|
|
119
|
-
|
|
120
|
-
const it = typeof input === 'string' ? new URLSearchParams(input) : input
|
|
121
|
-
for (const [key, value] of it) {
|
|
122
|
-
if (key === 'scope') {
|
|
123
|
-
if ('scope' in params) {
|
|
124
|
-
return {
|
|
125
|
-
success: false,
|
|
126
|
-
message: 'Duplicate "scope" query parameter',
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const res = oauthScopeSchema.safeParse(value)
|
|
131
|
-
if (!res.success) {
|
|
132
|
-
const reason = res.error.issues.map((i) => i.message).join(', ')
|
|
133
|
-
return {
|
|
134
|
-
success: false,
|
|
135
|
-
message: `Invalid "scope" query parameter: ${reason || 'Validation failed'}`,
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
params.scope = res.data
|
|
140
|
-
} else if (key === 'redirect_uri') {
|
|
141
|
-
const res = oauthLoopbackClientRedirectUriSchema.safeParse(value)
|
|
142
|
-
if (!res.success) {
|
|
143
|
-
const reason = res.error.issues.map((i) => i.message).join(', ')
|
|
144
|
-
return {
|
|
145
|
-
success: false,
|
|
146
|
-
message: `Invalid "redirect_uri" query parameter: ${reason || 'Validation failed'}`,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (params.redirect_uris == null) params.redirect_uris = [res.data]
|
|
151
|
-
else params.redirect_uris.push(res.data)
|
|
152
|
-
} else {
|
|
153
|
-
return {
|
|
154
|
-
success: false,
|
|
155
|
-
message: `Unexpected query parameter "${key}"`,
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
success: true,
|
|
162
|
-
value: params,
|
|
163
|
-
}
|
|
164
|
-
}
|
package/src/oauth-client-id.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { jwksPubSchema } from '@atproto/jwk'
|
|
3
|
-
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
4
|
-
import { oauthEndpointAuthMethod } from './oauth-endpoint-auth-method.js'
|
|
5
|
-
import { oauthGrantTypeSchema } from './oauth-grant-type.js'
|
|
6
|
-
import { oauthRedirectUriSchema } from './oauth-redirect-uri.js'
|
|
7
|
-
import { oauthResponseTypeSchema } from './oauth-response-type.js'
|
|
8
|
-
import { oauthScopeSchema } from './oauth-scope.js'
|
|
9
|
-
import { webUriSchema } from './uri.js'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @see {@link https://openid.net/specs/openid-connect-registration-1_0.html}
|
|
13
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591}
|
|
14
|
-
* @note we do not enforce https: scheme in URIs to support development
|
|
15
|
-
* environments. Make sure to validate the URIs before using it in a production
|
|
16
|
-
* environment.
|
|
17
|
-
*/
|
|
18
|
-
export const oauthClientMetadataSchema = z.object({
|
|
19
|
-
/**
|
|
20
|
-
* @note redirect_uris require additional validation
|
|
21
|
-
*/
|
|
22
|
-
// https://www.rfc-editor.org/rfc/rfc7591.html#section-2
|
|
23
|
-
redirect_uris: z.array(oauthRedirectUriSchema).nonempty(),
|
|
24
|
-
response_types: z
|
|
25
|
-
.array(oauthResponseTypeSchema)
|
|
26
|
-
.nonempty()
|
|
27
|
-
// > If omitted, the default is that the client will use only the "code"
|
|
28
|
-
// > response type.
|
|
29
|
-
.default(['code']),
|
|
30
|
-
grant_types: z
|
|
31
|
-
.array(oauthGrantTypeSchema)
|
|
32
|
-
.nonempty()
|
|
33
|
-
// > If omitted, the default behavior is that the client will use only the
|
|
34
|
-
// > "authorization_code" Grant Type.
|
|
35
|
-
.default(['authorization_code']),
|
|
36
|
-
scope: oauthScopeSchema.optional(),
|
|
37
|
-
// https://www.rfc-editor.org/rfc/rfc7591.html#section-2
|
|
38
|
-
token_endpoint_auth_method: oauthEndpointAuthMethod
|
|
39
|
-
// > If unspecified or omitted, the default is "client_secret_basic" [...].
|
|
40
|
-
.default('client_secret_basic'),
|
|
41
|
-
token_endpoint_auth_signing_alg: z.string().optional(),
|
|
42
|
-
userinfo_signed_response_alg: z.string().optional(),
|
|
43
|
-
userinfo_encrypted_response_alg: z.string().optional(),
|
|
44
|
-
jwks_uri: webUriSchema.optional(),
|
|
45
|
-
jwks: jwksPubSchema.optional(),
|
|
46
|
-
application_type: z.enum(['web', 'native']).default('web'), // default, per spec, is "web"
|
|
47
|
-
subject_type: z.enum(['public', 'pairwise']).default('public'),
|
|
48
|
-
request_object_signing_alg: z.string().optional(),
|
|
49
|
-
id_token_signed_response_alg: z.string().optional(),
|
|
50
|
-
authorization_signed_response_alg: z.string().default('RS256'),
|
|
51
|
-
authorization_encrypted_response_enc: z.enum(['A128CBC-HS256']).optional(),
|
|
52
|
-
authorization_encrypted_response_alg: z.string().optional(),
|
|
53
|
-
client_id: oauthClientIdSchema.optional(),
|
|
54
|
-
client_name: z.string().optional(),
|
|
55
|
-
client_uri: webUriSchema.optional(),
|
|
56
|
-
policy_uri: webUriSchema.optional(),
|
|
57
|
-
tos_uri: webUriSchema.optional(),
|
|
58
|
-
logo_uri: webUriSchema.optional(), // @TODO: allow data: uri ?
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Default Maximum Authentication Age. Specifies that the End-User MUST be
|
|
62
|
-
* actively authenticated if the End-User was authenticated longer ago than
|
|
63
|
-
* the specified number of seconds. The max_age request parameter overrides
|
|
64
|
-
* this default value. If omitted, no default Maximum Authentication Age is
|
|
65
|
-
* specified.
|
|
66
|
-
*/
|
|
67
|
-
default_max_age: z.number().optional(),
|
|
68
|
-
require_auth_time: z.boolean().optional(),
|
|
69
|
-
contacts: z.array(z.string().email()).optional(),
|
|
70
|
-
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
|
|
71
|
-
|
|
72
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#section-5.2
|
|
73
|
-
dpop_bound_access_tokens: z.boolean().optional(),
|
|
74
|
-
|
|
75
|
-
// https://datatracker.ietf.org/doc/html/rfc9396#section-14.5
|
|
76
|
-
authorization_details_types: z.array(z.string()).optional(),
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
export type OAuthClientMetadata = z.infer<typeof oauthClientMetadataSchema>
|
|
80
|
-
export type OAuthClientMetadataInput = z.input<typeof oauthClientMetadataSchema>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const oauthEndpointAuthMethod = z.enum([
|
|
4
|
-
'client_secret_basic',
|
|
5
|
-
'client_secret_jwt',
|
|
6
|
-
'client_secret_post',
|
|
7
|
-
'none',
|
|
8
|
-
'private_key_jwt',
|
|
9
|
-
'self_signed_tls_client_auth',
|
|
10
|
-
'tls_client_auth',
|
|
11
|
-
])
|
|
12
|
-
|
|
13
|
-
export type OauthEndpointAuthMethod = z.infer<typeof oauthEndpointAuthMethod>
|
package/src/oauth-grant-type.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
export const oauthGrantTypeSchema = z.enum([
|
|
4
|
-
'authorization_code',
|
|
5
|
-
'implicit',
|
|
6
|
-
'refresh_token',
|
|
7
|
-
'password', // Not part of OAuth 2.1
|
|
8
|
-
'client_credentials',
|
|
9
|
-
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
10
|
-
'urn:ietf:params:oauth:grant-type:saml2-bearer',
|
|
11
|
-
])
|
|
12
|
-
|
|
13
|
-
export type OAuthGrantType = z.infer<typeof oauthGrantTypeSchema>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { OAuthAuthorizationDetails } from './oauth-authorization-details.js'
|
|
2
|
-
import { OAuthTokenType } from './oauth-token-type.js'
|
|
3
|
-
|
|
4
|
-
// https://datatracker.ietf.org/doc/html/rfc7662#section-2.2
|
|
5
|
-
export type OAuthIntrospectionResponse =
|
|
6
|
-
| { active: false }
|
|
7
|
-
| {
|
|
8
|
-
active: true
|
|
9
|
-
|
|
10
|
-
scope?: string
|
|
11
|
-
client_id?: string
|
|
12
|
-
username?: string
|
|
13
|
-
token_type?: OAuthTokenType
|
|
14
|
-
authorization_details?: OAuthAuthorizationDetails
|
|
15
|
-
|
|
16
|
-
aud?: string | [string, ...string[]]
|
|
17
|
-
exp?: number
|
|
18
|
-
iat?: number
|
|
19
|
-
iss?: string
|
|
20
|
-
jti?: string
|
|
21
|
-
nbf?: number
|
|
22
|
-
sub?: string
|
|
23
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { webUriSchema } from './uri.js'
|
|
3
|
-
|
|
4
|
-
export const oauthIssuerIdentifierSchema = webUriSchema.superRefine(
|
|
5
|
-
(value, ctx) => {
|
|
6
|
-
// Validate the issuer (MIX-UP attacks)
|
|
7
|
-
|
|
8
|
-
if (value.endsWith('/')) {
|
|
9
|
-
ctx.addIssue({
|
|
10
|
-
code: z.ZodIssueCode.custom,
|
|
11
|
-
message: 'Issuer URL must not end with a slash',
|
|
12
|
-
})
|
|
13
|
-
return false
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const url = new URL(value)
|
|
17
|
-
|
|
18
|
-
if (url.username || url.password) {
|
|
19
|
-
ctx.addIssue({
|
|
20
|
-
code: z.ZodIssueCode.custom,
|
|
21
|
-
message: 'Issuer URL must not contain a username or password',
|
|
22
|
-
})
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (url.hash || url.search) {
|
|
27
|
-
ctx.addIssue({
|
|
28
|
-
code: z.ZodIssueCode.custom,
|
|
29
|
-
message: 'Issuer URL must not contain a query or fragment',
|
|
30
|
-
})
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const canonicalValue = url.pathname === '/' ? url.origin : url.href
|
|
35
|
-
if (value !== canonicalValue) {
|
|
36
|
-
ctx.addIssue({
|
|
37
|
-
code: z.ZodIssueCode.custom,
|
|
38
|
-
message: 'Issuer URL must be in the canonical form',
|
|
39
|
-
})
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return true
|
|
44
|
-
},
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
export type OAuthIssuerIdentifier = z.infer<typeof oauthIssuerIdentifierSchema>
|