@atproto/oauth-types 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/atproto-loopback-client-metadata.d.ts +4 -1
  3. package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
  4. package/dist/atproto-loopback-client-metadata.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/oauth-authorization-code-grant-token-request.d.ts +2 -2
  10. package/dist/oauth-authorization-code-grant-token-request.d.ts.map +1 -1
  11. package/dist/oauth-authorization-code-grant-token-request.js +2 -1
  12. package/dist/oauth-authorization-code-grant-token-request.js.map +1 -1
  13. package/dist/oauth-authorization-details.d.ts +42 -4
  14. package/dist/oauth-authorization-details.d.ts.map +1 -1
  15. package/dist/oauth-authorization-details.js +21 -1
  16. package/dist/oauth-authorization-details.js.map +1 -1
  17. package/dist/oauth-authorization-request-jar.d.ts +1 -1
  18. package/dist/oauth-authorization-request-par.d.ts +8 -8
  19. package/dist/oauth-authorization-request-parameters.d.ts +7 -7
  20. package/dist/oauth-authorization-request-parameters.d.ts.map +1 -1
  21. package/dist/oauth-authorization-request-parameters.js +2 -1
  22. package/dist/oauth-authorization-request-parameters.js.map +1 -1
  23. package/dist/oauth-authorization-request-query.d.ts +8 -8
  24. package/dist/oauth-authorization-server-metadata.d.ts +69 -66
  25. package/dist/oauth-authorization-server-metadata.d.ts.map +1 -1
  26. package/dist/oauth-authorization-server-metadata.js +14 -10
  27. package/dist/oauth-authorization-server-metadata.js.map +1 -1
  28. package/dist/oauth-client-id-discoverable.d.ts +3 -2
  29. package/dist/oauth-client-id-discoverable.d.ts.map +1 -1
  30. package/dist/oauth-client-id-discoverable.js +52 -28
  31. package/dist/oauth-client-id-discoverable.js.map +1 -1
  32. package/dist/oauth-client-id-loopback.d.ts +5 -5
  33. package/dist/oauth-client-id-loopback.d.ts.map +1 -1
  34. package/dist/oauth-client-id-loopback.js +29 -27
  35. package/dist/oauth-client-id-loopback.js.map +1 -1
  36. package/dist/oauth-client-metadata.d.ts +22 -12
  37. package/dist/oauth-client-metadata.d.ts.map +1 -1
  38. package/dist/oauth-client-metadata.js +18 -8
  39. package/dist/oauth-client-metadata.js.map +1 -1
  40. package/dist/oauth-issuer-identifier.d.ts +1 -1
  41. package/dist/oauth-issuer-identifier.d.ts.map +1 -1
  42. package/dist/oauth-issuer-identifier.js +3 -19
  43. package/dist/oauth-issuer-identifier.js.map +1 -1
  44. package/dist/oauth-protected-resource-metadata.d.ts +15 -12
  45. package/dist/oauth-protected-resource-metadata.d.ts.map +1 -1
  46. package/dist/oauth-protected-resource-metadata.js +15 -5
  47. package/dist/oauth-protected-resource-metadata.js.map +1 -1
  48. package/dist/oauth-redirect-uri.d.ts +10 -0
  49. package/dist/oauth-redirect-uri.d.ts.map +1 -0
  50. package/dist/oauth-redirect-uri.js +35 -0
  51. package/dist/oauth-redirect-uri.js.map +1 -0
  52. package/dist/oauth-token-request.d.ts +2 -2
  53. package/dist/oauth-token-response.d.ts +6 -6
  54. package/dist/uri.d.ts +20 -0
  55. package/dist/uri.d.ts.map +1 -0
  56. package/dist/uri.js +127 -0
  57. package/dist/uri.js.map +1 -0
  58. package/package.json +1 -1
  59. package/src/atproto-loopback-client-metadata.ts +8 -3
  60. package/src/index.ts +2 -0
  61. package/src/oauth-authorization-code-grant-token-request.ts +2 -1
  62. package/src/oauth-authorization-details.ts +21 -1
  63. package/src/oauth-authorization-request-parameters.ts +2 -1
  64. package/src/oauth-authorization-server-metadata.ts +14 -10
  65. package/src/oauth-client-id-discoverable.ts +69 -51
  66. package/src/oauth-client-id-loopback.ts +40 -40
  67. package/src/oauth-client-metadata.ts +18 -8
  68. package/src/oauth-issuer-identifier.ts +6 -21
  69. package/src/oauth-protected-resource-metadata.ts +15 -5
  70. package/src/oauth-redirect-uri.ts +56 -0
  71. package/src/uri.ts +171 -0
  72. package/tsconfig.build.tsbuildinfo +1 -1
