@atproto/oauth-types 0.2.0 → 0.2.2

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 (72) hide show
  1. package/CHANGELOG.md +17 -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 +2 -2
  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.2",
4
4
  "license": "MIT",
5
5
  "description": "OAuth typing & validation library",
6
6
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "zod": "^3.23.8",
29
- "@atproto/jwk": "0.1.1"
29
+ "@atproto/jwk": "0.1.2"
30
30
  },
31
31
  "devDependencies": {
32
32
  "typescript": "^5.6.3"
@@ -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