@atproto/oauth-types 0.1.4 → 0.1.5

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.
Files changed (148) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
  3. package/dist/atproto-loopback-client-metadata.js +3 -14
  4. package/dist/atproto-loopback-client-metadata.js.map +1 -1
  5. package/dist/index.d.ts +18 -5
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +18 -5
  8. package/dist/index.js.map +1 -1
  9. package/dist/oauth-access-token.d.ts +4 -0
  10. package/dist/oauth-access-token.d.ts.map +1 -0
  11. package/dist/oauth-access-token.js +6 -0
  12. package/dist/oauth-access-token.js.map +1 -0
  13. package/dist/oauth-authorization-code-grant-token-request.d.ts +20 -0
  14. package/dist/oauth-authorization-code-grant-token-request.d.ts.map +1 -0
  15. package/dist/oauth-authorization-code-grant-token-request.js +17 -0
  16. package/dist/oauth-authorization-code-grant-token-request.js.map +1 -0
  17. package/dist/oauth-authorization-request-jar.d.ts +16 -0
  18. package/dist/oauth-authorization-request-jar.d.ts.map +1 -0
  19. package/dist/oauth-authorization-request-jar.js +15 -0
  20. package/dist/oauth-authorization-request-jar.js.map +1 -0
  21. package/dist/oauth-authorization-request-par.d.ts +122 -0
  22. package/dist/oauth-authorization-request-par.d.ts.map +1 -0
  23. package/dist/oauth-authorization-request-par.js +11 -0
  24. package/dist/oauth-authorization-request-par.js.map +1 -0
  25. package/dist/{oauth-authentication-request-parameters.d.ts → oauth-authorization-request-parameters.d.ts} +15 -15
  26. package/dist/oauth-authorization-request-parameters.d.ts.map +1 -0
  27. package/dist/{oauth-authentication-request-parameters.js → oauth-authorization-request-parameters.js} +15 -16
  28. package/dist/oauth-authorization-request-parameters.js.map +1 -0
  29. package/dist/oauth-authorization-request-query.d.ts +128 -0
  30. package/dist/oauth-authorization-request-query.d.ts.map +1 -0
  31. package/dist/oauth-authorization-request-query.js +13 -0
  32. package/dist/oauth-authorization-request-query.js.map +1 -0
  33. package/dist/oauth-authorization-request-uri.d.ts +10 -0
  34. package/dist/oauth-authorization-request-uri.d.ts.map +1 -0
  35. package/dist/oauth-authorization-request-uri.js +9 -0
  36. package/dist/oauth-authorization-request-uri.js.map +1 -0
  37. package/dist/oauth-authorization-server-metadata.d.ts +10 -10
  38. package/dist/oauth-authorization-server-metadata.d.ts.map +1 -1
  39. package/dist/oauth-authorization-server-metadata.js +5 -1
  40. package/dist/oauth-authorization-server-metadata.js.map +1 -1
  41. package/dist/oauth-client-credentials-grant-token-request.d.ts +10 -0
  42. package/dist/oauth-client-credentials-grant-token-request.d.ts.map +1 -0
  43. package/dist/oauth-client-credentials-grant-token-request.js +8 -0
  44. package/dist/oauth-client-credentials-grant-token-request.js.map +1 -0
  45. package/dist/oauth-client-credentials.d.ts +18 -2
  46. package/dist/oauth-client-credentials.d.ts.map +1 -1
  47. package/dist/oauth-client-credentials.js +8 -2
  48. package/dist/oauth-client-credentials.js.map +1 -1
  49. package/dist/oauth-client-id-discoverable.d.ts +3 -2
  50. package/dist/oauth-client-id-discoverable.d.ts.map +1 -1
  51. package/dist/oauth-client-id-discoverable.js +21 -18
  52. package/dist/oauth-client-id-discoverable.js.map +1 -1
  53. package/dist/oauth-client-id-loopback.d.ts +10 -3
  54. package/dist/oauth-client-id-loopback.d.ts.map +1 -1
  55. package/dist/oauth-client-id-loopback.js +58 -21
  56. package/dist/oauth-client-id-loopback.js.map +1 -1
  57. package/dist/oauth-client-metadata.d.ts +1 -1
  58. package/dist/oauth-client-metadata.d.ts.map +1 -1
  59. package/dist/oauth-client-metadata.js +2 -1
  60. package/dist/oauth-client-metadata.js.map +1 -1
  61. package/dist/oauth-code-challenge-method.d.ts +3 -0
  62. package/dist/oauth-code-challenge-method.d.ts.map +1 -0
  63. package/dist/oauth-code-challenge-method.js +6 -0
  64. package/dist/oauth-code-challenge-method.js.map +1 -0
  65. package/dist/oauth-introspection-response.d.ts +20 -0
  66. package/dist/oauth-introspection-response.d.ts.map +1 -0
  67. package/dist/oauth-introspection-response.js +3 -0
  68. package/dist/oauth-introspection-response.js.map +1 -0
  69. package/dist/oauth-par-response.d.ts +3 -0
  70. package/dist/oauth-par-response.d.ts.map +1 -1
  71. package/dist/oauth-par-response.js +1 -0
  72. package/dist/oauth-par-response.js.map +1 -1
  73. package/dist/oauth-password-grant-token-request.d.ts +16 -0
  74. package/dist/oauth-password-grant-token-request.d.ts.map +1 -0
  75. package/dist/oauth-password-grant-token-request.js +10 -0
  76. package/dist/oauth-password-grant-token-request.js.map +1 -0
  77. package/dist/oauth-refresh-token-grant-token-request.d.ts +16 -0
  78. package/dist/oauth-refresh-token-grant-token-request.d.ts.map +1 -0
  79. package/dist/oauth-refresh-token-grant-token-request.js +12 -0
  80. package/dist/oauth-refresh-token-grant-token-request.js.map +1 -0
  81. package/dist/oauth-refresh-token.d.ts +4 -0
  82. package/dist/oauth-refresh-token.d.ts.map +1 -0
  83. package/dist/oauth-refresh-token.js +6 -0
  84. package/dist/oauth-refresh-token.js.map +1 -0
  85. package/dist/oauth-request-uri.d.ts +4 -0
  86. package/dist/oauth-request-uri.d.ts.map +1 -0
  87. package/dist/oauth-request-uri.js +6 -0
  88. package/dist/oauth-request-uri.js.map +1 -0
  89. package/dist/oauth-scope.d.ts +10 -0
  90. package/dist/oauth-scope.d.ts.map +1 -0
  91. package/dist/oauth-scope.js +16 -0
  92. package/dist/oauth-scope.js.map +1 -0
  93. package/dist/oauth-token-identification.d.ts +13 -0
  94. package/dist/oauth-token-identification.d.ts.map +1 -0
  95. package/dist/oauth-token-identification.js +11 -0
  96. package/dist/oauth-token-identification.js.map +1 -0
  97. package/dist/oauth-token-request.d.ts +49 -0
  98. package/dist/oauth-token-request.d.ts.map +1 -0
  99. package/dist/oauth-token-request.js +15 -0
  100. package/dist/oauth-token-request.js.map +1 -0
  101. package/dist/util.d.ts +2 -1
  102. package/dist/util.d.ts.map +1 -1
  103. package/dist/util.js +34 -3
  104. package/dist/util.js.map +1 -1
  105. package/package.json +1 -1
  106. package/src/atproto-loopback-client-metadata.ts +7 -20
  107. package/src/index.ts +18 -5
  108. package/src/oauth-access-token.ts +4 -0
  109. package/src/oauth-authorization-code-grant-token-request.ts +18 -0
  110. package/src/oauth-authorization-request-jar.ts +16 -0
  111. package/src/oauth-authorization-request-par.ts +13 -0
  112. package/src/{oauth-authentication-request-parameters.ts → oauth-authorization-request-parameters.ts} +20 -21
  113. package/src/oauth-authorization-request-query.ts +15 -0
  114. package/src/oauth-authorization-request-uri.ts +11 -0
  115. package/src/oauth-authorization-server-metadata.ts +5 -1
  116. package/src/oauth-client-credentials-grant-token-request.ts +9 -0
  117. package/src/oauth-client-credentials.ts +21 -1
  118. package/src/oauth-client-id-discoverable.ts +29 -26
  119. package/src/oauth-client-id-loopback.ts +78 -30
  120. package/src/oauth-client-metadata.ts +2 -1
  121. package/src/oauth-code-challenge-method.ts +3 -0
  122. package/src/oauth-introspection-response.ts +23 -0
  123. package/src/oauth-par-response.ts +1 -0
  124. package/src/oauth-password-grant-token-request.ts +11 -0
  125. package/src/oauth-refresh-token-grant-token-request.ts +13 -0
  126. package/src/oauth-refresh-token.ts +4 -0
  127. package/src/oauth-request-uri.ts +5 -0
  128. package/src/oauth-scope.ts +15 -0
  129. package/src/oauth-token-identification.ts +12 -0
  130. package/src/oauth-token-request.ts +14 -0
  131. package/src/util.ts +41 -1
  132. package/dist/access-token.d.ts +0 -4
  133. package/dist/access-token.d.ts.map +0 -1
  134. package/dist/access-token.js +0 -6
  135. package/dist/access-token.js.map +0 -1
  136. package/dist/oauth-authentication-request-parameters.d.ts.map +0 -1
  137. package/dist/oauth-authentication-request-parameters.js.map +0 -1
  138. package/dist/oauth-client-id-url.d.ts +0 -3
  139. package/dist/oauth-client-id-url.d.ts.map +0 -1
  140. package/dist/oauth-client-id-url.js +0 -21
  141. package/dist/oauth-client-id-url.js.map +0 -1
  142. package/dist/oauth-client-identification.d.ts +0 -31
  143. package/dist/oauth-client-identification.d.ts.map +0 -1
  144. package/dist/oauth-client-identification.js +0 -12
  145. package/dist/oauth-client-identification.js.map +0 -1
  146. package/src/access-token.ts +0 -4
  147. package/src/oauth-client-id-url.ts +0 -25
  148. package/src/oauth-client-identification.ts +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-types",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "MIT",