package/dist/uri.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { TypeOf, z } from 'zod';
2
+ /**
3
+ * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
4
+ *
5
+ * Any value that matches this schema is safe to parse using `new URL()`.
6
+ */
7
+ export declare const dangerousUriSchema: z.ZodEffects<z.ZodString, `${string}:${string}`, string>;
8
+ /**
9
+ * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
10
+ */
11
+ export type DangerousUrl = TypeOf<typeof dangerousUriSchema>;
12
+ export declare const loopbackUriSchema: z.ZodEffects<z.ZodEffects<z.ZodString, `${string}:${string}`, string>, `http://[::1]${string}` | "http://localhost" | `http://localhost#${string}` | `http://localhost?${string}` | `http://localhost/${string}` | `http://localhost:${string}` | "http://127.0.0.1" | `http://127.0.0.1#${string}` | `http://127.0.0.1?${string}` | `http://127.0.0.1/${string}` | `http://127.0.0.1:${string}`, string>;
13
+ export type LoopbackUri = TypeOf<typeof loopbackUriSchema>;
14
+ export declare const httpsUriSchema: z.ZodEffects<z.ZodEffects<z.ZodString, `${string}:${string}`, string>, `https://${string}`, string>;
15
+ export type HttpsUri = TypeOf<typeof httpsUriSchema>;
16
+ export declare const webUriSchema: z.ZodEffects<z.ZodString, `http://[::1]${string}` | "http://localhost" | `http://localhost#${string}` | `http://localhost?${string}` | `http://localhost/${string}` | `http://localhost:${string}` | "http://127.0.0.1" | `http://127.0.0.1#${string}` | `http://127.0.0.1?${string}` | `http://127.0.0.1/${string}` | `http://127.0.0.1:${string}` | `https://${string}`, string>;
17
+ export type WebUri = TypeOf<typeof webUriSchema>;
18
+ export declare const privateUseUriSchema: z.ZodEffects<z.ZodEffects<z.ZodString, `${string}:${string}`, string>, `${string}.${string}:/${string}`, string>;
19
+ export type PrivateUseUri = TypeOf<typeof privateUseUriSchema>;
20
+ //# sourceMappingURL=uri.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uri.d.ts","sourceRoot":"","sources":["../src/uri.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AAG7C;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,0DAQ5B,CAAA;AAEH;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAE5D,eAAO,MAAM,iBAAiB,2YA6B7B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE1D,eAAO,MAAM,cAAc,qGA6C1B,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAA;AAEpD,eAAO,MAAM,YAAY,oXAqBrB,CAAA;AAEJ,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAA;AAEhD,eAAO,MAAM,mBAAmB,kHAsC/B,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAA"}
package/dist/uri.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.privateUseUriSchema = exports.webUriSchema = exports.httpsUriSchema = exports.loopbackUriSchema = exports.dangerousUriSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const util_js_1 = require("./util.js");
6
+ /**
7
+ * Valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
8
+ *
9
+ * Any value that matches this schema is safe to parse using `new URL()`.
10
+ */
11
+ exports.dangerousUriSchema = zod_1.z
12
+ .string()
13
+ .refine((data) => data.includes(':') && URL.canParse(data), {
14
+ message: 'Invalid URL',
15
+ });
16
+ exports.loopbackUriSchema = exports.dangerousUriSchema.superRefine((value, ctx) => {
17
+ // Loopback url must use the "http:" protocol
18
+ if (!value.startsWith('http://')) {
19
+ ctx.addIssue({
20
+ code: zod_1.ZodIssueCode.custom,
21
+ message: 'URL must use the "http:" protocol',
22
+ });
23
+ return false;
24
+ }
25
+ const url = new URL(value);
26
+ if (!(0, util_js_1.isLoopbackHost)(url.hostname)) {
27
+ ctx.addIssue({
28
+ code: zod_1.ZodIssueCode.custom,
29
+ message: 'URL must use "localhost", "127.0.0.1" or "[::1]" as hostname',
30
+ });
31
+ return false;
32
+ }
33
+ return true;
34
+ });
35
+ exports.httpsUriSchema = exports.dangerousUriSchema.superRefine((value, ctx) => {
36
+ if (!value.startsWith('https://')) {
37
+ ctx.addIssue({
38
+ code: zod_1.ZodIssueCode.custom,
39
+ message: 'URL must use the "https:" protocol',
40
+ });
41
+ return false;
42
+ }
43
+ const url = new URL(value);
44
+ // Disallow loopback URLs with the `https:` protocol
45
+ if ((0, util_js_1.isLoopbackHost)(url.hostname)) {
46
+ ctx.addIssue({
47
+ code: zod_1.ZodIssueCode.custom,
48
+ message: 'https: URL must not use a loopback host',
49
+ });
50
+ return false;
51
+ }
52
+ if ((0, util_js_1.isHostnameIP)(url.hostname)) {
53
+ // Hostname is an IP address
54
+ }
55
+ else {
56
+ // Hostname is a domain name
57
+ if (!url.hostname.includes('.')) {
58
+ // we don't depend on PSL here, so we only check for a dot
59
+ ctx.addIssue({
60
+ code: zod_1.ZodIssueCode.custom,
61
+ message: 'Domain name must contain at least two segments',
62
+ });
63
+ return false;
64
+ }
65
+ if (url.hostname.endsWith('.local')) {
66
+ ctx.addIssue({
67
+ code: zod_1.ZodIssueCode.custom,
68
+ message: 'Domain name must not end with ".local"',
69
+ });
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ });
75
+ exports.webUriSchema = zod_1.z
76
+ .string()
77
+ .superRefine((value, ctx) => {
78
+ // discriminated union of `loopbackUriSchema` and `httpsUriSchema`
79
+ if (value.startsWith('http://')) {
80
+ const result = exports.loopbackUriSchema.safeParse(value);
81
+ if (!result.success)
82
+ result.error.issues.forEach(ctx.addIssue, ctx);
83
+ return result.success;
84
+ }
85
+ if (value.startsWith('https://')) {
86
+ const result = exports.httpsUriSchema.safeParse(value);
87
+ if (!result.success)
88
+ result.error.issues.forEach(ctx.addIssue, ctx);
89
+ return result.success;
90
+ }
91
+ ctx.addIssue({
92
+ code: zod_1.ZodIssueCode.custom,
93
+ message: 'URL must use the "http:" or "https:" protocol',
94
+ });
95
+ return false;
96
+ });
97
+ exports.privateUseUriSchema = exports.dangerousUriSchema.superRefine((value, ctx) => {
98
+ const dotIdx = value.indexOf('.');
99
+ const colonIdx = value.indexOf(':');
100
+ // Optimization: avoid parsing the URL if the protocol does not contain a "."
101
+ if (dotIdx === -1 || colonIdx === -1 || dotIdx > colonIdx) {
102
+ ctx.addIssue({
103
+ code: zod_1.ZodIssueCode.custom,
104
+ message: 'Private-use URI scheme requires a "." as part of the protocol',
105
+ });
106
+ return false;
107
+ }
108
+ const url = new URL(value);
109
+ // Should be covered by the check before, but let's be extra sure
110
+ if (!url.protocol.includes('.')) {
111
+ ctx.addIssue({
112
+ code: zod_1.ZodIssueCode.custom,
113
+ message: 'Invalid private-use URI scheme',
114
+ });
115
+ return false;
116
+ }
117
+ if (url.hostname) {
118
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
119
+ ctx.addIssue({
120
+ code: zod_1.ZodIssueCode.custom,
121
+ message: 'Private-use URI schemes must not include a hostname (only one "/" is allowed after the protocol, as per RFC 8252)',
122
+ });
123
+ return false;
124
+ }
125
+ return true;
126
+ });
127
+ //# sourceMappingURL=uri.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uri.js","sourceRoot":"","sources":["../src/uri.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-types",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "description": "OAuth typing & validation library",
6
6
  "keywords": [
@@ -1,16 +1,21 @@
1
- import { parseOAuthLoopbackClientId } from './oauth-client-id-loopback.js'
1
+ import {
2
+ OAuthClientIdLoopback,
3
+ parseOAuthLoopbackClientId,
4
+ } from './oauth-client-id-loopback.js'
2
5
  import { OAuthClientMetadataInput } from './oauth-client-metadata.js'
3
6
 
4
7
  export function atprotoLoopbackClientMetadata(
5
8
  clientId: string,
6
- ): OAuthClientMetadataInput {
9
+ ): OAuthClientMetadataInput & {
10
+ client_id: OAuthClientIdLoopback
11
+ } {
7
12
  const {
8
13
  scope = 'atproto',
9
14
  redirect_uris = [`http://127.0.0.1/`, `http://[::1]/`],
10
15
  } = parseOAuthLoopbackClientId(clientId)
11
16
 
12
17
  return {
13
- client_id: clientId,
18
+ client_id: clientId as OAuthClientIdLoopback,
14
19
  scope,
15
20
  redirect_uris,
16
21
  client_name: 'Loopback client',
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './constants.js'
2
+ export * from './uri.js'
2
3
  export * from './util.js'
3
4
 
4
5
  export * from './atproto-loopback-client-metadata.js'
@@ -25,6 +26,7 @@ export * from './oauth-issuer-identifier.js'
25
26
  export * from './oauth-par-response.js'
26
27
  export * from './oauth-password-grant-token-request.js'
27
28
  export * from './oauth-protected-resource-metadata.js'
29
+ export * from './oauth-redirect-uri.js'
28
30
  export * from './oauth-refresh-token-grant-token-request.js'
29
31
  export * from './oauth-refresh-token.js'
30
32
  export * from './oauth-request-uri.js'
@@ -1,9 +1,10 @@
1
1
  import { z } from 'zod'
2
+ import { oauthRedirectUriSchema } from './oauth-redirect-uri.js'
2
3
 
3
4
  export const oauthAuthorizationCodeGrantTokenRequestSchema = z.object({
4
5
  grant_type: z.literal('authorization_code'),
5
6
  code: z.string().min(1),
6
- redirect_uri: z.string().url(),
7
+ redirect_uri: oauthRedirectUriSchema,
7
8
  /** @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.1} */
8
9
  code_verifier: z
9
10
  .string()
@@ -1,14 +1,34 @@
1
1
  import { z } from 'zod'
2
+ import { dangerousUriSchema } from './uri.js'
2
3
 
3
4
  /**
4
5
  * @see {@link https://datatracker.ietf.org/doc/html/rfc9396#section-2 | RFC 9396, Section 2}
5
6
  */
6
7
  export const oauthAuthorizationDetailSchema = z.object({
7
8
  type: z.string(),
8
- locations: z.array(z.string().url()).optional(),
9
+ /**
10
+ * An array of strings representing the location of the resource or RS. These
11
+ * strings are typically URIs identifying the location of the RS.
12
+ */
13
+ locations: z.array(dangerousUriSchema).optional(),
14
+ /**
15
+ * An array of strings representing the kinds of actions to be taken at the
16
+ * resource.
17
+ */
9
18
  actions: z.array(z.string()).optional(),
19
+ /**
20
+ * An array of strings representing the kinds of data being requested from the
21
+ * resource.
22
+ */
10
23
  datatypes: z.array(z.string()).optional(),
24
+ /**
25
+ * A string identifier indicating a specific resource available at the API.
26
+ */
11
27
  identifier: z.string().optional(),
28
+ /**
29
+ * An array of strings representing the types or levels of privilege being
30
+ * requested at the resource.
31
+ */
12
32
  privileges: z.array(z.string()).optional(),
13
33
  })
14
34
 
@@ -4,6 +4,7 @@ import { z } from 'zod'
4
4
  import { oauthAuthorizationDetailsSchema } from './oauth-authorization-details.js'
5
5
  import { oauthClientIdSchema } from './oauth-client-id.js'
6
6
  import { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'
7
+ import { oauthRedirectUriSchema } from './oauth-redirect-uri.js'
7
8
  import { oauthResponseTypeSchema } from './oauth-response-type.js'
8
9
  import { oauthScopeSchema } from './oauth-scope.js'
9
10
  import { oidcClaimsParameterSchema } from './oidc-claims-parameter.js'
@@ -16,7 +17,7 @@ import { oidcEntityTypeSchema } from './oidc-entity-type.js'
16
17
  export const oauthAuthorizationRequestParametersSchema = z.object({
17
18
  client_id: oauthClientIdSchema,
18
19
  state: z.string().optional(),
19
- redirect_uri: z.string().url().optional(),
20
+ redirect_uri: oauthRedirectUriSchema.optional(),
20
21
  scope: oauthScopeSchema.optional(),
21
22
  response_type: oauthResponseTypeSchema,
22
23
 
@@ -2,9 +2,13 @@ import { z } from 'zod'
2
2
 
3
3
  import { oauthCodeChallengeMethodSchema } from './oauth-code-challenge-method.js'
4
4
  import { oauthIssuerIdentifierSchema } from './oauth-issuer-identifier.js'
5
+ import { webUriSchema } from './uri.js'
5
6
 
6
7
  /**
7
8
  * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
9
+ * @note we do not enforce https: scheme in URIs to support development
10
+ * environments. Make sure to validate the URIs before using it in a production
11
+ * environment.
8
12
  */
9
13
  export const oauthAuthorizationServerMetadataSchema = z.object({
10
14
  issuer: oauthIssuerIdentifierSchema,
@@ -37,31 +41,31 @@ export const oauthAuthorizationServerMetadataSchema = z.object({
37
41
  .array(z.string())
38
42
  .optional(),
39
43
 
40
- jwks_uri: z.string().url().optional(),
44
+ jwks_uri: webUriSchema.optional(),
41
45
 
42
- authorization_endpoint: z.string().url(), // .optional(),
46
+ authorization_endpoint: webUriSchema, // .optional(),
43
47
 
44
- token_endpoint: z.string().url(), // .optional(),
48
+ token_endpoint: webUriSchema, // .optional(),
45
49
  token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
46
50
  token_endpoint_auth_signing_alg_values_supported: z
47
51
  .array(z.string())
48
52
  .optional(),
49
53
 
50
- revocation_endpoint: z.string().url().optional(),
51
- introspection_endpoint: z.string().url().optional(),
52
- pushed_authorization_request_endpoint: z.string().url().optional(),
54
+ revocation_endpoint: webUriSchema.optional(),
55
+ introspection_endpoint: webUriSchema.optional(),
56
+ pushed_authorization_request_endpoint: webUriSchema.optional(),
53
57
 
54
58
  require_pushed_authorization_requests: z.boolean().optional(),
55
59
 
56
- userinfo_endpoint: z.string().url().optional(),
57
- end_session_endpoint: z.string().url().optional(),
58
- registration_endpoint: z.string().url().optional(),
60
+ userinfo_endpoint: webUriSchema.optional(),
61
+ end_session_endpoint: webUriSchema.optional(),
62
+ registration_endpoint: webUriSchema.optional(),
59
63
 
60
64
  // https://datatracker.ietf.org/doc/html/rfc9449#section-5.1
61
65
  dpop_signing_alg_values_supported: z.array(z.string()).optional(),
62
66
 
63
67
  // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#section-4
64
- protected_resources: z.array(z.string().url()).optional(),
68
+ protected_resources: z.array(webUriSchema).optional(),
65
69
 
66
70
  // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
67
71
  client_id_metadata_document_supported: z.boolean().optional(),
@@ -1,69 +1,87 @@
1
- import { OAuthClientId } from './oauth-client-id.js'
1
+ import { TypeOf, z } from 'zod'
2
+ import { oauthClientIdSchema } from './oauth-client-id.js'
3
+ import { httpsUriSchema } from './uri.js'
2
4
  import { extractUrlPath, isHostnameIP } from './util.js'
3
5
 
4
6
  /**
5
7
  * @see {@link https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html}
6
8
  */
7
- export type OAuthClientIdDiscoverable = OAuthClientId & `https://${string}`
9
+ export const oauthClientIdDiscoverableSchema = z
10
+ .intersection(oauthClientIdSchema, httpsUriSchema)
11
+ .superRefine((value, ctx): value is `https://${string}/${string}` => {
12
+ const url = new URL(value)
13
+
14
+ if (url.username || url.password) {
15
+ ctx.addIssue({
16
+ code: z.ZodIssueCode.custom,
17
+ message: 'ClientID must not contain credentials',
18
+ })
19
+ return false
20
+ }
21
+
22
+ if (url.hash) {
23
+ ctx.addIssue({
24
+ code: z.ZodIssueCode.custom,
25
+ message: 'ClientID must not contain a fragment',
26
+ })
27
+ return false
28
+ }
29
+
30
+ if (url.pathname === '/') {
31
+ ctx.addIssue({
32
+ code: z.ZodIssueCode.custom,
33
+ message:
34
+ 'ClientID must contain a path component (e.g. "/client-metadata.json")',
35
+ })
36
+ return false
37
+ }
38
+
39
+ if (url.pathname.endsWith('/')) {
40
+ ctx.addIssue({
41
+ code: z.ZodIssueCode.custom,
42
+ message: 'ClientID path must not end with a trailing slash',
43
+ })
44
+ return false
45
+ }
46
+
47
+ if (isHostnameIP(url.hostname)) {
48
+ ctx.addIssue({
49
+ code: z.ZodIssueCode.custom,
50
+ message: 'ClientID hostname must not be an IP address',
51
+ })
52
+ return false
53
+ }
54
+
55
+ // URL constructor normalizes the URL, so we extract the path manually to
56
+ // avoid normalization, then compare it to the normalized path to ensure
57
+ // that the URL does not contain path traversal or other unexpected characters
58
+ if (extractUrlPath(value) !== url.pathname) {
59
+ ctx.addIssue({
60
+ code: z.ZodIssueCode.custom,
61
+ message: `ClientID must be in canonical form ("${url.href}", got "${value}")`,
62
+ })
63
+ return false
64
+ }
65
+
66
+ return true
67
+ })
68
+
69
+ export type OAuthClientIdDiscoverable = TypeOf<
70
+ typeof oauthClientIdDiscoverableSchema
71
+ >
8
72
 
9
73
  export function isOAuthClientIdDiscoverable(
10
74
  clientId: string,
11
75
  ): clientId is OAuthClientIdDiscoverable {
12
- try {
13
- parseOAuthDiscoverableClientId(clientId)
14
- return true
15
- } catch {
16
- return false
17
- }
76
+ return oauthClientIdDiscoverableSchema.safeParse(clientId).success
18
77
  }
19
78
 
20
79
  export function assertOAuthDiscoverableClientId(
21
80
  value: string,
22
81
  ): asserts value is OAuthClientIdDiscoverable {
23
- void parseOAuthDiscoverableClientId(value)
82
+ void oauthClientIdDiscoverableSchema.parse(value)
24
83
  }
25
84
 
26
85
  export function parseOAuthDiscoverableClientId(clientId: string): URL {
27
- const url = new URL(clientId)
28
-
29
- if (url.protocol !== 'https:') {
30
- throw new TypeError('ClientID must use the "https:" protocol')
31
- }
32
-
33
- if (url.username || url.password) {
34
- throw new TypeError('ClientID must not contain credentials')
35
- }
36
-
37
- if (url.hash) {
38
- throw new TypeError('ClientID must not contain a fragment')
39
- }
40
-
41
- if (url.hostname === 'localhost') {
42
- throw new TypeError('ClientID hostname must not be "localhost"')
43
- }
44
-
45
- if (url.pathname === '/') {
46
- throw new TypeError(
47
- 'ClientID must contain a path component (e.g. "/client-metadata.json")',
48
- )
49
- }
50
-
51
- if (url.pathname.endsWith('/')) {
52
- throw new TypeError('ClientID path must not end with a trailing slash')
53
- }
54
-
55
- if (isHostnameIP(url.hostname)) {
56
- throw new TypeError('ClientID hostname must not be an IP address')
57
- }
58
-
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
- )
66
- }
67
-
68
- return url
86
+ return new URL(oauthClientIdDiscoverableSchema.parse(clientId))
69
87
  }
@@ -1,11 +1,33 @@
1
- import { OAuthClientId } from './oauth-client-id.js'
1
+ import { TypeOf, ZodIssueCode } from 'zod'
2
+ import { oauthClientIdSchema } from './oauth-client-id.js'
3
+ import {
4
+ OAuthLoopbackRedirectURI,
5
+ oauthLoopbackRedirectURISchema,
6
+ OAuthRedirectUri,
7
+ } from './oauth-redirect-uri.js'
2
8
  import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
3
- import { isLoopbackHost, safeUrl } from './util.js'
4
9
 
5
- const OAUTH_CLIENT_ID_LOOPBACK_URL = 'http://localhost'
10
+ const PREFIX = 'http://localhost'
6
11
 
7
- export type OAuthClientIdLoopback = OAuthClientId &
8
- `${typeof OAUTH_CLIENT_ID_LOOPBACK_URL}${'' | '/'}${'' | `?${string}`}`
12
+ export const oauthClientIdLoopbackSchema = oauthClientIdSchema.superRefine(
13
+ (value, ctx): value is `${typeof PREFIX}${'' | '/'}${'' | `?${string}`}` => {
14
+ try {
15
+ assertOAuthLoopbackClientId(value)
16
+ return true
17
+ } catch (error) {
18
+ ctx.addIssue({
19
+ code: ZodIssueCode.custom,
20
+ message:
21
+ error instanceof TypeError
22
+ ? error.message
23
+ : 'Invalid loopback client ID',
24
+ })
25
+ return false
26
+ }
27
+ },
28
+ )
29
+
30
+ export type OAuthClientIdLoopback = TypeOf<typeof oauthClientIdLoopbackSchema>
9
31
 
10
32
  export function isOAuthClientIdLoopback(
11
33
  clientId: string,
@@ -28,21 +50,18 @@ export function assertOAuthLoopbackClientId(
28
50
  // validation functions)
29
51
  export function parseOAuthLoopbackClientId(clientId: string): {
30
52
  scope?: OAuthScope
31
- redirect_uris?: [string, ...string[]]
53
+ redirect_uris?: [OAuthRedirectUri, ...OAuthRedirectUri[]]
32
54
  } {
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)) {
55
+ if (!clientId.startsWith(PREFIX)) {
56
+ throw new TypeError(`Loopback ClientID must start with "${PREFIX}"`)
57
+ } else if (clientId.includes('#', PREFIX.length)) {
38
58
  throw new TypeError('Loopback ClientID must not contain a hash component')
39
59
  }
40
60
 
41
61
  const queryStringIdx =
42
- clientId.length > 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
62
+ clientId.length > PREFIX.length && clientId[PREFIX.length] === '/'
63
+ ? PREFIX.length + 1
64
+ : PREFIX.length
46
65
 
47
66
  if (clientId.length === queryStringIdx) {
48
67
  return {} // no query string to parse
@@ -72,33 +91,14 @@ export function parseOAuthLoopbackClientId(clientId: string): {
72
91
  }
73
92
 
74
93
  const redirect_uris = searchParams.has('redirect_uri')
75
- ? (searchParams.getAll('redirect_uri') as [string, ...string[]])
94
+ ? (searchParams
95
+ .getAll('redirect_uri')
96
+ .map((value) => oauthLoopbackRedirectURISchema.parse(value)) as [
97
+ OAuthLoopbackRedirectURI,
98
+ ...OAuthLoopbackRedirectURI[],
99
+ ])
76
100
  : undefined
77
101
 
78
- if (redirect_uris) {
79
- for (const uri of redirect_uris) {
80
- const url = safeUrl(uri)
81
- if (!url) {
82
- throw new TypeError(`Invalid redirect_uri in client ID: ${uri}`)
83
- }
84
- if (url.protocol !== 'http:') {
85
- throw new TypeError(
86
- `Loopback ClientID must use "http:" redirect_uri's (got ${uri})`,
87
- )
88
- }
89
- if (url.hostname === 'localhost') {
90
- throw new TypeError(
91
- `Loopback ClientID must not use "localhost" as redirect_uri hostname (got ${uri})`,
92
- )
93
- }
94
- if (!isLoopbackHost(url.hostname)) {
95
- throw new TypeError(
96
- `Loopback ClientID must use loopback addresses as redirect_uri's (got ${uri})`,
97
- )
98
- }
99
- }
100
- }
101
-
102
102
  return {
103
103
  scope,
104
104
  redirect_uris,
@@ -4,13 +4,23 @@ import { z } from 'zod'
4
4
  import { oauthClientIdSchema } from './oauth-client-id.js'
5
5
  import { oauthEndpointAuthMethod } from './oauth-endpoint-auth-method.js'
6
6
  import { oauthGrantTypeSchema } from './oauth-grant-type.js'
7
+ import { oauthRedirectUriSchema } from './oauth-redirect-uri.js'
7
8
  import { oauthResponseTypeSchema } from './oauth-response-type.js'
8
9
  import { oauthScopeSchema } from './oauth-scope.js'
10
+ import { webUriSchema } from './uri.js'
9
11
 
10
- // https://openid.net/specs/openid-connect-registration-1_0.html
11
- // https://datatracker.ietf.org/doc/html/rfc7591
12
+ /**
13
+ * @see {@link https://openid.net/specs/openid-connect-registration-1_0.html}
14
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7591}
15
+ * @note we do not enforce https: scheme in URIs to support development
16
+ * environments. Make sure to validate the URIs before using it in a production
17
+ * environment.
18
+ */
12
19
  export const oauthClientMetadataSchema = z.object({
13
- redirect_uris: z.array(z.string().url()).nonempty(),
20
+ /**
21
+ * @note redirect_uris require additional validation
22
+ */
23
+ redirect_uris: z.array(oauthRedirectUriSchema).nonempty(),
14
24
  response_types: z
15
25
  .array(oauthResponseTypeSchema)
16
26
  .nonempty()
@@ -30,7 +40,7 @@ export const oauthClientMetadataSchema = z.object({
30
40
  token_endpoint_auth_signing_alg: z.string().optional(),
31
41
  userinfo_signed_response_alg: z.string().optional(),
32
42
  userinfo_encrypted_response_alg: z.string().optional(),
33
- jwks_uri: z.string().url().optional(),
43
+ jwks_uri: webUriSchema.optional(),
34
44
  jwks: jwksPubSchema.optional(),
35
45
  application_type: z.enum(['web', 'native']).default('web').optional(), // default, per spec, is "web"
36
46
  subject_type: z.enum(['public', 'pairwise']).default('public').optional(),
@@ -41,10 +51,10 @@ export const oauthClientMetadataSchema = z.object({
41
51
  authorization_encrypted_response_alg: z.string().optional(),
42
52
  client_id: oauthClientIdSchema.optional(),
43
53
  client_name: z.string().optional(),
44
- client_uri: z.string().url().optional(),
45
- policy_uri: z.string().url().optional(),
46
- tos_uri: z.string().url().optional(),
47
- logo_uri: z.string().url().optional(),
54
+ client_uri: webUriSchema.optional(),
55
+ policy_uri: webUriSchema.optional(),
56
+ tos_uri: webUriSchema.optional(),
57
+ logo_uri: webUriSchema.optional(), // TODO: allow data: uri ?
48
58
 
49
59
  /**
50
60
  * Default Maximum Authentication Age. Specifies that the End-User MUST be