@atcute/oauth-types 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +48 -0
  3. package/dist/build-client-metadata.d.ts +168 -0
  4. package/dist/build-client-metadata.d.ts.map +1 -0
  5. package/dist/build-client-metadata.js +53 -0
  6. package/dist/build-client-metadata.js.map +1 -0
  7. package/dist/constants.d.ts +5 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/index.d.ts +31 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +37 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/schemas/atcute-confidential-client-metadata.d.ts +21 -0
  16. package/dist/schemas/atcute-confidential-client-metadata.d.ts.map +1 -0
  17. package/dist/schemas/atcute-confidential-client-metadata.js +112 -0
  18. package/dist/schemas/atcute-confidential-client-metadata.js.map +1 -0
  19. package/dist/schemas/atproto-authorization-server-metadata.d.ts +55 -0
  20. package/dist/schemas/atproto-authorization-server-metadata.d.ts.map +1 -0
  21. package/dist/schemas/atproto-authorization-server-metadata.js +25 -0
  22. package/dist/schemas/atproto-authorization-server-metadata.js.map +1 -0
  23. package/dist/schemas/atproto-oauth-scope.d.ts +8 -0
  24. package/dist/schemas/atproto-oauth-scope.d.ts.map +1 -0
  25. package/dist/schemas/atproto-oauth-scope.js +12 -0
  26. package/dist/schemas/atproto-oauth-scope.js.map +1 -0
  27. package/dist/schemas/atproto-oauth-token-response.d.ts +19 -0
  28. package/dist/schemas/atproto-oauth-token-response.d.ts.map +1 -0
  29. package/dist/schemas/atproto-oauth-token-response.js +16 -0
  30. package/dist/schemas/atproto-oauth-token-response.js.map +1 -0
  31. package/dist/schemas/atproto-protected-resource-metadata.d.ts +21 -0
  32. package/dist/schemas/atproto-protected-resource-metadata.d.ts.map +1 -0
  33. package/dist/schemas/atproto-protected-resource-metadata.js +18 -0
  34. package/dist/schemas/atproto-protected-resource-metadata.js.map +1 -0
  35. package/dist/schemas/jwk.d.ts +241 -0
  36. package/dist/schemas/jwk.d.ts.map +1 -0
  37. package/dist/schemas/jwk.js +138 -0
  38. package/dist/schemas/jwk.js.map +1 -0
  39. package/dist/schemas/jwks.d.ts +242 -0
  40. package/dist/schemas/jwks.d.ts.map +1 -0
  41. package/dist/schemas/jwks.js +34 -0
  42. package/dist/schemas/jwks.js.map +1 -0
  43. package/dist/schemas/oauth-authorization-details.d.ts +64 -0
  44. package/dist/schemas/oauth-authorization-details.d.ts.map +1 -0
  45. package/dist/schemas/oauth-authorization-details.js +37 -0
  46. package/dist/schemas/oauth-authorization-details.js.map +1 -0
  47. package/dist/schemas/oauth-authorization-server-metadata.d.ts +96 -0
  48. package/dist/schemas/oauth-authorization-server-metadata.d.ts.map +1 -0
  49. package/dist/schemas/oauth-authorization-server-metadata.js +81 -0
  50. package/dist/schemas/oauth-authorization-server-metadata.js.map +1 -0
  51. package/dist/schemas/oauth-client-id-discoverable.d.ts +6 -0
  52. package/dist/schemas/oauth-client-id-discoverable.d.ts.map +1 -0
  53. package/dist/schemas/oauth-client-id-discoverable.js +43 -0
  54. package/dist/schemas/oauth-client-id-discoverable.js.map +1 -0
  55. package/dist/schemas/oauth-client-id.d.ts +5 -0
  56. package/dist/schemas/oauth-client-id.d.ts.map +1 -0
  57. package/dist/schemas/oauth-client-id.js +4 -0
  58. package/dist/schemas/oauth-client-id.js.map +1 -0
  59. package/dist/schemas/oauth-client-metadata.d.ts +164 -0
  60. package/dist/schemas/oauth-client-metadata.d.ts.map +1 -0
  61. package/dist/schemas/oauth-client-metadata.js +74 -0
  62. package/dist/schemas/oauth-client-metadata.js.map +1 -0
  63. package/dist/schemas/oauth-code-challenge-method.d.ts +4 -0
  64. package/dist/schemas/oauth-code-challenge-method.d.ts.map +1 -0
  65. package/dist/schemas/oauth-code-challenge-method.js +3 -0
  66. package/dist/schemas/oauth-code-challenge-method.js.map +1 -0
  67. package/dist/schemas/oauth-endpoint-auth-method.d.ts +4 -0
  68. package/dist/schemas/oauth-endpoint-auth-method.d.ts.map +1 -0
  69. package/dist/schemas/oauth-endpoint-auth-method.js +3 -0
  70. package/dist/schemas/oauth-endpoint-auth-method.js.map +1 -0
  71. package/dist/schemas/oauth-grant-type.d.ts +4 -0
  72. package/dist/schemas/oauth-grant-type.d.ts.map +1 -0
  73. package/dist/schemas/oauth-grant-type.js +4 -0
  74. package/dist/schemas/oauth-grant-type.js.map +1 -0
  75. package/dist/schemas/oauth-issuer-identifier.d.ts +4 -0
  76. package/dist/schemas/oauth-issuer-identifier.d.ts.map +1 -0
  77. package/dist/schemas/oauth-issuer-identifier.js +21 -0
  78. package/dist/schemas/oauth-issuer-identifier.js.map +1 -0
  79. package/dist/schemas/oauth-par-response.d.ts +7 -0
  80. package/dist/schemas/oauth-par-response.d.ts.map +1 -0
  81. package/dist/schemas/oauth-par-response.js +7 -0
  82. package/dist/schemas/oauth-par-response.js.map +1 -0
  83. package/dist/schemas/oauth-prompt.d.ts +13 -0
  84. package/dist/schemas/oauth-prompt.d.ts.map +1 -0
  85. package/dist/schemas/oauth-prompt.js +12 -0
  86. package/dist/schemas/oauth-prompt.js.map +1 -0
  87. package/dist/schemas/oauth-protected-resource-metadata.d.ts +66 -0
  88. package/dist/schemas/oauth-protected-resource-metadata.d.ts.map +1 -0
  89. package/dist/schemas/oauth-protected-resource-metadata.js +71 -0
  90. package/dist/schemas/oauth-protected-resource-metadata.js.map +1 -0
  91. package/dist/schemas/oauth-redirect-uri.d.ts +20 -0
  92. package/dist/schemas/oauth-redirect-uri.d.ts.map +1 -0
  93. package/dist/schemas/oauth-redirect-uri.js +32 -0
  94. package/dist/schemas/oauth-redirect-uri.js.map +1 -0
  95. package/dist/schemas/oauth-response-mode.d.ts +4 -0
  96. package/dist/schemas/oauth-response-mode.d.ts.map +1 -0
  97. package/dist/schemas/oauth-response-mode.js +3 -0
  98. package/dist/schemas/oauth-response-mode.js.map +1 -0
  99. package/dist/schemas/oauth-response-type.d.ts +4 -0
  100. package/dist/schemas/oauth-response-type.d.ts.map +1 -0
  101. package/dist/schemas/oauth-response-type.js +8 -0
  102. package/dist/schemas/oauth-response-type.js.map +1 -0
  103. package/dist/schemas/oauth-scope.d.ts +12 -0
  104. package/dist/schemas/oauth-scope.d.ts.map +1 -0
  105. package/dist/schemas/oauth-scope.js +14 -0
  106. package/dist/schemas/oauth-scope.js.map +1 -0
  107. package/dist/schemas/oauth-token-response.d.ts +22 -0
  108. package/dist/schemas/oauth-token-response.d.ts.map +1 -0
  109. package/dist/schemas/oauth-token-response.js +19 -0
  110. package/dist/schemas/oauth-token-response.js.map +1 -0
  111. package/dist/schemas/oauth-token-type.d.ts +5 -0
  112. package/dist/schemas/oauth-token-type.d.ts.map +1 -0
  113. package/dist/schemas/oauth-token-type.js +13 -0
  114. package/dist/schemas/oauth-token-type.js.map +1 -0
  115. package/dist/schemas/uri.d.ts +18 -0
  116. package/dist/schemas/uri.d.ts.map +1 -0
  117. package/dist/schemas/uri.js +81 -0
  118. package/dist/schemas/uri.js.map +1 -0
  119. package/dist/schemas/utils.d.ts +32 -0
  120. package/dist/schemas/utils.d.ts.map +1 -0
  121. package/dist/schemas/utils.js +94 -0
  122. package/dist/schemas/utils.js.map +1 -0
  123. package/dist/scope.d.ts +84 -0
  124. package/dist/scope.d.ts.map +1 -0
  125. package/dist/scope.js +102 -0
  126. package/dist/scope.js.map +1 -0
  127. package/lib/build-client-metadata.ts +72 -0
  128. package/lib/constants.ts +5 -0
  129. package/lib/index.ts +116 -0
  130. package/lib/schemas/atcute-confidential-client-metadata.ts +139 -0
  131. package/lib/schemas/atproto-authorization-server-metadata.ts +32 -0
  132. package/lib/schemas/atproto-oauth-scope.ts +18 -0
  133. package/lib/schemas/atproto-oauth-token-response.ts +20 -0
  134. package/lib/schemas/atproto-protected-resource-metadata.ts +24 -0
  135. package/lib/schemas/jwk.ts +189 -0
  136. package/lib/schemas/jwks.ts +45 -0
  137. package/lib/schemas/oauth-authorization-details.ts +43 -0
  138. package/lib/schemas/oauth-authorization-server-metadata.ts +101 -0
  139. package/lib/schemas/oauth-client-id-discoverable.ts +53 -0
  140. package/lib/schemas/oauth-client-id.ts +6 -0
  141. package/lib/schemas/oauth-client-metadata.ts +83 -0
  142. package/lib/schemas/oauth-code-challenge-method.ts +5 -0
  143. package/lib/schemas/oauth-endpoint-auth-method.ts +13 -0
  144. package/lib/schemas/oauth-grant-type.ts +13 -0
  145. package/lib/schemas/oauth-issuer-identifier.ts +30 -0
  146. package/lib/schemas/oauth-par-response.ts +10 -0
  147. package/lib/schemas/oauth-prompt.ts +20 -0
  148. package/lib/schemas/oauth-protected-resource-metadata.ts +89 -0
  149. package/lib/schemas/oauth-redirect-uri.ts +42 -0
  150. package/lib/schemas/oauth-response-mode.ts +9 -0
  151. package/lib/schemas/oauth-response-type.ts +17 -0
  152. package/lib/schemas/oauth-scope.ts +18 -0
  153. package/lib/schemas/oauth-token-response.ts +22 -0
  154. package/lib/schemas/oauth-token-type.ts +15 -0
  155. package/lib/schemas/uri.ts +100 -0
  156. package/lib/schemas/utils.ts +113 -0
  157. package/lib/scope.ts +187 -0
  158. package/package.json +38 -0
