@atproto/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/CHANGELOG.md +12 -0
- package/LICENSE.txt +7 -0
- package/README.md +3 -0
- package/dist/access-token.d.ts +4 -0
- package/dist/access-token.d.ts.map +1 -0
- package/dist/access-token.js +6 -0
- package/dist/access-token.js.map +1 -0
- package/dist/atproto-loopback-client-metadata.d.ts +3 -0
- package/dist/atproto-loopback-client-metadata.d.ts.map +1 -0
- package/dist/atproto-loopback-client-metadata.js +26 -0
- package/dist/atproto-loopback-client-metadata.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth-authentication-request-parameters.d.ts +128 -0
- package/dist/oauth-authentication-request-parameters.d.ts.map +1 -0
- package/dist/oauth-authentication-request-parameters.js +76 -0
- package/dist/oauth-authentication-request-parameters.js.map +1 -0
- package/dist/oauth-authorization-details.d.ts +54 -0
- package/dist/oauth-authorization-details.d.ts.map +1 -0
- package/dist/oauth-authorization-details.js +20 -0
- package/dist/oauth-authorization-details.js.map +1 -0
- package/dist/oauth-authorization-server-metadata.d.ts +428 -0
- package/dist/oauth-authorization-server-metadata.d.ts.map +1 -0
- package/dist/oauth-authorization-server-metadata.js +88 -0
- package/dist/oauth-authorization-server-metadata.js.map +1 -0
- package/dist/oauth-client-credentials.d.ts +66 -0
- package/dist/oauth-client-credentials.d.ts.map +1 -0
- package/dist/oauth-client-credentials.js +30 -0
- package/dist/oauth-client-credentials.js.map +1 -0
- package/dist/oauth-client-id-discoverable.d.ts +8 -0
- package/dist/oauth-client-id-discoverable.d.ts.map +1 -0
- package/dist/oauth-client-id-discoverable.js +48 -0
- package/dist/oauth-client-id-discoverable.js.map +1 -0
- package/dist/oauth-client-id-loopback.d.ts +5 -0
- package/dist/oauth-client-id-loopback.d.ts.map +1 -0
- package/dist/oauth-client-id-loopback.js +44 -0
- package/dist/oauth-client-id-loopback.js.map +1 -0
- package/dist/oauth-client-id-url.d.ts +3 -0
- package/dist/oauth-client-id-url.d.ts.map +1 -0
- package/dist/oauth-client-id-url.js +21 -0
- package/dist/oauth-client-id-url.js.map +1 -0
- package/dist/oauth-client-id.d.ts +4 -0
- package/dist/oauth-client-id.d.ts.map +1 -0
- package/dist/oauth-client-id.js +6 -0
- package/dist/oauth-client-id.js.map +1 -0
- package/dist/oauth-client-identification.d.ts +31 -0
- package/dist/oauth-client-identification.d.ts.map +1 -0
- package/dist/oauth-client-identification.js +12 -0
- package/dist/oauth-client-identification.js.map +1 -0
- package/dist/oauth-client-metadata.d.ts +1576 -0
- package/dist/oauth-client-metadata.d.ts.map +1 -0
- package/dist/oauth-client-metadata.js +70 -0
- package/dist/oauth-client-metadata.js.map +1 -0
- package/dist/oauth-endpoint-auth-method.d.ts +4 -0
- package/dist/oauth-endpoint-auth-method.d.ts.map +1 -0
- package/dist/oauth-endpoint-auth-method.js +14 -0
- package/dist/oauth-endpoint-auth-method.js.map +1 -0
- package/dist/oauth-endpoint-name.d.ts +2 -0
- package/dist/oauth-endpoint-name.d.ts.map +1 -0
- package/dist/oauth-endpoint-name.js +3 -0
- package/dist/oauth-endpoint-name.js.map +1 -0
- package/dist/oauth-grant-type.d.ts +4 -0
- package/dist/oauth-grant-type.d.ts.map +1 -0
- package/dist/oauth-grant-type.js +14 -0
- package/dist/oauth-grant-type.js.map +1 -0
- package/dist/oauth-issuer-identifier.d.ts +3 -0
- package/dist/oauth-issuer-identifier.d.ts.map +1 -0
- package/dist/oauth-issuer-identifier.js +59 -0
- package/dist/oauth-issuer-identifier.js.map +1 -0
- package/dist/oauth-par-response.d.ts +10 -0
- package/dist/oauth-par-response.d.ts.map +1 -0
- package/dist/oauth-par-response.js +8 -0
- package/dist/oauth-par-response.js.map +1 -0
- package/dist/oauth-protected-resource-metadata.d.ts +90 -0
- package/dist/oauth-protected-resource-metadata.d.ts.map +1 -0
- package/dist/oauth-protected-resource-metadata.js +75 -0
- package/dist/oauth-protected-resource-metadata.js.map +1 -0
- package/dist/oauth-response-mode.d.ts +4 -0
- package/dist/oauth-response-mode.d.ts.map +1 -0
- package/dist/oauth-response-mode.js +10 -0
- package/dist/oauth-response-mode.js.map +1 -0
- package/dist/oauth-response-type.d.ts +4 -0
- package/dist/oauth-response-type.d.ts.map +1 -0
- package/dist/oauth-response-type.js +17 -0
- package/dist/oauth-response-type.js.map +1 -0
- package/dist/oauth-token-response.d.ts +103 -0
- package/dist/oauth-token-response.d.ts.map +1 -0
- package/dist/oauth-token-response.js +26 -0
- package/dist/oauth-token-response.js.map +1 -0
- package/dist/oauth-token-type.d.ts +4 -0
- package/dist/oauth-token-type.d.ts.map +1 -0
- package/dist/oauth-token-type.js +16 -0
- package/dist/oauth-token-type.js.map +1 -0
- package/dist/oidc-claims-parameter.d.ts +4 -0
- package/dist/oidc-claims-parameter.d.ts.map +1 -0
- package/dist/oidc-claims-parameter.js +36 -0
- package/dist/oidc-claims-parameter.js.map +1 -0
- package/dist/oidc-claims-properties.d.ts +16 -0
- package/dist/oidc-claims-properties.d.ts.map +1 -0
- package/dist/oidc-claims-properties.js +11 -0
- package/dist/oidc-claims-properties.js.map +1 -0
- package/dist/oidc-entity-type.d.ts +4 -0
- package/dist/oidc-entity-type.d.ts.map +1 -0
- package/dist/oidc-entity-type.js +6 -0
- package/dist/oidc-entity-type.js.map +1 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +23 -0
- package/dist/util.js.map +1 -0
- package/package.json +37 -0
- package/src/access-token.ts +4 -0
- package/src/atproto-loopback-client-metadata.ts +30 -0
- package/src/constants.ts +9 -0
- package/src/index.ts +27 -0
- package/src/oauth-authentication-request-parameters.ts +104 -0
- package/src/oauth-authorization-details.ts +28 -0
- package/src/oauth-authorization-server-metadata.ts +106 -0
- package/src/oauth-client-credentials.ts +34 -0
- package/src/oauth-client-id-discoverable.ts +66 -0
- package/src/oauth-client-id-loopback.ts +58 -0
- package/src/oauth-client-id-url.ts +25 -0
- package/src/oauth-client-id.ts +4 -0
- package/src/oauth-client-identification.ts +14 -0
- package/src/oauth-client-metadata.ts +75 -0
- package/src/oauth-endpoint-auth-method.ts +13 -0
- package/src/oauth-endpoint-name.ts +5 -0
- package/src/oauth-grant-type.ts +13 -0
- package/src/oauth-issuer-identifier.ts +61 -0
- package/src/oauth-par-response.ts +7 -0
- package/src/oauth-protected-resource-metadata.ts +85 -0
- package/src/oauth-response-mode.ts +9 -0
- package/src/oauth-response-type.ts +17 -0
- package/src/oauth-token-response.ts +29 -0
- package/src/oauth-token-type.ts +15 -0
- package/src/oidc-claims-parameter.ts +40 -0
- package/src/oidc-claims-properties.ts +11 -0
- package/src/oidc-entity-type.ts +5 -0
- package/src/util.ts +20 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parseOAuthClientIdUrl } from './oauth-client-id-url.js'
|
|
2
|
+
import { OAuthClientId } from './oauth-client-id.js'
|
|
3
|
+
|
|
4
|
+
export type OAuthClientIdLoopback = OAuthClientId &
|
|
5
|
+
`http://localhost${'' | `${'/' | '?' | '#'}${string}`}`
|
|
6
|
+
|
|
7
|
+
export function isOAuthClientIdLoopback<C extends OAuthClientId>(
|
|
8
|
+
clientId: C,
|
|
9
|
+
): clientId is C & OAuthClientIdLoopback {
|
|
10
|
+
try {
|
|
11
|
+
parseOAuthLoopbackClientId(clientId)
|
|
12
|
+
return true
|
|
13
|
+
} catch {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseOAuthLoopbackClientId(clientId: OAuthClientId): URL {
|
|
19
|
+
const url = parseOAuthClientIdUrl(clientId)
|
|
20
|
+
|
|
21
|
+
// Optimization: cheap checks first
|
|
22
|
+
|
|
23
|
+
if (url.protocol !== 'http:') {
|
|
24
|
+
throw new TypeError('Loopback ClientID must use the "http:" protocol')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (url.hostname !== 'localhost') {
|
|
28
|
+
throw new TypeError('Loopback ClientID must use the "localhost" hostname')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (url.hash) {
|
|
32
|
+
throw new TypeError('Loopback ClientID must not contain a fragment')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (url.username || url.password) {
|
|
36
|
+
throw new TypeError('Loopback ClientID must not contain credentials')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (url.port) {
|
|
40
|
+
throw new TypeError('Loopback ClientID must not contain a port')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Note: url.pathname === '/' is allowed for loopback URIs
|
|
44
|
+
|
|
45
|
+
if (url.pathname !== '/' && url.pathname.endsWith('/')) {
|
|
46
|
+
throw new TypeError('Loopback ClientID must not end with a trailing slash')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (url.pathname.includes('//')) {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
`Loopback ClientID must not contain any double slashes in its path`,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Note: Query string is allowed
|
|
56
|
+
|
|
57
|
+
return url
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OAuthClientId } from './oauth-client-id.js'
|
|
2
|
+
|
|
3
|
+
export function parseOAuthClientIdUrl(clientId: OAuthClientId): URL {
|
|
4
|
+
if (clientId.endsWith('/')) {
|
|
5
|
+
throw new TypeError('ClientID must not end with a trailing slash')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const url = new URL(clientId)
|
|
9
|
+
|
|
10
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
11
|
+
throw new TypeError('ClientID must use the "https:" or "http:" protocol')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
url.searchParams.sort()
|
|
15
|
+
|
|
16
|
+
// URL constructor normalizes the URL, so we need to compare the canonical form
|
|
17
|
+
const canonicalUri = url.pathname === '/' ? url.origin + url.search : url.href
|
|
18
|
+
if (canonicalUri !== clientId) {
|
|
19
|
+
throw new TypeError(
|
|
20
|
+
`ClientID must be in canonical form ("${canonicalUri}", got "${clientId}")`,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return url
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
4
|
+
import { oauthClientCredentialsSchema } from './oauth-client-credentials.js'
|
|
5
|
+
|
|
6
|
+
export const oauthClientIdentificationSchema = z.union([
|
|
7
|
+
oauthClientCredentialsSchema,
|
|
8
|
+
// Must be last since it is less specific
|
|
9
|
+
z.object({ client_id: oauthClientIdSchema }),
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
export type OAuthClientIdentification = z.infer<
|
|
13
|
+
typeof oauthClientIdentificationSchema
|
|
14
|
+
>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jwksPubSchema } from '@atproto/jwk'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { oauthClientIdSchema } from './oauth-client-id.js'
|
|
5
|
+
import { oauthEndpointAuthMethod } from './oauth-endpoint-auth-method.js'
|
|
6
|
+
import { oauthGrantTypeSchema } from './oauth-grant-type.js'
|
|
7
|
+
import { oauthResponseTypeSchema } from './oauth-response-type.js'
|
|
8
|
+
|
|
9
|
+
// https://openid.net/specs/openid-connect-registration-1_0.html
|
|
10
|
+
// https://datatracker.ietf.org/doc/html/rfc7591
|
|
11
|
+
export const oauthClientMetadataSchema = z.object({
|
|
12
|
+
redirect_uris: z.array(z.string().url()).nonempty(),
|
|
13
|
+
response_types: z
|
|
14
|
+
.array(oauthResponseTypeSchema)
|
|
15
|
+
.nonempty()
|
|
16
|
+
// > If omitted, the default is that the client will use only the "code"
|
|
17
|
+
// > response type.
|
|
18
|
+
.default(['code']),
|
|
19
|
+
grant_types: z
|
|
20
|
+
.array(oauthGrantTypeSchema)
|
|
21
|
+
.nonempty()
|
|
22
|
+
// > If omitted, the default behavior is that the client will use only the
|
|
23
|
+
// > "authorization_code" Grant Type.
|
|
24
|
+
.default(['authorization_code']),
|
|
25
|
+
scope: z.string().optional(),
|
|
26
|
+
token_endpoint_auth_method: oauthEndpointAuthMethod
|
|
27
|
+
.default('none')
|
|
28
|
+
.optional(),
|
|
29
|
+
token_endpoint_auth_signing_alg: z.string().optional(),
|
|
30
|
+
introspection_endpoint_auth_method: oauthEndpointAuthMethod.optional(),
|
|
31
|
+
introspection_endpoint_auth_signing_alg: z.string().optional(),
|
|
32
|
+
revocation_endpoint_auth_method: oauthEndpointAuthMethod.optional(),
|
|
33
|
+
revocation_endpoint_auth_signing_alg: z.string().optional(),
|
|
34
|
+
pushed_authorization_request_endpoint_auth_method:
|
|
35
|
+
oauthEndpointAuthMethod.optional(),
|
|
36
|
+
pushed_authorization_request_endpoint_auth_signing_alg: z.string().optional(),
|
|
37
|
+
userinfo_signed_response_alg: z.string().optional(),
|
|
38
|
+
userinfo_encrypted_response_alg: z.string().optional(),
|
|
39
|
+
jwks_uri: z.string().url().optional(),
|
|
40
|
+
jwks: jwksPubSchema.optional(),
|
|
41
|
+
application_type: z.enum(['web', 'native']).default('web').optional(), // default, per spec, is "web"
|
|
42
|
+
subject_type: z.enum(['public', 'pairwise']).default('public').optional(),
|
|
43
|
+
request_object_signing_alg: z.string().optional(),
|
|
44
|
+
id_token_signed_response_alg: z.string().optional(),
|
|
45
|
+
authorization_signed_response_alg: z.string().default('RS256').optional(),
|
|
46
|
+
authorization_encrypted_response_enc: z.enum(['A128CBC-HS256']).optional(),
|
|
47
|
+
authorization_encrypted_response_alg: z.string().optional(),
|
|
48
|
+
client_id: oauthClientIdSchema.optional(),
|
|
49
|
+
client_name: z.string().optional(),
|
|
50
|
+
client_uri: z.string().url().optional(),
|
|
51
|
+
policy_uri: z.string().url().optional(),
|
|
52
|
+
tos_uri: z.string().url().optional(),
|
|
53
|
+
logo_uri: z.string().url().optional(),
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default Maximum Authentication Age. Specifies that the End-User MUST be
|
|
57
|
+
* actively authenticated if the End-User was authenticated longer ago than
|
|
58
|
+
* the specified number of seconds. The max_age request parameter overrides
|
|
59
|
+
* this default value. If omitted, no default Maximum Authentication Age is
|
|
60
|
+
* specified.
|
|
61
|
+
*/
|
|
62
|
+
default_max_age: z.number().optional(),
|
|
63
|
+
require_auth_time: z.boolean().optional(),
|
|
64
|
+
contacts: z.array(z.string().email()).optional(),
|
|
65
|
+
tls_client_certificate_bound_access_tokens: z.boolean().optional(),
|
|
66
|
+
|
|
67
|
+
// https://datatracker.ietf.org/doc/html/rfc9449#section-5.2
|
|
68
|
+
dpop_bound_access_tokens: z.boolean().optional(),
|
|
69
|
+
|
|
70
|
+
// https://datatracker.ietf.org/doc/html/rfc9396#section-14.5
|
|
71
|
+
authorization_details_types: z.array(z.string()).optional(),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export type OAuthClientMetadata = z.infer<typeof oauthClientMetadataSchema>
|
|
75
|
+
export type OAuthClientMetadataInput = z.input<typeof oauthClientMetadataSchema>
|
|
@@ -0,0 +1,13 @@
|
|
|
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>
|
|
@@ -0,0 +1,13 @@
|
|
|
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>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// try/catch to support running in a browser, including when process.env is
|
|
4
|
+
// shimmed (e.g. by webpack)
|
|
5
|
+
const ALLOW_INSECURE = (() => {
|
|
6
|
+
try {
|
|
7
|
+
const env = process.env.NODE_ENV
|
|
8
|
+
return env === 'development' || env === 'test'
|
|
9
|
+
} catch {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
})()
|
|
13
|
+
|
|
14
|
+
export const oauthIssuerIdentifierSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.url()
|
|
17
|
+
.superRefine((value, ctx) => {
|
|
18
|
+
// Validate the issuer (MIX-UP attacks)
|
|
19
|
+
|
|
20
|
+
if (value.endsWith('/')) {
|
|
21
|
+
ctx.addIssue({
|
|
22
|
+
code: z.ZodIssueCode.custom,
|
|
23
|
+
message: 'Issuer URL must not end with a slash',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const url = new URL(value)
|
|
28
|
+
|
|
29
|
+
if (url.protocol !== 'https:') {
|
|
30
|
+
if (ALLOW_INSECURE && url.protocol === 'http:') {
|
|
31
|
+
// We'll allow HTTP in development mode
|
|
32
|
+
} else {
|
|
33
|
+
ctx.addIssue({
|
|
34
|
+
code: z.ZodIssueCode.custom,
|
|
35
|
+
message: 'Issuer must be an HTTPS URL',
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (url.username || url.password) {
|
|
41
|
+
ctx.addIssue({
|
|
42
|
+
code: z.ZodIssueCode.custom,
|
|
43
|
+
message: 'Issuer URL must not contain a username or password',
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (url.hash || url.search) {
|
|
48
|
+
ctx.addIssue({
|
|
49
|
+
code: z.ZodIssueCode.custom,
|
|
50
|
+
message: 'Issuer URL must not contain a query or fragment',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const canonicalValue = url.pathname === '/' ? url.origin : url.href
|
|
55
|
+
if (value !== canonicalValue) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: z.ZodIssueCode.custom,
|
|
58
|
+
message: 'Issuer URL must be in the canonical form',
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#name-protected-resource-metadata-r}
|
|
7
|
+
*/
|
|
8
|
+
export const oauthProtectedResourceMetadataSchema = z.object({
|
|
9
|
+
/**
|
|
10
|
+
* REQUIRED. The protected resource's resource identifier, which is a URL that
|
|
11
|
+
* uses the https scheme and has no query or fragment components. Using these
|
|
12
|
+
* well-known resources is described in Section 3.
|
|
13
|
+
*/
|
|
14
|
+
resource: z.string().url(),
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* OPTIONAL. JSON array containing a list of OAuth authorization server issuer
|
|
18
|
+
* identifiers, as defined in [RFC8414], for authorization servers that can be
|
|
19
|
+
* used with this protected resource. Protected resources MAY choose not to
|
|
20
|
+
* advertise some supported authorization servers even when this parameter is
|
|
21
|
+
* used. In some use cases, the set of authorization servers will not be
|
|
22
|
+
* enumerable, in which case this metadata parameter would not be used.
|
|
23
|
+
*/
|
|
24
|
+
authorization_servers: z.array(oauthIssuerIdentifierSchema).optional(),
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* OPTIONAL. URL of the protected resource's JWK Set [JWK] document. This
|
|
28
|
+
* contains public keys belonging to the protected resource, such as signing
|
|
29
|
+
* key(s) that the resource server uses to sign resource responses. This URL
|
|
30
|
+
* MUST use the https scheme. When both signing and encryption keys are made
|
|
31
|
+
* available, a use (public key use) parameter value is REQUIRED for all keys
|
|
32
|
+
* in the referenced JWK Set to indicate each key's intended usage.
|
|
33
|
+
*/
|
|
34
|
+
jwks_uri: z.string().url().optional(),
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* RECOMMENDED. JSON array containing a list of the OAuth 2.0 [RFC6749] scope
|
|
38
|
+
* values that are used in authorization requests to request access to this
|
|
39
|
+
* protected resource. Protected resources MAY choose not to advertise some
|
|
40
|
+
* scope values supported even when this parameter is used.
|
|
41
|
+
*/
|
|
42
|
+
scopes_supported: z.array(z.string()).optional(),
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* OPTIONAL. JSON array containing a list of the supported methods of sending
|
|
46
|
+
* an OAuth 2.0 Bearer Token [RFC6750] to the protected resource. Defined
|
|
47
|
+
* values are ["header", "body", "query"], corresponding to Sections 2.1, 2.2,
|
|
48
|
+
* and 2.3 of RFC 6750.
|
|
49
|
+
*/
|
|
50
|
+
bearer_methods_supported: z
|
|
51
|
+
.array(z.enum(['header', 'body', 'query']))
|
|
52
|
+
.optional(),
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* OPTIONAL. JSON array containing a list of the JWS [JWS] signing algorithms
|
|
56
|
+
* (alg values) [JWA] supported by the protected resource for signing resource
|
|
57
|
+
* responses, for instance, as described in [FAPI.MessageSigning]. No default
|
|
58
|
+
* algorithms are implied if this entry is omitted. The value none MUST NOT be
|
|
59
|
+
* used.
|
|
60
|
+
*/
|
|
61
|
+
resource_signing_alg_values_supported: z.array(z.string()).optional(),
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* OPTIONAL. URL of a page containing human-readable information that
|
|
65
|
+
* developers might want or need to know when using the protected resource
|
|
66
|
+
*/
|
|
67
|
+
resource_documentation: z.string().url().optional(),
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* OPTIONAL. URL that the protected resource provides to read about the
|
|
71
|
+
* protected resource's requirements on how the client can use the data
|
|
72
|
+
* provided by the protected resource
|
|
73
|
+
*/
|
|
74
|
+
resource_policy_uri: z.string().url().optional(),
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* OPTIONAL. URL that the protected resource provides to read about the
|
|
78
|
+
* protected resource's terms of service
|
|
79
|
+
*/
|
|
80
|
+
resource_tos_uri: z.string().url().optional(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export type OAuthProtectedResourceMetadata = z.infer<
|
|
84
|
+
typeof oauthProtectedResourceMetadataSchema
|
|
85
|
+
>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const oauthResponseTypeSchema = z.enum([
|
|
4
|
+
// OAuth
|
|
5
|
+
'code', // Authorization Code Grant
|
|
6
|
+
'token', // Implicit Grant
|
|
7
|
+
|
|
8
|
+
// OpenID
|
|
9
|
+
'none',
|
|
10
|
+
'code id_token token',
|
|
11
|
+
'code id_token',
|
|
12
|
+
'code token',
|
|
13
|
+
'id_token token',
|
|
14
|
+
'id_token',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export type OAuthResponseType = z.infer<typeof oauthResponseTypeSchema>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { signedJwtSchema } from '@atproto/jwk'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js'
|
|
5
|
+
import { oauthTokenTypeSchema } from './oauth-token-type.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1 | RFC 6749 (OAuth2), Section 5.1}
|
|
9
|
+
*/
|
|
10
|
+
export const oauthTokenResponseSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
access_token: z.string(),
|
|
13
|
+
token_type: oauthTokenTypeSchema,
|
|
14
|
+
issuer: z.string().url().optional(),
|
|
15
|
+
sub: z.string().optional(),
|
|
16
|
+
scope: z.string().optional(),
|
|
17
|
+
id_token: signedJwtSchema.optional(),
|
|
18
|
+
refresh_token: z.string().optional(),
|
|
19
|
+
expires_in: z.number().optional(),
|
|
20
|
+
authorization_details: oauthAuthorizationDetailsSchema.optional(),
|
|
21
|
+
})
|
|
22
|
+
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
|
23
|
+
// > The client MUST ignore unrecognized value names in the response.
|
|
24
|
+
.passthrough()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @see {@link oauthTokenResponseSchema}
|
|
28
|
+
*/
|
|
29
|
+
export type OAuthTokenResponse = z.infer<typeof oauthTokenResponseSchema>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// Case insensitive input, normalized output
|
|
4
|
+
export const oauthTokenTypeSchema = z.union([
|
|
5
|
+
z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(/^DPoP$/i)
|
|
8
|
+
.transform(() => 'DPoP' as const),
|
|
9
|
+
z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^Bearer$/i)
|
|
12
|
+
.transform(() => 'Bearer' as const),
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
export type OAuthTokenType = z.infer<typeof oauthTokenTypeSchema>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const oidcClaimsParameterSchema = z.enum([
|
|
4
|
+
// https://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html#rfc.section.5.2
|
|
5
|
+
// if client metadata "require_auth_time" is true, this *must* be provided
|
|
6
|
+
'auth_time',
|
|
7
|
+
|
|
8
|
+
// OIDC
|
|
9
|
+
'nonce',
|
|
10
|
+
'acr',
|
|
11
|
+
|
|
12
|
+
// OpenID: "profile" scope
|
|
13
|
+
'name',
|
|
14
|
+
'family_name',
|
|
15
|
+
'given_name',
|
|
16
|
+
'middle_name',
|
|
17
|
+
'nickname',
|
|
18
|
+
'preferred_username',
|
|
19
|
+
'gender',
|
|
20
|
+
'picture',
|
|
21
|
+
'profile',
|
|
22
|
+
'website',
|
|
23
|
+
'birthdate',
|
|
24
|
+
'zoneinfo',
|
|
25
|
+
'locale',
|
|
26
|
+
'updated_at',
|
|
27
|
+
|
|
28
|
+
// OpenID: "email" scope
|
|
29
|
+
'email',
|
|
30
|
+
'email_verified',
|
|
31
|
+
|
|
32
|
+
// OpenID: "phone" scope
|
|
33
|
+
'phone_number',
|
|
34
|
+
'phone_number_verified',
|
|
35
|
+
|
|
36
|
+
// OpenID: "address" scope
|
|
37
|
+
'address',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
export type OidcClaimsParameter = z.infer<typeof oidcClaimsParameterSchema>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const oidcClaimsValueSchema = z.union([z.string(), z.number(), z.boolean()])
|
|
4
|
+
|
|
5
|
+
export const oidcClaimsPropertiesSchema = z.object({
|
|
6
|
+
essential: z.boolean().optional(),
|
|
7
|
+
value: oidcClaimsValueSchema.optional(),
|
|
8
|
+
values: z.array(oidcClaimsValueSchema).optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export type OidcClaimsProperties = z.infer<typeof oidcClaimsPropertiesSchema>
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function isIP(hostname: string) {
|
|
2
|
+
// IPv4
|
|
3
|
+
if (hostname.match(/^\d+\.\d+\.\d+\.\d+$/)) return true
|
|
4
|
+
|
|
5
|
+
// IPv6
|
|
6
|
+
if (hostname.startsWith('[') && hostname.endsWith(']')) return true
|
|
7
|
+
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type LoopbackHost = 'localhost' | '127.0.0.1' | '[::1]'
|
|
12
|
+
|
|
13
|
+
export function isLoopbackHost(host: unknown): host is LoopbackHost {
|
|
14
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '[::1]'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isLoopbackUrl(input: URL | string): boolean {
|
|
18
|
+
const url = typeof input === 'string' ? new URL(input) : input
|
|
19
|
+
return isLoopbackHost(url.hostname)
|
|
20
|
+
}
|
package/tsconfig.json
ADDED