5
5
  "description": "OAuth typing & validation library",
6
6
  "keywords": [
@@ -1,34 +1,21 @@
1
- import { isOAuthClientIdLoopback } from './oauth-client-id-loopback.js'
1
+ import { parseOAuthLoopbackClientId } from './oauth-client-id-loopback.js'
2
2
  import { OAuthClientMetadataInput } from './oauth-client-metadata.js'
3
- import { parseOAuthClientIdUrl } from './oauth-client-id-url.js'
4
3
 
5
4
  export function atprotoLoopbackClientMetadata(
6
5
  clientId: string,
7
6
  ): OAuthClientMetadataInput {
8
- if (!isOAuthClientIdLoopback(clientId)) {
9
- throw new TypeError(`Invalid loopback client ID ${clientId}`)
10
- }
11
-
12
- const { origin, pathname, searchParams } = parseOAuthClientIdUrl(clientId)
13
-
14
- for (const name of searchParams.keys()) {
15
- if (name !== 'redirect_uri') {
16
- throw new TypeError(`Invalid query parameter ${name} in client ID`)
17
- }
18
- }
19
- const redirectUris = searchParams.getAll('redirect_uri')
7
+ const {
8
+ scope = 'atproto',
9
+ redirect_uris = [`http://127.0.0.1/`, `http://[::1]/`],
10
+ } = parseOAuthLoopbackClientId(clientId)
20
11
 
21
12
  return {
22
13
  client_id: clientId,
14
+ scope,
15
+ redirect_uris,
23
16
  client_name: 'Loopback client',
24
17
  response_types: ['code'],
25
18
  grant_types: ['authorization_code', 'refresh_token'],
26
- redirect_uris: (redirectUris.length
27
- ? redirectUris
28
- : (['127.0.0.1', '[::1]'] as const).map(
29
- (ip) =>
30
- Object.assign(new URL(pathname, origin), { hostname: ip }).href,
31
- )) as [string, ...string[]],
32
19
  token_endpoint_auth_method: 'none',
33
20
  application_type: 'native',
34
21
  dpop_bound_access_tokens: true,
package/src/index.ts CHANGED
@@ -1,25 +1,38 @@
1
1
  export * from './constants.js'
2
2
  export * from './util.js'
3
3
 
4
- export * from './access-token.js'
5
4
  export * from './atproto-loopback-client-metadata.js'
6
- export * from './oauth-client-id-discoverable.js'
7
- export * from './oauth-client-id-loopback.js'
8
- export * from './oauth-authentication-request-parameters.js'
5
+ export * from './oauth-access-token.js'
6
+ export * from './oauth-authorization-code-grant-token-request.js'
9
7
  export * from './oauth-authorization-details.js'
8
+ export * from './oauth-authorization-request-jar.js'
9
+ export * from './oauth-authorization-request-par.js'
10
+ export * from './oauth-authorization-request-parameters.js'
11
+ export * from './oauth-authorization-request-query.js'
12
+ export * from './oauth-authorization-request-uri.js'
10
13
  export * from './oauth-authorization-server-metadata.js'
14
+ export * from './oauth-client-credentials-grant-token-request.js'
11
15
  export * from './oauth-client-credentials.js'
16
+ export * from './oauth-client-id-discoverable.js'
17
+ export * from './oauth-client-id-loopback.js'
12
18
  export * from './oauth-client-id.js'
13
- export * from './oauth-client-identification.js'
14
19
  export * from './oauth-client-metadata.js'
15
20
  export * from './oauth-endpoint-auth-method.js'
16
21
  export * from './oauth-endpoint-name.js'
17
22
  export * from './oauth-grant-type.js'
23
+ export * from './oauth-introspection-response.js'
18
24
  export * from './oauth-issuer-identifier.js'
19
25
  export * from './oauth-par-response.js'
26
+ export * from './oauth-password-grant-token-request.js'
20
27
  export * from './oauth-protected-resource-metadata.js'
28
+ export * from './oauth-refresh-token-grant-token-request.js'
29
+ export * from './oauth-refresh-token.js'
30
+ export * from './oauth-request-uri.js'
21
31
  export * from './oauth-response-mode.js'
22
32
  export * from './oauth-response-type.js'
33
+ export * from './oauth-scope.js'
34
+ export * from './oauth-token-identification.js'
35
+ export * from './oauth-token-request.js'
23
36
  export * from './oauth-token-response.js'
24
37
  export * from './oauth-token-type.js'
25
38
  export * from './oidc-claims-parameter.js'
@@ -0,0 +1,4 @@
1
+ import { z } from 'zod'
2
+
3
+ export const oauthAccessTokenSchema = z.string().min(1)
4
+ export type OAuthAccessToken = z.infer<typeof oauthAccessTokenSchema>
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod'
2
+
3
+ export const oauthAuthorizationCodeGrantTokenRequestSchema = z.object({
4
+ grant_type: z.literal('authorization_code'),
5
+ code: z.string().min(1),
6
+ redirect_uri: z.string().url(),
7
+ /** @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1} */
8
+ code_verifier: z
9
+ .string()
10
+ .min(43)
11
+ .max(128)
12
+ .regex(/^[a-zA-Z0-9-._~]+$/)
13
+ .optional(),
14
+ })
15
+
16
+ export type OAuthAuthorizationCodeGrantTokenRequest = z.infer<
17
+ typeof oauthAuthorizationCodeGrantTokenRequestSchema
18
+ >
@@ -0,0 +1,16 @@
1
+ import { signedJwtSchema, unsignedJwtSchema } from '@atproto/jwk'
2
+ import { z } from 'zod'
3
+
4
+ export const oauthAuthorizationRequestJarSchema = z.object({
5
+ /**
6
+ * AuthorizationRequest inside a JWT:
7
+ * - "iat" is required and **MUST** be less than one minute
8
+ *
9
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc9101}
10
+ */
11
+ request: z.union([signedJwtSchema, unsignedJwtSchema]),
12
+ })
13
+
14
+ export type OAuthAuthorizationRequestJar = z.infer<
15
+ typeof oauthAuthorizationRequestJarSchema
16
+ >
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod'
2
+
3
+ import { oauthAuthorizationRequestJarSchema } from './oauth-authorization-request-jar.js'
4
+ import { oauthAuthorizationRequestParametersSchema } from './oauth-authorization-request-parameters.js'
5
+
6
+ export const oauthAuthorizationRequestParSchema = z.union([
7
+ oauthAuthorizationRequestParametersSchema,
8
+ oauthAuthorizationRequestJarSchema,
9
+ ])
10
+
11
+ export type OAuthAuthorizationRequestPar = z.infer<
12
+ typeof oauthAuthorizationRequestParSchema
13
+ >
@@ -3,7 +3,9 @@ import { z } from 'zod'
3
3
 
4
4
  import { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js'
5
5
  import { oauthClientIdSchema } from './oauth-client-id.js'
6
+ import { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'
6
7
  import { oauthResponseTypeSchema } from './oauth-response-type.js'
8
+ import { oauthScopeSchema } from './oauth-scope.js'
7
9
  import { oidcClaimsParameterSchema } from './oidc-claims-parameter.js'
8
10
  import { oidcClaimsPropertiesSchema } from './oidc-claims-properties.js'
9
11
  import { oidcEntityTypeSchema } from './oidc-entity-type.js'
@@ -11,35 +13,32 @@ import { oidcEntityTypeSchema } from './oidc-entity-type.js'
11
13
  /**
12
14
  * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest | OIDC}
13
15
  */
14
- export const oauthAuthenticationRequestParametersSchema = z.object({
16
+ export const oauthAuthorizationRequestParametersSchema = z.object({
15
17
  client_id: oauthClientIdSchema,
16
-
17
18
  state: z.string().optional(),
18
- nonce: z.string().optional(),
19
- dpop_jkt: z.string().optional(),
20
-
19
+ redirect_uri: z.string().url().optional(),
20
+ scope: oauthScopeSchema.optional(),
21
21
  response_type: oauthResponseTypeSchema,
22
22
 
23
- // Default depend on response_type
24
- response_mode: z.enum(['query', 'fragment', 'form_post']).optional(),
25
-
26
23
  // PKCE
24
+
27
25
  code_challenge: z.string().optional(),
28
- code_challenge_method: z.enum(['S256', 'plain']).default('S256').optional(),
26
+ code_challenge_method: oauthCodeChallengeMethodSchema
27
+ .default('S256')
28
+ .optional(),
29
29
 
30
- redirect_uri: z.string().url().optional(),
30
+ // DPOP
31
31
 
32
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1
33
- // scope = scope-token *( SP scope-token )
34
- // scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
35
- // = Basically most ASCII characters except backslash and double quote
36
- scope: z
37
- .string()
38
- .regex(/^[!\x23-\x5B\x5D-\x7E]+( [!\x23-\x5B\x5D-\x7E]+)*$/)
39
- .optional(),
32
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-12.3
33
+ dpop_jkt: z.string().optional(),
40
34
 
41
35
  // OIDC
42
36
 
37
+ // Default depend on response_type
38
+ response_mode: z.enum(['query', 'fragment', 'form_post']).optional(),
39
+
40
+ nonce: z.string().optional(),
41
+
43
42
  // Specifies the allowable elapsed time in seconds since the last time the
44
43
  // End-User was actively authenticated by the OP. If the elapsed time is
45
44
  // greater than this value, the OP MUST attempt to actively re-authenticate
@@ -89,8 +88,8 @@ export const oauthAuthenticationRequestParametersSchema = z.object({
89
88
  })
90
89
 
91
90
  /**
92
- * @see {oauthAuthenticationRequestParametersSchema}
91
+ * @see {oauthAuthorizationRequestParametersSchema}
93
92
  */
94
- export type OAuthAuthenticationRequestParameters = z.infer<
95
- typeof oauthAuthenticationRequestParametersSchema
93
+ export type OAuthAuthorizationRequestParameters = z.infer<
94
+ typeof oauthAuthorizationRequestParametersSchema
96
95
  >
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod'
2
+
3
+ import { oauthAuthorizationRequestJarSchema } from './oauth-authorization-request-jar.js'
4
+ import { oauthAuthorizationRequestParametersSchema } from './oauth-authorization-request-parameters.js'
5
+ import { oauthAuthorizationRequestUriSchema } from './oauth-authorization-request-uri.js'
6
+
7
+ export const oauthAuthorizationRequestQuerySchema = z.union([
8
+ oauthAuthorizationRequestParametersSchema,
9
+ oauthAuthorizationRequestJarSchema,
10
+ oauthAuthorizationRequestUriSchema,
11
+ ])
12
+
13
+ export type OAuthAuthorizationRequestQuery = z.infer<
14
+ typeof oauthAuthorizationRequestQuerySchema
15
+ >
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod'
2
+
3
+ import { oauthRequestUriSchema } from './oauth-request-uri.js'
4
+
5
+ export const oauthAuthorizationRequestUriSchema = z.object({
6
+ request_uri: oauthRequestUriSchema,
7
+ })
8
+
9
+ export type OAuthAuthorizationRequestUri = z.infer<
10
+ typeof oauthAuthorizationRequestUriSchema
11
+ >
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'
3
4
  import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
4
5
 
5
6
  /**
@@ -19,7 +20,10 @@ export const oauthAuthorizationServerMetadataSchema = z.object({
19
20
  response_types_supported: z.array(z.string()).optional(),
20
21
  response_modes_supported: z.array(z.string()).optional(),
21
22
  grant_types_supported: z.array(z.string()).optional(),
22
- code_challenge_methods_supported: z.array(z.string()).min(1).optional(),
23
+ code_challenge_methods_supported: z
24
+ .array(oauthCodeChallengeMethodSchema)
25
+ .min(1)
26
+ .optional(),
23
27
  ui_locales_supported: z.array(z.string()).optional(),
24
28
  id_token_signing_alg_values_supported: z.array(z.string()).optional(),
25
29
  display_values_supported: z.array(z.string()).optional(),
@@ -0,0 +1,9 @@
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
+ >
@@ -14,19 +14,39 @@ export const oauthClientCredentialsJwtBearerSchema = z.object({
14
14
  * - The JWT MAY contain a "jti" (JWT ID) claim that provides a unique identifier for the token.
15
15
  * - Note that the authorization server may reject JWTs with an "exp" claim value that is unreasonably far in the future.
16
16
  *
17
- * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-bearer-11#section-3}
17
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}
18
18
  */
19
19
  client_assertion: signedJwtSchema,
20
20
  })
21
21
 
22
+ export type OAuthClientCredentialsJwtBearer = z.infer<
23
+ typeof oauthClientCredentialsJwtBearerSchema
24
+ >
25
+
22
26
  export const oauthClientCredentialsSecretPostSchema = z.object({
23
27
  client_id: oauthClientIdSchema,
24
28
  client_secret: z.string(),
25
29
  })
26
30
 
31
+ export type OAuthClientCredentialsSecretPost = z.infer<
32
+ typeof oauthClientCredentialsSecretPostSchema
33
+ >
34
+
35
+ export const oauthClientCredentialsNoneSchema = z.object({
36
+ client_id: oauthClientIdSchema,
37
+ })
38
+
39
+ export type OAuthClientCredentialsNone = z.infer<
40
+ typeof oauthClientCredentialsNoneSchema
41
+ >
42
+
43
+ //
44
+
27
45
  export const oauthClientCredentialsSchema = z.union([
28
46
  oauthClientCredentialsJwtBearerSchema,
29
47
  oauthClientCredentialsSecretPostSchema,
48
+ // Must be last since it is less specific
49
+ oauthClientCredentialsNoneSchema,
30
50
  ])
31
51
 
32
52
  export type OAuthClientCredentials = z.infer<
@@ -1,15 +1,14 @@
1
- import { parseOAuthClientIdUrl } from './oauth-client-id-url.js'
2
1
  import { OAuthClientId } from './oauth-client-id.js'
3
- import { isIP } from './util.js'
2
+ import { extractUrlPath, isHostnameIP } from './util.js'
4
3
 
5
4
  /**
6
5
  * @see {@link https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html}
7
6
  */
8
7
  export type OAuthClientIdDiscoverable = OAuthClientId & `https://${string}`
9
8
 
10
- export function isOAuthClientIdDiscoverable<C extends OAuthClientId>(
11
- clientId: C,
12
- ): clientId is C & OAuthClientIdDiscoverable {
9
+ export function isOAuthClientIdDiscoverable(
10
+ clientId: string,
11
+ ): clientId is OAuthClientIdDiscoverable {
13
12
  try {
14
13
  parseOAuthDiscoverableClientId(clientId)
15
14
  return true
@@ -18,48 +17,52 @@ export function isOAuthClientIdDiscoverable<C extends OAuthClientId>(
18
17
  }
19
18
  }
20
19
 
21
- export function parseOAuthDiscoverableClientId(clientId: OAuthClientId): URL {
22
- const url = parseOAuthClientIdUrl(clientId)
23
-
24
- // Optimization: cheap checks first
20
+ export function assertOAuthDiscoverableClientId(
21
+ value: string,
22
+ ): asserts value is OAuthClientIdDiscoverable {
23
+ void parseOAuthDiscoverableClientId(value)
24
+ }
25
25
 
26
- if (url.hostname === 'localhost') {
27
- throw new TypeError('ClientID must not be a loopback hostname')
28
- }
26
+ export function parseOAuthDiscoverableClientId(clientId: string): URL {
27
+ const url = new URL(clientId)
29
28
 
30
29
  if (url.protocol !== 'https:') {
31
30
  throw new TypeError('ClientID must use the "https:" protocol')
32
31
  }
33
32
 
33
+ if (url.username || url.password) {
34
+ throw new TypeError('ClientID must not contain credentials')
35
+ }
36
+
34
37
  if (url.hash) {
35
38
  throw new TypeError('ClientID must not contain a fragment')
36
39
  }
37
40
 
38
- if (url.username || url.password) {
39
- throw new TypeError('ClientID must not contain credentials')
41
+ if (url.hostname === 'localhost') {
42
+ throw new TypeError('ClientID hostname must not be "localhost"')
40
43
  }
41
44
 
42
45
  if (url.pathname === '/') {
43
46
  throw new TypeError(
44
- 'ClientID must contain a path (e.g. "/client-metadata")',
47
+ 'ClientID must contain a path component (e.g. "/client-metadata.json")',
45
48
  )
46
49
  }
47
50
 
48
- if (url.pathname !== '/' && url.pathname.endsWith('/')) {
49
- throw new TypeError('ClientID must not end with a trailing slash')
51
+ if (url.pathname.endsWith('/')) {
52
+ throw new TypeError('ClientID path must not end with a trailing slash')
50
53
  }
51
54
 
52
- if (url.pathname.includes('//')) {
53
- throw new TypeError(
54
- `ClientID must not contain any double slashes in its path`,
55
- )
55
+ if (isHostnameIP(url.hostname)) {
56
+ throw new TypeError('ClientID hostname must not be an IP address')
56
57
  }
57
58
 
58
- // Note: Query string is allowed
59
- // Note: no restriction on the port for non-loopback URIs
60
-
61
- if (isIP(url.hostname)) {
62
- throw new TypeError('ClientID must not be an IP address')
59
+ // URL constructor normalizes the URL, so we extract the path manually to
60
+ // avoid normalization, then compare it to the normalized path to ensure
61
+ // that the URL does not contain path traversal or other unexpected characters
62
+ if (extractUrlPath(clientId) !== url.pathname) {
63
+ throw new TypeError(
64
+ `ClientID must be in canonical form ("${url.href}", got "${clientId}")`,
65
+ )
63
66
  }
64
67
 
65
68
  return url
@@ -1,12 +1,15 @@
1
- import { parseOAuthClientIdUrl } from './oauth-client-id-url.js'
2
1
  import { OAuthClientId } from './oauth-client-id.js'
2
+ import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
3
+ import { isLoopbackHost, safeUrl } from './util.js'
4
+
5
+ const OAUTH_CLIENT_ID_LOOPBACK_URL = 'http://localhost'
3
6
 
4
7
  export type OAuthClientIdLoopback = OAuthClientId &
5
- `http://localhost${'' | `${'/' | '?' | '#'}${string}`}`
8
+ `${typeof OAUTH_CLIENT_ID_LOOPBACK_URL}${'' | '/'}${'' | `?${string}`}`
6
9
 
7
- export function isOAuthClientIdLoopback<C extends OAuthClientId>(
8
- clientId: C,
9
- ): clientId is C & OAuthClientIdLoopback {
10
+ export function isOAuthClientIdLoopback(
11
+ clientId: string,
12
+ ): clientId is OAuthClientIdLoopback {
10
13
  try {
11
14
  parseOAuthLoopbackClientId(clientId)
12
15
  return true
@@ -15,44 +18,89 @@ export function isOAuthClientIdLoopback<C extends OAuthClientId>(
15
18
  }
16
19
  }
17
20
 
18
- export function parseOAuthLoopbackClientId(clientId: OAuthClientId): URL {
19
- const url = parseOAuthClientIdUrl(clientId)
20
-
21
- // Optimization: cheap checks first
21
+ export function assertOAuthLoopbackClientId(
22
+ clientId: string,
23
+ ): asserts clientId is OAuthClientIdLoopback {
24
+ void parseOAuthLoopbackClientId(clientId)
25
+ }
22
26
 
23
- if (url.protocol !== 'http:') {
24
- throw new TypeError('Loopback ClientID must use the "http:" protocol')
27
+ // @TODO: should we turn this into a zod schema? (more coherent error with other
28
+ // validation functions)
29
+ export function parseOAuthLoopbackClientId(clientId: string): {
30
+ scope?: OAuthScope
31
+ redirect_uris?: [string, ...string[]]
32
+ } {
33
+ if (!clientId.startsWith(OAUTH_CLIENT_ID_LOOPBACK_URL)) {
34
+ throw new TypeError(
35
+ `Loopback ClientID must start with "${OAUTH_CLIENT_ID_LOOPBACK_URL}"`,
36
+ )
37
+ } else if (clientId.includes('#', OAUTH_CLIENT_ID_LOOPBACK_URL.length)) {
38
+ throw new TypeError('Loopback ClientID must not contain a hash component')
25
39
  }
26
40
 
27
- if (url.hostname !== 'localhost') {
28
- throw new TypeError('Loopback ClientID must use the "localhost" hostname')
41
+ const queryStringIdx =
42
+ clientId.length > OAUTH_CLIENT_ID_LOOPBACK_URL.length &&
43
+ clientId[OAUTH_CLIENT_ID_LOOPBACK_URL.length] === '/'
44
+ ? OAUTH_CLIENT_ID_LOOPBACK_URL.length + 1
45
+ : OAUTH_CLIENT_ID_LOOPBACK_URL.length
46
+
47
+ if (clientId.length === queryStringIdx) {
48
+ return {} // no query string to parse
29
49
  }
30
50
 
31
- if (url.hash) {
32
- throw new TypeError('Loopback ClientID must not contain a fragment')
51
+ if (clientId[queryStringIdx] !== '?') {
52
+ throw new TypeError('Loopback ClientID must not contain a path component')
33
53
  }
34
54
 
35
- if (url.username || url.password) {
36
- throw new TypeError('Loopback ClientID must not contain credentials')
55
+ const searchParams = new URLSearchParams(clientId.slice(queryStringIdx + 1))
56
+
57
+ for (const name of searchParams.keys()) {
58
+ if (name !== 'redirect_uri' && name !== 'scope') {
59
+ throw new TypeError(`Invalid query parameter "${name}" in client ID`)
60
+ }
37
61
  }
38
62
 
39
- if (url.port) {
40
- throw new TypeError('Loopback ClientID must not contain a port')
63
+ const scope = searchParams.get('scope') ?? undefined
64
+ if (scope != null) {
65
+ if (searchParams.getAll('scope').length > 1) {
66
+ throw new TypeError(
67
+ 'Loopback ClientID must contain at most one scope query parameter',
68
+ )
69
+ } else if (!oauthScopeSchema.safeParse(scope).success) {
70
+ throw new TypeError('Invalid scope query parameter in client ID')
71
+ }
41
72
  }
42
73
 
43
- // Note: url.pathname === '/' is allowed for loopback URIs
74
+ const redirect_uris = searchParams.has('redirect_uri')
75
+ ? (searchParams.getAll('redirect_uri') as [string, ...string[]])
76
+ : undefined
44
77
 
45
- if (url.pathname !== '/' && url.pathname.endsWith('/')) {
46
- throw new TypeError('Loopback ClientID must not end with a trailing slash')
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
+ }
47
100
  }
48
101
 
49
- if (url.pathname.includes('//')) {
50
- throw new TypeError(
51
- `Loopback ClientID must not contain any double slashes in its path`,
52
- )
102
+ return {
103
+ scope,
104
+ redirect_uris,
53
105
  }
54
-
55
- // Note: Query string is allowed
56
-
57
- return url
58
106
  }
@@ -5,6 +5,7 @@ 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
7
  import { oauthResponseTypeSchema } from './oauth-response-type.js'
8
+ import { oauthScopeSchema } from './oauth-scope.js'
8
9
 
9
10
  // https://openid.net/specs/openid-connect-registration-1_0.html
10
11
  // https://datatracker.ietf.org/doc/html/rfc7591
@@ -22,7 +23,7 @@ export const oauthClientMetadataSchema = z.object({
22
23
  // > If omitted, the default behavior is that the client will use only the
23
24
  // > "authorization_code" Grant Type.
24
25
  .default(['authorization_code']),
25
- scope: z.string().optional(),
26
+ scope: oauthScopeSchema.optional(),
26
27
  token_endpoint_auth_method: oauthEndpointAuthMethod
27
28
  .default('none')
28
29
  .optional(),
@@ -0,0 +1,3 @@
1
+ import { z } from 'zod'
2
+
3
+ export const oauthCodeChallengeMethodSchema = z.enum(['S256', 'plain'])
@@ -0,0 +1,23 @@
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
+ }
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
 
3
3
  export const oauthParResponseSchema = z.object({
4
4
  request_uri: z.string(),
5
+ expires_in: z.number().int().positive(),
5
6
  })
6
7
 
7
8
  export type OAuthParResponse = z.infer<typeof oauthParResponseSchema>
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod'
2
+
3
+ export const oauthPasswordGrantTokenRequestSchema = z.object({
4
+ grant_type: z.literal('password'),
5
+ username: z.string(),
6
+ password: z.string(),
7
+ })
8
+
9
+ export type OAuthPasswordGrantTokenRequest = z.infer<
10
+ typeof oauthPasswordGrantTokenRequestSchema
11
+ >
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod'
2
+ import { oauthClientIdSchema } from './oauth-client-id.js'
3
+ import { oauthRefreshTokenSchema } from './oauth-refresh-token.js'
4
+
5
+ export const oauthRefreshTokenGrantTokenRequestSchema = z.object({
6
+ grant_type: z.literal('refresh_token'),
7
+ refresh_token: oauthRefreshTokenSchema,
8
+ client_id: oauthClientIdSchema,
9
+ })
10
+
11
+ export type OAuthRefreshTokenGrantTokenRequest = z.infer<
12
+ typeof oauthRefreshTokenGrantTokenRequestSchema
13
+ >
@@ -0,0 +1,4 @@
1
+ import { z } from 'zod'
2
+
3
+ export const oauthRefreshTokenSchema = z.string().min(1)
4
+ export type OAuthRefreshToken = z.infer<typeof oauthRefreshTokenSchema>
@@ -0,0 +1,5 @@
1
+ import { z } from 'zod'
2
+
3
+ export const oauthRequestUriSchema = z.string()
4
+
5
+ export type OAuthRequestUri = z.infer<typeof oauthRequestUriSchema>