@@ -0,0 +1,43 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { urlSchema } from './uri.js';
4
+
5
+ /**
6
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc9396#section-2 | RFC 9396, Section 2}
7
+ */
8
+ export const oauthAuthorizationDetailSchema = v.object({
9
+ type: v.string(),
10
+ /**
11
+ * an array of strings representing the location of the resource or RS. these
12
+ * strings are typically URIs identifying the location of the RS.
13
+ */
14
+ locations: v.array(urlSchema).optional(),
15
+ /**
16
+ * an array of strings representing the kinds of actions to be taken at the
17
+ * resource.
18
+ */
19
+ actions: v.array(v.string()).optional(),
20
+ /**
21
+ * an array of strings representing the kinds of data being requested from the
22
+ * resource.
23
+ */
24
+ datatypes: v.array(v.string()).optional(),
25
+ /**
26
+ * a string identifier indicating a specific resource available at the API.
27
+ */
28
+ identifier: v.string().optional(),
29
+ /**
30
+ * an array of strings representing the types or levels of privilege being
31
+ * requested at the resource.
32
+ */
33
+ privileges: v.array(v.string()).optional(),
34
+ });
35
+
36
+ export type OAuthAuthorizationDetail = v.Infer<typeof oauthAuthorizationDetailSchema>;
37
+
38
+ /**
39
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc9396#section-2 | RFC 9396, Section 2}
40
+ */
41
+ export const oauthAuthorizationDetailsSchema = v.array(oauthAuthorizationDetailSchema);
42
+
43
+ export type OAuthAuthorizationDetails = v.Infer<typeof oauthAuthorizationDetailsSchema>;
@@ -0,0 +1,101 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js';
4
+ import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js';
5
+ import { oauthPromptSchema } from './oauth-prompt.js';
6
+ import { webUriSchema } from './uri.js';
7
+
8
+ /**
9
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
10
+ */
11
+ export const oauthAuthorizationServerMetadataSchema = v.object({
12
+ issuer: oauthIssuerIdentifierSchema,
13
+
14
+ claims_supported: v.array(v.string()).optional(),
15
+ claims_locales_supported: v.array(v.string()).optional(),
16
+ claims_parameter_supported: v.boolean().optional(),
17
+ request_parameter_supported: v.boolean().optional(),
18
+ request_uri_parameter_supported: v.boolean().optional(),
19
+ require_request_uri_registration: v.boolean().optional(),
20
+ scopes_supported: v.array(v.string()).optional(),
21
+ subject_types_supported: v.array(v.string()).optional(),
22
+ response_types_supported: v.array(v.string()).optional(),
23
+ response_modes_supported: v.array(v.string()).optional(),
24
+ grant_types_supported: v.array(v.string()).optional(),
25
+ code_challenge_methods_supported: v.array(oauthCodeChallengeMethodSchema).optional(),
26
+ ui_locales_supported: v.array(v.string()).optional(),
27
+ id_token_signing_alg_values_supported: v.array(v.string()).optional(),
28
+ display_values_supported: v.array(v.string()).optional(),
29
+ prompt_values_supported: v.array(oauthPromptSchema).optional(),
30
+ request_object_signing_alg_values_supported: v.array(v.string()).optional(),
31
+ authorization_response_iss_parameter_supported: v.boolean().optional(),
32
+ authorization_details_types_supported: v.array(v.string()).optional(),
33
+ request_object_encryption_alg_values_supported: v.array(v.string()).optional(),
34
+ request_object_encryption_enc_values_supported: v.array(v.string()).optional(),
35
+
36
+ jwks_uri: webUriSchema.optional(),
37
+
38
+ authorization_endpoint: webUriSchema,
39
+
40
+ token_endpoint: webUriSchema,
41
+ // https://www.rfc-editor.org/rfc/rfc8414.html#section-2
42
+ token_endpoint_auth_methods_supported: v.array(v.string()).optional(),
43
+ token_endpoint_auth_signing_alg_values_supported: v.array(v.string()).optional(),
44
+
45
+ revocation_endpoint: webUriSchema.optional(),
46
+ revocation_endpoint_auth_methods_supported: v.array(v.string()).optional(),
47
+ revocation_endpoint_auth_signing_alg_values_supported: v.array(v.string()).optional(),
48
+
49
+ introspection_endpoint: webUriSchema.optional(),
50
+ introspection_endpoint_auth_methods_supported: v.array(v.string()).optional(),
51
+ introspection_endpoint_auth_signing_alg_values_supported: v.array(v.string()).optional(),
52
+
53
+ pushed_authorization_request_endpoint: webUriSchema.optional(),
54
+ pushed_authorization_request_endpoint_auth_methods_supported: v.array(v.string()).optional(),
55
+ pushed_authorization_request_endpoint_auth_signing_alg_values_supported: v.array(v.string()).optional(),
56
+ require_pushed_authorization_requests: v.boolean().optional(),
57
+
58
+ userinfo_endpoint: webUriSchema.optional(),
59
+ end_session_endpoint: webUriSchema.optional(),
60
+ registration_endpoint: webUriSchema.optional(),
61
+
62
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-5.1
63
+ dpop_signing_alg_values_supported: v.array(v.string()).optional(),
64
+
65
+ // https://www.rfc-editor.org/rfc/rfc9728.html#section-4
66
+ protected_resources: v.array(webUriSchema).optional(),
67
+
68
+ // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
69
+ client_id_metadata_document_supported: v.boolean().optional(),
70
+ });
71
+
72
+ export type OAuthAuthorizationServerMetadata = v.Infer<typeof oauthAuthorizationServerMetadataSchema>;
73
+
74
+ export const oauthAuthorizationServerMetadataValidator = oauthAuthorizationServerMetadataSchema.chain(
75
+ (data) => {
76
+ if (data.require_pushed_authorization_requests && !data.pushed_authorization_request_endpoint) {
77
+ return v.err({
78
+ message: `"pushed_authorization_request_endpoint" required when "require_pushed_authorization_requests" is true`,
79
+ path: ['pushed_authorization_request_endpoint'],
80
+ });
81
+ }
82
+
83
+ if (data.response_types_supported && !data.response_types_supported.includes('code')) {
84
+ return v.err({
85
+ message: `response type "code" is required`,
86
+ path: ['response_types_supported'],
87
+ });
88
+ }
89
+
90
+ if (data.token_endpoint_auth_signing_alg_values_supported?.includes('none')) {
91
+ // https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
92
+ // > The value `none` MUST NOT be used.
93
+ return v.err({
94
+ message: `client authentication method "none" is not allowed`,
95
+ path: ['token_endpoint_auth_signing_alg_values_supported'],
96
+ });
97
+ }
98
+
99
+ return v.ok(data);
100
+ },
101
+ );
@@ -0,0 +1,53 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { oauthClientIdSchema } from './oauth-client-id.js';
4
+ import { httpsUriSchema } from './uri.js';
5
+ import { extractUrlPath, isHostnameIP } from './utils.js';
6
+
7
+ /**
8
+ * @see {@link https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html}
9
+ */
10
+ export const oauthClientIdDiscoverableSchema = v.string().chain((input, options) => {
11
+ // first validate as base client ID
12
+ const clientIdResult = oauthClientIdSchema.try(input, options);
13
+ if (!clientIdResult.ok) {
14
+ return clientIdResult;
15
+ }
16
+
17
+ // then validate as https URI
18
+ const httpsResult = httpsUriSchema.try(input, options);
19
+ if (!httpsResult.ok) {
20
+ return httpsResult;
21
+ }
22
+
23
+ const url = new URL(input);
24
+
25
+ if (url.username || url.password) {
26
+ return v.err(`client ID must not contain credentials`);
27
+ }
28
+
29
+ if (url.hash) {
30
+ return v.err(`client ID must not contain a fragment`);
31
+ }
32
+
33
+ if (url.pathname === '/') {
34
+ return v.err(`client ID must contain a path component (e.g. "/client-metadata.json")`);
35
+ }
36
+
37
+ if (url.pathname.endsWith('/')) {
38
+ return v.err(`client ID path must not end with a trailing slash`);
39
+ }
40
+
41
+ if (isHostnameIP(url.hostname)) {
42
+ return v.err(`client ID hostname must not be an IP address`);
43
+ }
44
+
45
+ // URL constructor normalizes the URL, so we extract the path manually to
46
+ // avoid normalization, then compare it to the normalized path to ensure
47
+ // that the URL does not contain path traversal or other unexpected characters
48
+ if (extractUrlPath(input) !== url.pathname) {
49
+ return v.err(`client ID must be in canonical form ("${url.href}", got "${input}")`);
50
+ }
51
+
52
+ return v.ok(input);
53
+ });
@@ -0,0 +1,6 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ /** base OAuth client ID (any non-empty string) */
4
+ export const oauthClientIdSchema = v.string().assert((input) => input.length > 0, `must not be empty`);
5
+
6
+ export type OAuthClientId = v.Infer<typeof oauthClientIdSchema>;
@@ -0,0 +1,83 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { jwksPubSchema } from './jwks.js';
4
+ import { oauthClientIdSchema } from './oauth-client-id.js';
5
+ import { oauthEndpointAuthMethodSchema } from './oauth-endpoint-auth-method.js';
6
+ import { oauthGrantTypeSchema } from './oauth-grant-type.js';
7
+ import { oauthRedirectUriSchema } from './oauth-redirect-uri.js';
8
+ import { oauthResponseTypeSchema } from './oauth-response-type.js';
9
+ import { oauthScopeSchema } from './oauth-scope.js';
10
+ import { webUriSchema } from './uri.js';
11
+
12
+ const oauthApplicationTypeSchema = v.union(v.literal('web'), v.literal('native'));
13
+
14
+ const oauthSubjectTypeSchema = v.union(v.literal('public'), v.literal('pairwise'));
15
+
16
+ // simple email validation
17
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18
+
19
+ /**
20
+ * base OAuth client metadata schema.
21
+ *
22
+ * @see {@link https://openid.net/specs/openid-connect-registration-1_0.html}
23
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7591}
24
+ */
25
+ export const oauthClientMetadataSchema = v.object({
26
+ // https://www.rfc-editor.org/rfc/rfc7591.html#section-2
27
+ redirect_uris: v
28
+ .array(oauthRedirectUriSchema)
29
+ .assert((arr) => arr.length > 0, `must have at least one redirect URI`),
30
+ response_types: v.array(oauthResponseTypeSchema).optional(),
31
+ // > If omitted, the default is that the client will use only the "code"
32
+ // > response type.
33
+ // .optional((): OAuthResponseType[] => ['code'])
34
+ grant_types: v.array(oauthGrantTypeSchema).optional(),
35
+ // > If omitted, the default behavior is that the client will use only the
36
+ // > "authorization_code" Grant Type.
37
+ // .optional((): OAuthGrantType[] => ['authorization_code']),
38
+ scope: oauthScopeSchema.optional(),
39
+ // https://www.rfc-editor.org/rfc/rfc7591.html#section-2
40
+ token_endpoint_auth_method: oauthEndpointAuthMethodSchema.optional(),
41
+ // > If unspecified or omitted, the default is "client_secret_basic" [...].
42
+ // .optional((): OAuthEndpointAuthMethod => 'client_secret_basic'),
43
+ token_endpoint_auth_signing_alg: v.string().optional(),
44
+ userinfo_signed_response_alg: v.string().optional(),
45
+ userinfo_encrypted_response_alg: v.string().optional(),
46
+ jwks_uri: webUriSchema.optional(),
47
+ jwks: jwksPubSchema.optional(),
48
+ application_type: oauthApplicationTypeSchema.optional(),
49
+ // .optional((): OAuthApplicationType => 'web'),
50
+ subject_type: oauthSubjectTypeSchema.optional(),
51
+ // .optional((): OAuthSubjectType => 'public'),
52
+ request_object_signing_alg: v.string().optional(),
53
+ id_token_signed_response_alg: v.string().optional(),
54
+ authorization_signed_response_alg: v.string().optional(),
55
+ authorization_encrypted_response_enc: v.literal('A128CBC-HS256').optional(),
56
+ authorization_encrypted_response_alg: v.string().optional(),
57
+ client_id: oauthClientIdSchema.optional(),
58
+ client_name: v.string().optional(),
59
+ client_uri: webUriSchema.optional(),
60
+ policy_uri: webUriSchema.optional(),
61
+ tos_uri: webUriSchema.optional(),
62
+ logo_uri: webUriSchema.optional(),
63
+
64
+ /**
65
+ * default Maximum Authentication Age. specifies that the End-User MUST be
66
+ * actively authenticated if the End-User was authenticated longer ago than
67
+ * the specified number of seconds. the max_age request parameter overrides
68
+ * this default value. if omitted, no default Maximum Authentication Age is
69
+ * specified.
70
+ */
71
+ default_max_age: v.number().optional(),
72
+ require_auth_time: v.boolean().optional(),
73
+ contacts: v.array(v.string().assert((s) => EMAIL_RE.test(s), `must be a valid email`)).optional(),
74
+ tls_client_certificate_bound_access_tokens: v.boolean().optional(),
75
+
76
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-5.2
77
+ dpop_bound_access_tokens: v.boolean().optional(),
78
+
79
+ // https://datatracker.ietf.org/doc/html/rfc9396#section-14.5
80
+ authorization_details_types: v.array(v.string()).optional(),
81
+ });
82
+
83
+ export type OAuthClientMetadata = v.Infer<typeof oauthClientMetadataSchema>;
@@ -0,0 +1,5 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ export const oauthCodeChallengeMethodSchema = v.union(v.literal('S256'), v.literal('plain'));
4
+
5
+ export type OAuthCodeChallengeMethod = v.Infer<typeof oauthCodeChallengeMethodSchema>;
@@ -0,0 +1,13 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ export const oauthEndpointAuthMethodSchema = v.union(
4
+ v.literal('client_secret_basic'),
5
+ v.literal('client_secret_jwt'),
6
+ v.literal('client_secret_post'),
7
+ v.literal('none'),
8
+ v.literal('private_key_jwt'),
9
+ v.literal('self_signed_tls_client_auth'),
10
+ v.literal('tls_client_auth'),
11
+ );
12
+
13
+ export type OAuthEndpointAuthMethod = v.Infer<typeof oauthEndpointAuthMethodSchema>;
@@ -0,0 +1,13 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ export const oauthGrantTypeSchema = v.union(
4
+ v.literal('authorization_code'),
5
+ v.literal('implicit'),
6
+ v.literal('refresh_token'),
7
+ v.literal('password'), // not part of OAuth 2.1
8
+ v.literal('client_credentials'),
9
+ v.literal('urn:ietf:params:oauth:grant-type:jwt-bearer'),
10
+ v.literal('urn:ietf:params:oauth:grant-type:saml2-bearer'),
11
+ );
12
+
13
+ export type OAuthGrantType = v.Infer<typeof oauthGrantTypeSchema>;
@@ -0,0 +1,30 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { webUriSchema } from './uri.js';
4
+
5
+ export const oauthIssuerIdentifierSchema = webUriSchema.chain((input) => {
6
+ // validate the issuer (MIX-UP attacks)
7
+
8
+ if (input.endsWith('/')) {
9
+ return v.err(`issuer URL must not end with a slash`);
10
+ }
11
+
12
+ const url = new URL(input);
13
+
14
+ if (url.username || url.password) {
15
+ return v.err(`issuer URL must not contain a username or password`);
16
+ }
17
+
18
+ if (url.hash || url.search) {
19
+ return v.err(`issuer URL must not contain a query or fragment`);
20
+ }
21
+
22
+ const canonicalValue = url.pathname === '/' ? url.origin : url.href;
23
+ if (input !== canonicalValue) {
24
+ return v.err(`issuer URL must be in the canonical form`);
25
+ }
26
+
27
+ return v.ok(input);
28
+ });
29
+
30
+ export type OAuthIssuerIdentifier = v.Infer<typeof oauthIssuerIdentifierSchema>;
@@ -0,0 +1,10 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ const isPositiveInteger = (n: number): boolean => Number.isInteger(n) && n > 0;
4
+
5
+ export const oauthParResponseSchema = v.object({
6
+ request_uri: v.string(),
7
+ expires_in: v.number().assert(isPositiveInteger, `must be a positive integer`),
8
+ });
9
+
10
+ export type OAuthParResponse = v.Infer<typeof oauthParResponseSchema>;
@@ -0,0 +1,20 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ /**
4
+ * OAuth prompt mode values.
5
+ *
6
+ * - `none`: only succeed if user already authorized this client on this device
7
+ * - `login`: force re-authentication
8
+ * - `consent`: force re-consent
9
+ * - `select_account`: force account selection
10
+ * - `create`: force user registration screen
11
+ */
12
+ export const oauthPromptSchema = v.union(
13
+ v.literal('none'),
14
+ v.literal('login'),
15
+ v.literal('consent'),
16
+ v.literal('select_account'),
17
+ v.literal('create'),
18
+ );
19
+
20
+ export type OAuthPrompt = v.Infer<typeof oauthPromptSchema>;
@@ -0,0 +1,89 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js';
4
+ import { webUriSchema } from './uri.js';
5
+
6
+ export const oauthBearerMethodSchema = v.union(v.literal('header'), v.literal('body'), v.literal('query'));
7
+
8
+ export type OAuthBearerMethod = v.Infer<typeof oauthBearerMethodSchema>;
9
+
10
+ /**
11
+ * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-3.2}
12
+ */
13
+ export const oauthProtectedResourceMetadataSchema = v.object({
14
+ /**
15
+ * REQUIRED. the protected resource's resource identifier, which is a URL that
16
+ * uses the https scheme and has no query or fragment components.
17
+ */
18
+ resource: webUriSchema,
19
+
20
+ /**
21
+ * OPTIONAL. JSON array containing a list of OAuth authorization server issuer
22
+ * identifiers, as defined in RFC8414, for authorization servers that can be
23
+ * used with this protected resource.
24
+ */
25
+ authorization_servers: v.array(oauthIssuerIdentifierSchema).optional(),
26
+
27
+ /**
28
+ * OPTIONAL. URL of the protected resource's JWK Set document.
29
+ */
30
+ jwks_uri: webUriSchema.optional(),
31
+
32
+ /**
33
+ * RECOMMENDED. JSON array containing a list of the OAuth 2.0 scope values that
34
+ * are used in authorization requests to request access to this protected resource.
35
+ */
36
+ scopes_supported: v.array(v.string()).optional(),
37
+
38
+ /**
39
+ * OPTIONAL. JSON array containing a list of the supported methods of sending
40
+ * an OAuth 2.0 Bearer Token to the protected resource.
41
+ */
42
+ bearer_methods_supported: v.array(oauthBearerMethodSchema).optional(),
43
+
44
+ /**
45
+ * OPTIONAL. JSON array containing a list of the JWS signing algorithms
46
+ * supported by the protected resource for signing resource responses.
47
+ */
48
+ resource_signing_alg_values_supported: v.array(v.string()).optional(),
49
+
50
+ /**
51
+ * OPTIONAL. URL of a page containing human-readable information that
52
+ * developers might want or need to know when using the protected resource.
53
+ */
54
+ resource_documentation: webUriSchema.optional(),
55
+
56
+ /**
57
+ * OPTIONAL. URL that the protected resource provides to read about the
58
+ * protected resource's requirements on how the client can use the data.
59
+ */
60
+ resource_policy_uri: webUriSchema.optional(),
61
+
62
+ /**
63
+ * OPTIONAL. URL that the protected resource provides to read about the
64
+ * protected resource's terms of service.
65
+ */
66
+ resource_tos_uri: webUriSchema.optional(),
67
+ });
68
+
69
+ export const oauthProtectedResourceMetadataValidator = oauthProtectedResourceMetadataSchema.chain((data) => {
70
+ const url = new URL(data.resource);
71
+
72
+ if (url.search) {
73
+ return v.err({
74
+ message: `resource URL must not contain query parameters`,
75
+ path: ['resource'],
76
+ });
77
+ }
78
+
79
+ if (url.hash) {
80
+ return v.err({
81
+ message: `resource URL must not contain a fragment`,
82
+ path: ['resource'],
83
+ });
84
+ }
85
+
86
+ return v.ok(data);
87
+ });
88
+
89
+ export type OAuthProtectedResourceMetadata = v.Infer<typeof oauthProtectedResourceMetadataSchema>;
@@ -0,0 +1,42 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { httpsUriSchema, loopbackUriSchema, privateUseUriSchema } from './uri.js';
4
+
5
+ /**
6
+ * this is a loopback URI with the additional restriction that the hostname
7
+ * `localhost` is not allowed.
8
+ *
9
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-8.3 Loopback Redirect Considerations} RFC8252
10
+ *
11
+ * > While redirect URIs using localhost (i.e.,
12
+ * > "http://localhost:{port}/{path}") function similarly to loopback IP
13
+ * > redirects described in Section 7.3, the use of localhost is NOT
14
+ * > RECOMMENDED. Specifying a redirect URI with the loopback IP literal rather
15
+ * > than localhost avoids inadvertently listening on network interfaces other
16
+ * > than the loopback interface. It is also less susceptible to client-side
17
+ * > firewalls and misconfigured host name resolution on the user's device.
18
+ */
19
+ export const loopbackRedirectUriSchema = loopbackUriSchema.chain((input) => {
20
+ if (input.startsWith('http://localhost')) {
21
+ return v.err(
22
+ `use of "localhost" hostname is not allowed (RFC 8252), use a loopback IP such as "127.0.0.1" instead`,
23
+ );
24
+ }
25
+ return v.ok(input);
26
+ });
27
+
28
+ export type LoopbackRedirectUri = v.Infer<typeof loopbackRedirectUriSchema>;
29
+
30
+ export const oauthRedirectUriSchema = v.string().chain((input, options) => {
31
+ if (input.startsWith('http://')) {
32
+ return loopbackRedirectUriSchema.try(input, options);
33
+ }
34
+
35
+ if (input.startsWith('https://')) {
36
+ return httpsUriSchema.try(input, options);
37
+ }
38
+
39
+ return privateUseUriSchema.try(input, options);
40
+ });
41
+
42
+ export type OAuthRedirectUri = v.Infer<typeof oauthRedirectUriSchema>;
@@ -0,0 +1,9 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ export const oauthResponseModeSchema = v.union(
4
+ v.literal('query'),
5
+ v.literal('fragment'),
6
+ v.literal('form_post'),
7
+ );
8
+
9
+ export type OAuthResponseMode = v.Infer<typeof oauthResponseModeSchema>;
@@ -0,0 +1,17 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ export const oauthResponseTypeSchema = v.union(
4
+ // OAuth2 (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-4.1.1)
5
+ v.literal('code'), // Authorization Code Grant
6
+ v.literal('token'), // Implicit Grant
7
+
8
+ // OIDC (https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)
9
+ v.literal('none'),
10
+ v.literal('code id_token token'),
11
+ v.literal('code id_token'),
12
+ v.literal('code token'),
13
+ v.literal('id_token token'),
14
+ v.literal('id_token'),
15
+ );
16
+
17
+ export type OAuthResponseType = v.Infer<typeof oauthResponseTypeSchema>;
@@ -0,0 +1,18 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ // scope = scope-token *( SP scope-token )
4
+ // scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
5
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1
6
+ export const OAUTH_SCOPE_REGEXP = /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/;
7
+
8
+ export const isOAuthScope = (input: string): boolean => OAUTH_SCOPE_REGEXP.test(input);
9
+
10
+ /**
11
+ * a (single) space separated list of non empty printable ASCII char string
12
+ * (except backslash and double quote).
13
+ *
14
+ * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-1.4.1}
15
+ */
16
+ export const oauthScopeSchema = v.string().assert(isOAuthScope, `invalid OAuth scope`);
17
+
18
+ export type OAuthScope = v.Infer<typeof oauthScopeSchema>;
@@ -0,0 +1,22 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js';
4
+ import { oauthTokenTypeSchema } from './oauth-token-type.js';
5
+
6
+ /**
7
+ * @see {@link https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1 | RFC 6749 (OAuth2), Section 5.1}
8
+ */
9
+ export const oauthTokenResponseSchema = v.object({
10
+ // https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
11
+ access_token: v.string(),
12
+ token_type: oauthTokenTypeSchema,
13
+ scope: v.string().optional(),
14
+ refresh_token: v.string().optional(),
15
+ expires_in: v.number().optional(),
16
+ // https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
17
+ id_token: v.string().optional(),
18
+ // https://datatracker.ietf.org/doc/html/rfc9396#name-enriched-authorization-deta
19
+ authorization_details: oauthAuthorizationDetailsSchema.optional(),
20
+ });
21
+
22
+ export type OAuthTokenResponse = v.Infer<typeof oauthTokenResponseSchema>;
@@ -0,0 +1,15 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ /** token type (case-insensitive input, normalized output) */
4
+ export const oauthTokenTypeSchema = v.string().chain((input) => {
5
+ const lower = input.toLowerCase();
6
+ if (lower === 'dpop') {
7
+ return v.ok('DPoP');
8
+ }
9
+ if (lower === 'bearer') {
10
+ return v.ok('Bearer');
11
+ }
12
+ return v.err(`must be "DPoP" or "Bearer"`);
13
+ });
14
+
15
+ export type OAuthTokenType = v.Infer<typeof oauthTokenTypeSchema>;