@atproto/oauth-types 0.1.5 → 0.2.1
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 +28 -0
- package/dist/atproto-loopback-client-metadata.d.ts +4 -1
- package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
- package/dist/atproto-loopback-client-metadata.js +1 -2
- package/dist/atproto-loopback-client-metadata.js.map +1 -1
- package/dist/constants.d.ts +0 -6
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -17
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth-authorization-code-grant-token-request.d.ts +2 -2
- package/dist/oauth-authorization-code-grant-token-request.d.ts.map +1 -1
- package/dist/oauth-authorization-code-grant-token-request.js +2 -1
- package/dist/oauth-authorization-code-grant-token-request.js.map +1 -1
- package/dist/oauth-authorization-details.d.ts +42 -4
- package/dist/oauth-authorization-details.d.ts.map +1 -1
- package/dist/oauth-authorization-details.js +21 -1
- package/dist/oauth-authorization-details.js.map +1 -1
- package/dist/oauth-authorization-request-jar.d.ts +1 -1
- package/dist/oauth-authorization-request-par.d.ts +11 -11
- package/dist/oauth-authorization-request-parameters.d.ts +10 -10
- package/dist/oauth-authorization-request-parameters.d.ts.map +1 -1
- package/dist/oauth-authorization-request-parameters.js +3 -2
- package/dist/oauth-authorization-request-parameters.js.map +1 -1
- package/dist/oauth-authorization-request-query.d.ts +11 -11
- package/dist/oauth-authorization-server-metadata.d.ts +69 -66
- package/dist/oauth-authorization-server-metadata.d.ts.map +1 -1
- package/dist/oauth-authorization-server-metadata.js +14 -10
- package/dist/oauth-authorization-server-metadata.js.map +1 -1
- package/dist/oauth-client-id-discoverable.d.ts +3 -2
- package/dist/oauth-client-id-discoverable.d.ts.map +1 -1
- package/dist/oauth-client-id-discoverable.js +54 -31
- package/dist/oauth-client-id-discoverable.js.map +1 -1
- package/dist/oauth-client-id-loopback.d.ts +5 -5
- package/dist/oauth-client-id-loopback.d.ts.map +1 -1
- package/dist/oauth-client-id-loopback.js +32 -31
- package/dist/oauth-client-id-loopback.js.map +1 -1
- package/dist/oauth-client-metadata.d.ts +112 -102
- package/dist/oauth-client-metadata.d.ts.map +1 -1
- package/dist/oauth-client-metadata.js +18 -8
- package/dist/oauth-client-metadata.js.map +1 -1
- package/dist/oauth-issuer-identifier.d.ts +2 -1
- package/dist/oauth-issuer-identifier.d.ts.map +1 -1
- package/dist/oauth-issuer-identifier.js +8 -23
- package/dist/oauth-issuer-identifier.js.map +1 -1
- package/dist/oauth-protected-resource-metadata.d.ts +15 -12
- package/dist/oauth-protected-resource-metadata.d.ts.map +1 -1
- package/dist/oauth-protected-resource-metadata.js +15 -5
- package/dist/oauth-protected-resource-metadata.js.map +1 -1
- package/dist/oauth-redirect-uri.d.ts +10 -0
- package/dist/oauth-redirect-uri.d.ts.map +1 -0
- package/dist/oauth-redirect-uri.js +35 -0
- package/dist/oauth-redirect-uri.js.map +1 -0
- package/dist/oauth-refresh-token-grant-token-request.d.ts +0 -3
- package/dist/oauth-refresh-token-grant-token-request.d.ts.map +1 -1
- package/dist/oauth-refresh-token-grant-token-request.js +0 -2
- package/dist/oauth-refresh-token-grant-token-request.js.map +1 -1
- package/dist/oauth-token-request.d.ts +2 -5
- package/dist/oauth-token-request.d.ts.map +1 -1
- package/dist/oauth-token-response.d.ts +9 -12
- package/dist/oauth-token-response.d.ts.map +1 -1
- package/dist/oauth-token-response.js +4 -2
- package/dist/oauth-token-response.js.map +1 -1
- package/dist/uri.d.ts +20 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +127 -0
- package/dist/uri.js.map +1 -0
- package/dist/util.js +5 -6
- package/dist/util.js.map +1 -1
- package/package.json +2 -2
- package/src/atproto-loopback-client-metadata.ts +8 -3
- package/src/constants.ts +0 -16
- package/src/index.ts +2 -0
- package/src/oauth-authorization-code-grant-token-request.ts +2 -1
- package/src/oauth-authorization-details.ts +21 -1
- package/src/oauth-authorization-request-parameters.ts +3 -2
- package/src/oauth-authorization-server-metadata.ts +14 -10
- package/src/oauth-client-id-discoverable.ts +69 -51
- package/src/oauth-client-id-loopback.ts +40 -40
- package/src/oauth-client-metadata.ts +18 -8
- package/src/oauth-issuer-identifier.ts +14 -24
- package/src/oauth-protected-resource-metadata.ts +15 -5
- package/src/oauth-redirect-uri.ts +56 -0
- package/src/oauth-refresh-token-grant-token-request.ts +0 -2
- package/src/oauth-token-response.ts +4 -2
- package/src/uri.ts +171 -0
- package/tsconfig.build.tsbuildinfo +1 -0
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TypeOf, ZodIssueCode } from 'zod'
|
|
2
|
+
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
3
|
+
import {
|
|
4
|
+
OAuthLoopbackRedirectURI,
|
|
5
|
+
oauthLoopbackRedirectURISchema,
|
|
6
|
+
OAuthRedirectUri,
|
|
7
|
+
} from './oauth-redirect-uri.js'
|
|
2
8
|
import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
|
|
3
|
-
import { isLoopbackHost, safeUrl } from './util.js'
|
|
4
9
|
|
|
5
|
-
const
|
|
10
|
+
const PREFIX = 'http://localhost'
|
|
6
11
|
|
|
7
|
-
export
|
|
8
|
-
`${typeof
|
|
12
|
+
export const oauthClientIdLoopbackSchema = oauthClientIdSchema.superRefine(
|
|
13
|
+
(value, ctx): value is `${typeof PREFIX}${'' | '/'}${'' | `?${string}`}` => {
|
|
14
|
+
try {
|
|
15
|
+
assertOAuthLoopbackClientId(value)
|
|
16
|
+
return true
|
|
17
|
+
} catch (error) {
|
|
18
|
+
ctx.addIssue({
|
|
19
|
+
code: ZodIssueCode.custom,
|
|
20
|
+
message:
|
|
21
|
+
error instanceof TypeError
|
|
22
|
+
? error.message
|
|
23
|
+
: 'Invalid loopback client ID',
|
|
24
|
+
})
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export type OAuthClientIdLoopback = TypeOf<typeof oauthClientIdLoopbackSchema>
|
|
9
31
|
|
|
10
32
|
export function isOAuthClientIdLoopback(
|
|
11
33
|
clientId: string,
|
|
@@ -28,21 +50,18 @@ export function assertOAuthLoopbackClientId(
|
|
|
28
50
|
// validation functions)
|
|
29
51
|
export function parseOAuthLoopbackClientId(clientId: string): {
|
|
30
52
|
scope?: OAuthScope
|
|
31
|
-
redirect_uris?: [
|
|
53
|
+
redirect_uris?: [OAuthRedirectUri, ...OAuthRedirectUri[]]
|
|
32
54
|
} {
|
|
33
|
-
if (!clientId.startsWith(
|
|
34
|
-
throw new TypeError(
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
} else if (clientId.includes('#', OAUTH_CLIENT_ID_LOOPBACK_URL.length)) {
|
|
55
|
+
if (!clientId.startsWith(PREFIX)) {
|
|
56
|
+
throw new TypeError(`Loopback ClientID must start with "${PREFIX}"`)
|
|
57
|
+
} else if (clientId.includes('#', PREFIX.length)) {
|
|
38
58
|
throw new TypeError('Loopback ClientID must not contain a hash component')
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
const queryStringIdx =
|
|
42
|
-
clientId.length >
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
: OAUTH_CLIENT_ID_LOOPBACK_URL.length
|
|
62
|
+
clientId.length > PREFIX.length && clientId[PREFIX.length] === '/'
|
|
63
|
+
? PREFIX.length + 1
|
|
64
|
+
: PREFIX.length
|
|
46
65
|
|
|
47
66
|
if (clientId.length === queryStringIdx) {
|
|
48
67
|
return {} // no query string to parse
|
|
@@ -72,33 +91,14 @@ export function parseOAuthLoopbackClientId(clientId: string): {
|
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
const redirect_uris = searchParams.has('redirect_uri')
|
|
75
|
-
? (searchParams
|
|
94
|
+
? (searchParams
|
|
95
|
+
.getAll('redirect_uri')
|
|
96
|
+
.map((value) => oauthLoopbackRedirectURISchema.parse(value)) as [
|
|
97
|
+
OAuthLoopbackRedirectURI,
|
|
98
|
+
...OAuthLoopbackRedirectURI[],
|
|
99
|
+
])
|
|
76
100
|
: undefined
|
|
77
101
|
|
|
78
|
-
if (redirect_uris) {
|
|
79
|
-
for (const uri of redirect_uris) {
|
|
80
|
-
const url = safeUrl(uri)
|
|
81
|
-
if (!url) {
|
|
82
|
-
throw new TypeError(`Invalid redirect_uri in client ID: ${uri}`)
|
|
83
|
-
}
|
|
84
|
-
if (url.protocol !== 'http:') {
|
|
85
|
-
throw new TypeError(
|
|
86
|
-
`Loopback ClientID must use "http:" redirect_uri's (got ${uri})`,
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
if (url.hostname === 'localhost') {
|
|
90
|
-
throw new TypeError(
|
|
91
|
-
`Loopback ClientID must not use "localhost" as redirect_uri hostname (got ${uri})`,
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
if (!isLoopbackHost(url.hostname)) {
|
|
95
|
-
throw new TypeError(
|
|
96
|
-
`Loopback ClientID must use loopback addresses as redirect_uri's (got ${uri})`,
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
102
|
return {
|
|
103
103
|
scope,
|
|
104
104
|
redirect_uris,
|
|
@@ -4,13 +4,23 @@ import { z } from 'zod'
|
|
|
4
4
|
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
5
5
|
import { oauthEndpointAuthMethod } from './oauth-endpoint-auth-method.js'
|
|
6
6
|
import { oauthGrantTypeSchema } from './oauth-grant-type.js'
|
|
7
|
+
import { oauthRedirectUriSchema } from './oauth-redirect-uri.js'
|
|
7
8
|
import { oauthResponseTypeSchema } from './oauth-response-type.js'
|
|
8
9
|
import { oauthScopeSchema } from './oauth-scope.js'
|
|
10
|
+
import { webUriSchema } from './uri.js'
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @see {@link https://openid.net/specs/openid-connect-registration-1_0.html}
|
|
14
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591}
|
|
15
|
+
* @note we do not enforce https: scheme in URIs to support development
|
|
16
|
+
* environments. Make sure to validate the URIs before using it in a production
|
|
17
|
+
* environment.
|
|
18
|
+
*/
|
|
12
19
|
export const oauthClientMetadataSchema = z.object({
|
|
13
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @note redirect_uris require additional validation
|
|
22
|
+
*/
|
|
23
|
+
redirect_uris: z.array(oauthRedirectUriSchema).nonempty(),
|
|
14
24
|
response_types: z
|
|
15
25
|
.array(oauthResponseTypeSchema)
|
|
16
26
|
.nonempty()
|
|
@@ -30,7 +40,7 @@ export const oauthClientMetadataSchema = z.object({
|
|
|
30
40
|
token_endpoint_auth_signing_alg: z.string().optional(),
|
|
31
41
|
userinfo_signed_response_alg: z.string().optional(),
|
|
32
42
|
userinfo_encrypted_response_alg: z.string().optional(),
|
|
33
|
-
jwks_uri:
|
|
43
|
+
jwks_uri: webUriSchema.optional(),
|
|
34
44
|
jwks: jwksPubSchema.optional(),
|
|
35
45
|
application_type: z.enum(['web', 'native']).default('web').optional(), // default, per spec, is "web"
|
|
36
46
|
subject_type: z.enum(['public', 'pairwise']).default('public').optional(),
|
|
@@ -41,10 +51,10 @@ export const oauthClientMetadataSchema = z.object({
|
|
|
41
51
|
authorization_encrypted_response_alg: z.string().optional(),
|
|
42
52
|
client_id: oauthClientIdSchema.optional(),
|
|
43
53
|
client_name: z.string().optional(),
|
|
44
|
-
client_uri:
|
|
45
|
-
policy_uri:
|
|
46
|
-
tos_uri:
|
|
47
|
-
logo_uri:
|
|
54
|
+
client_uri: webUriSchema.optional(),
|
|
55
|
+
policy_uri: webUriSchema.optional(),
|
|
56
|
+
tos_uri: webUriSchema.optional(),
|
|
57
|
+
logo_uri: webUriSchema.optional(), // TODO: allow data: uri ?
|
|
48
58
|
|
|
49
59
|
/**
|
|
50
60
|
* Default Maximum Authentication Age. Specifies that the End-User MUST be
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import { safeUrl } from './util.js'
|
|
2
|
+
import { webUriSchema } from './uri.js'
|
|
4
3
|
|
|
5
|
-
export const oauthIssuerIdentifierSchema =
|
|
6
|
-
|
|
7
|
-
.superRefine((value, ctx) => {
|
|
4
|
+
export const oauthIssuerIdentifierSchema = webUriSchema.superRefine(
|
|
5
|
+
(value, ctx) => {
|
|
8
6
|
// Validate the issuer (MIX-UP attacks)
|
|
9
7
|
|
|
10
8
|
if (value.endsWith('/')) {
|
|
@@ -12,32 +10,17 @@ export const oauthIssuerIdentifierSchema = z
|
|
|
12
10
|
code: z.ZodIssueCode.custom,
|
|
13
11
|
message: 'Issuer URL must not end with a slash',
|
|
14
12
|
})
|
|
13
|
+
return false
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
const url =
|
|
18
|
-
if (!url) {
|
|
19
|
-
return ctx.addIssue({
|
|
20
|
-
code: z.ZodIssueCode.custom,
|
|
21
|
-
message: 'Invalid url',
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (url.protocol !== 'https:') {
|
|
26
|
-
if (ALLOW_UNSECURE_ORIGINS && url.protocol === 'http:') {
|
|
27
|
-
// We'll allow HTTP in development mode
|
|
28
|
-
} else {
|
|
29
|
-
ctx.addIssue({
|
|
30
|
-
code: z.ZodIssueCode.custom,
|
|
31
|
-
message: 'Issuer must be an HTTPS URL',
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
}
|
|
16
|
+
const url = new URL(value)
|
|
35
17
|
|
|
36
18
|
if (url.username || url.password) {
|
|
37
19
|
ctx.addIssue({
|
|
38
20
|
code: z.ZodIssueCode.custom,
|
|
39
21
|
message: 'Issuer URL must not contain a username or password',
|
|
40
22
|
})
|
|
23
|
+
return false
|
|
41
24
|
}
|
|
42
25
|
|
|
43
26
|
if (url.hash || url.search) {
|
|
@@ -45,6 +28,7 @@ export const oauthIssuerIdentifierSchema = z
|
|
|
45
28
|
code: z.ZodIssueCode.custom,
|
|
46
29
|
message: 'Issuer URL must not contain a query or fragment',
|
|
47
30
|
})
|
|
31
|
+
return false
|
|
48
32
|
}
|
|
49
33
|
|
|
50
34
|
const canonicalValue = url.pathname === '/' ? url.origin : url.href
|
|
@@ -53,5 +37,11 @@ export const oauthIssuerIdentifierSchema = z
|
|
|
53
37
|
code: z.ZodIssueCode.custom,
|
|
54
38
|
message: 'Issuer URL must be in the canonical form',
|
|
55
39
|
})
|
|
40
|
+
return false
|
|
56
41
|
}
|
|
57
|
-
|
|
42
|
+
|
|
43
|
+
return true
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export type OAuthIssuerIdentifier = z.infer<typeof oauthIssuerIdentifierSchema>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
3
|
import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
|
|
4
|
+
import { webUriSchema } from './uri.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#name-protected-resource-metadata-r}
|
|
@@ -10,8 +11,17 @@ export const oauthProtectedResourceMetadataSchema = z.object({
|
|
|
10
11
|
* REQUIRED. The protected resource's resource identifier, which is a URL that
|
|
11
12
|
* uses the https scheme and has no query or fragment components. Using these
|
|
12
13
|
* well-known resources is described in Section 3.
|
|
14
|
+
*
|
|
15
|
+
* @note This schema allows non https URLs for testing & development purposes.
|
|
16
|
+
* Make sure to validate the URL before using it in a production environment.
|
|
13
17
|
*/
|
|
14
|
-
resource:
|
|
18
|
+
resource: webUriSchema
|
|
19
|
+
.refine((url) => !url.includes('?'), {
|
|
20
|
+
message: 'Resource URL must not contain query parameters',
|
|
21
|
+
})
|
|
22
|
+
.refine((url) => !url.includes('#'), {
|
|
23
|
+
message: 'Resource URL must not contain a fragment',
|
|
24
|
+
}),
|
|
15
25
|
|
|
16
26
|
/**
|
|
17
27
|
* OPTIONAL. JSON array containing a list of OAuth authorization server issuer
|
|
@@ -31,7 +41,7 @@ export const oauthProtectedResourceMetadataSchema = z.object({
|
|
|
31
41
|
* available, a use (public key use) parameter value is REQUIRED for all keys
|
|
32
42
|
* in the referenced JWK Set to indicate each key's intended usage.
|
|
33
43
|
*/
|
|
34
|
-
jwks_uri:
|
|
44
|
+
jwks_uri: webUriSchema.optional(),
|
|
35
45
|
|
|
36
46
|
/**
|
|
37
47
|
* RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope
|
|
@@ -64,20 +74,20 @@ export const oauthProtectedResourceMetadataSchema = z.object({
|
|
|
64
74
|
* OPTIONAL. URL of a page containing human-readable information that
|
|
65
75
|
* developers might want or need to know when using the protected resource
|
|
66
76
|
*/
|
|
67
|
-
resource_documentation:
|
|
77
|
+
resource_documentation: webUriSchema.optional(),
|
|
68
78
|
|
|
69
79
|
/**
|
|
70
80
|
* OPTIONAL. URL that the protected resource provides to read about the
|
|
71
81
|
* protected resource's requirements on how the client can use the data
|
|
72
82
|
* provided by the protected resource
|
|
73
83
|
*/
|
|
74
|
-
resource_policy_uri:
|
|
84
|
+
resource_policy_uri: webUriSchema.optional(),
|
|
75
85
|
|
|
76
86
|
/**
|
|
77
87
|
* OPTIONAL. URL that the protected resource provides to read about the
|
|
78
88
|
* protected resource's terms of service
|
|
79
89
|
*/
|
|
80
|
-
resource_tos_uri:
|
|
90
|
+
resource_tos_uri: webUriSchema.optional(),
|
|
81
91
|
})
|
|
82
92
|
|
|
83
93
|
export type OAuthProtectedResourceMetadata = z.infer<
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TypeOf, z, ZodIssueCode } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
httpsUriSchema,
|
|
4
|
+
LoopbackUri,
|
|
5
|
+
loopbackUriSchema,
|
|
6
|
+
privateUseUriSchema,
|
|
7
|
+
} from './uri.js'
|
|
8
|
+
|
|
9
|
+
export const oauthLoopbackRedirectURISchema = loopbackUriSchema.superRefine(
|
|
10
|
+
(value, ctx): value is Exclude<LoopbackUri, `http://localhost${string}`> => {
|
|
11
|
+
if (value.startsWith('http://localhost')) {
|
|
12
|
+
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.3
|
|
13
|
+
//
|
|
14
|
+
// > While redirect URIs using localhost (i.e.,
|
|
15
|
+
// > "http://localhost:{port}/{path}") function similarly to loopback IP
|
|
16
|
+
// > redirects described in Section 7.3, the use of localhost is NOT
|
|
17
|
+
// > RECOMMENDED. Specifying a redirect URI with the loopback IP literal
|
|
18
|
+
// > rather than localhost avoids inadvertently listening on network
|
|
19
|
+
// > interfaces other than the loopback interface. It is also less
|
|
20
|
+
// > susceptible to client-side firewalls and misconfigured host name
|
|
21
|
+
// > resolution on the user's device.
|
|
22
|
+
ctx.addIssue({
|
|
23
|
+
code: ZodIssueCode.custom,
|
|
24
|
+
message:
|
|
25
|
+
'Use of "localhost" hostname is not allowed (RFC 8252), use a loopback IP such as "127.0.0.1" instead',
|
|
26
|
+
})
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return true
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
export type OAuthLoopbackRedirectURI = TypeOf<
|
|
34
|
+
typeof oauthLoopbackRedirectURISchema
|
|
35
|
+
>
|
|
36
|
+
|
|
37
|
+
export const oauthHttpsRedirectURISchema = httpsUriSchema
|
|
38
|
+
export type OAuthHttpsRedirectURI = TypeOf<typeof oauthHttpsRedirectURISchema>
|
|
39
|
+
|
|
40
|
+
export const oauthPrivateUseRedirectURISchema = privateUseUriSchema
|
|
41
|
+
export type OAuthPrivateUseRedirectURI = TypeOf<
|
|
42
|
+
typeof oauthPrivateUseRedirectURISchema
|
|
43
|
+
>
|
|
44
|
+
|
|
45
|
+
export const oauthRedirectUriSchema = z.union(
|
|
46
|
+
[
|
|
47
|
+
oauthLoopbackRedirectURISchema,
|
|
48
|
+
oauthHttpsRedirectURISchema,
|
|
49
|
+
oauthPrivateUseRedirectURISchema,
|
|
50
|
+
],
|
|
51
|
+
{
|
|
52
|
+
message: `URL must use the "https:" or "http:" protocol, or a private-use URI scheme (RFC 8252)`,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export type OAuthRedirectUri = TypeOf<typeof oauthRedirectUriSchema>
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
3
2
|
import { oauthRefreshTokenSchema } from './oauth-refresh-token.js'
|
|
4
3
|
|
|
5
4
|
export const oauthRefreshTokenGrantTokenRequestSchema = z.object({
|
|
6
5
|
grant_type: z.literal('refresh_token'),
|
|
7
6
|
refresh_token: oauthRefreshTokenSchema,
|
|
8
|
-
client_id: oauthClientIdSchema,
|
|
9
7
|
})
|
|
10
8
|
|
|
11
9
|
export type OAuthRefreshTokenGrantTokenRequest = z.infer<
|
|
@@ -9,13 +9,15 @@ import { oauthTokenTypeSchema } from './oauth-token-type.js'
|
|
|
9
9
|
*/
|
|
10
10
|
export const oauthTokenResponseSchema = z
|
|
11
11
|
.object({
|
|
12
|
+
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
|
12
13
|
access_token: z.string(),
|
|
13
14
|
token_type: oauthTokenTypeSchema,
|
|
14
|
-
issuer: z.string().url().optional(),
|
|
15
15
|
scope: z.string().optional(),
|
|
16
|
-
id_token: signedJwtSchema.optional(),
|
|
17
16
|
refresh_token: z.string().optional(),
|
|
18
17
|
expires_in: z.number().optional(),
|
|
18
|
+
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
|
19
|
+
id_token: signedJwtSchema.optional(),
|
|
20
|
+
// https://datatracker.ietf.org/doc/html/rfc9396#name-enriched-authorization-deta
|
|
19
21
|
authorization_details: oauthAuthorizationDetailsSchema.optional(),
|
|
20
22
|
})
|
|
21
23
|
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
package/src/uri.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { TypeOf, z, ZodIssueCode } from 'zod'
|
|
2
|
+
import { isHostnameIP, isLoopbackHost } from './util.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Any value that matches this schema is safe to parse using `new URL()`.
|
|
8
|
+
*/
|
|
9
|
+
export const dangerousUriSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.refine(
|
|
12
|
+
(data): data is `${string}:${string}` =>
|
|
13
|
+
data.includes(':') && URL.canParse(data),
|
|
14
|
+
{
|
|
15
|
+
message: 'Invalid URL',
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
|
|
21
|
+
*/
|
|
22
|
+
export type DangerousUrl = TypeOf<typeof dangerousUriSchema>
|
|
23
|
+
|
|
24
|
+
export const loopbackUriSchema = dangerousUriSchema.superRefine(
|
|
25
|
+
(
|
|
26
|
+
value,
|
|
27
|
+
ctx,
|
|
28
|
+
): value is
|
|
29
|
+
| `http://[::1]${string}`
|
|
30
|
+
| `http://localhost${'' | `${':' | '/' | '?' | '#'}${string}`}`
|
|
31
|
+
| `http://127.0.0.1${'' | `${':' | '/' | '?' | '#'}${string}`}` => {
|
|
32
|
+
// Loopback url must use the "http:" protocol
|
|
33
|
+
if (!value.startsWith('http://')) {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: ZodIssueCode.custom,
|
|
36
|
+
message: 'URL must use the "http:" protocol',
|
|
37
|
+
})
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = new URL(value)
|
|
42
|
+
|
|
43
|
+
if (!isLoopbackHost(url.hostname)) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: ZodIssueCode.custom,
|
|
46
|
+
message: 'URL must use "localhost", "127.0.0.1" or "[::1]" as hostname',
|
|
47
|
+
})
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return true
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
export type LoopbackUri = TypeOf<typeof loopbackUriSchema>
|
|
56
|
+
|
|
57
|
+
export const httpsUriSchema = dangerousUriSchema.superRefine(
|
|
58
|
+
(value, ctx): value is `https://${string}` => {
|
|
59
|
+
if (!value.startsWith('https://')) {
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: ZodIssueCode.custom,
|
|
62
|
+
message: 'URL must use the "https:" protocol',
|
|
63
|
+
})
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const url = new URL(value)
|
|
68
|
+
|
|
69
|
+
// Disallow loopback URLs with the `https:` protocol
|
|
70
|
+
if (isLoopbackHost(url.hostname)) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: ZodIssueCode.custom,
|
|
73
|
+
message: 'https: URL must not use a loopback host',
|
|
74
|
+
})
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isHostnameIP(url.hostname)) {
|
|
79
|
+
// Hostname is an IP address
|
|
80
|
+
} else {
|
|
81
|
+
// Hostname is a domain name
|
|
82
|
+
if (!url.hostname.includes('.')) {
|
|
83
|
+
// we don't depend on PSL here, so we only check for a dot
|
|
84
|
+
ctx.addIssue({
|
|
85
|
+
code: ZodIssueCode.custom,
|
|
86
|
+
message: 'Domain name must contain at least two segments',
|
|
87
|
+
})
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (url.hostname.endsWith('.local')) {
|
|
92
|
+
ctx.addIssue({
|
|
93
|
+
code: ZodIssueCode.custom,
|
|
94
|
+
message: 'Domain name must not end with ".local"',
|
|
95
|
+
})
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
export type HttpsUri = TypeOf<typeof httpsUriSchema>
|
|
105
|
+
|
|
106
|
+
export const webUriSchema = z
|
|
107
|
+
.string()
|
|
108
|
+
.superRefine((value, ctx): value is LoopbackUri | HttpsUri => {
|
|
109
|
+
// discriminated union of `loopbackUriSchema` and `httpsUriSchema`
|
|
110
|
+
if (value.startsWith('http://')) {
|
|
111
|
+
const result = loopbackUriSchema.safeParse(value)
|
|
112
|
+
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
113
|
+
return result.success
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (value.startsWith('https://')) {
|
|
117
|
+
const result = httpsUriSchema.safeParse(value)
|
|
118
|
+
if (!result.success) result.error.issues.forEach(ctx.addIssue, ctx)
|
|
119
|
+
return result.success
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ctx.addIssue({
|
|
123
|
+
code: ZodIssueCode.custom,
|
|
124
|
+
message: 'URL must use the "http:" or "https:" protocol',
|
|
125
|
+
})
|
|
126
|
+
return false
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
export type WebUri = TypeOf<typeof webUriSchema>
|
|
130
|
+
|
|
131
|
+
export const privateUseUriSchema = dangerousUriSchema.superRefine(
|
|
132
|
+
(value, ctx): value is `${string}.${string}:/${string}` => {
|
|
133
|
+
const dotIdx = value.indexOf('.')
|
|
134
|
+
const colonIdx = value.indexOf(':')
|
|
135
|
+
|
|
136
|
+
// Optimization: avoid parsing the URL if the protocol does not contain a "."
|
|
137
|
+
if (dotIdx === -1 || colonIdx === -1 || dotIdx > colonIdx) {
|
|
138
|
+
ctx.addIssue({
|
|
139
|
+
code: ZodIssueCode.custom,
|
|
140
|
+
message:
|
|
141
|
+
'Private-use URI scheme requires a "." as part of the protocol',
|
|
142
|
+
})
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const url = new URL(value)
|
|
147
|
+
|
|
148
|
+
// Should be covered by the check before, but let's be extra sure
|
|
149
|
+
if (!url.protocol.includes('.')) {
|
|
150
|
+
ctx.addIssue({
|
|
151
|
+
code: ZodIssueCode.custom,
|
|
152
|
+
message: 'Invalid private-use URI scheme',
|
|
153
|
+
})
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (url.hostname) {
|
|
158
|
+
// https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
|
|
159
|
+
ctx.addIssue({
|
|
160
|
+
code: ZodIssueCode.custom,
|
|
161
|
+
message:
|
|
162
|
+
'Private-use URI schemes must not include a hostname (only one "/" is allowed after the protocol, as per RFC 8252)',
|
|
163
|
+
})
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return true
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
export type PrivateUseUri = TypeOf<typeof privateUseUriSchema>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/atproto-loopback-client-metadata.ts","./src/constants.ts","./src/index.ts","./src/oauth-access-token.ts","./src/oauth-authorization-code-grant-token-request.ts","./src/oauth-authorization-details.ts","./src/oauth-authorization-request-jar.ts","./src/oauth-authorization-request-par.ts","./src/oauth-authorization-request-parameters.ts","./src/oauth-authorization-request-query.ts","./src/oauth-authorization-request-uri.ts","./src/oauth-authorization-server-metadata.ts","./src/oauth-client-credentials-grant-token-request.ts","./src/oauth-client-credentials.ts","./src/oauth-client-id-discoverable.ts","./src/oauth-client-id-loopback.ts","./src/oauth-client-id.ts","./src/oauth-client-metadata.ts","./src/oauth-code-challenge-method.ts","./src/oauth-endpoint-auth-method.ts","./src/oauth-endpoint-name.ts","./src/oauth-grant-type.ts","./src/oauth-introspection-response.ts","./src/oauth-issuer-identifier.ts","./src/oauth-par-response.ts","./src/oauth-password-grant-token-request.ts","./src/oauth-protected-resource-metadata.ts","./src/oauth-redirect-uri.ts","./src/oauth-refresh-token-grant-token-request.ts","./src/oauth-refresh-token.ts","./src/oauth-request-uri.ts","./src/oauth-response-mode.ts","./src/oauth-response-type.ts","./src/oauth-scope.ts","./src/oauth-token-identification.ts","./src/oauth-token-request.ts","./src/oauth-token-response.ts","./src/oauth-token-type.ts","./src/oidc-claims-parameter.ts","./src/oidc-claims-properties.ts","./src/oidc-entity-type.ts","./src/uri.ts","./src/util.ts"],"version":"5.6.3"}
|