@atproto/oauth-provider 0.19.7 → 0.19.8

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 (165) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,772 +0,0 @@
1
- import { Jwks, Keyset, jwksPubSchema } from '@atproto/jwk'
2
- import {
3
- OAuthAuthorizationServerMetadata,
4
- OAuthClientIdDiscoverable,
5
- OAuthClientIdLoopback,
6
- OAuthClientMetadata,
7
- OAuthClientMetadataInput,
8
- isLocalHostname,
9
- isOAuthClientIdDiscoverable,
10
- isOAuthClientIdLoopback,
11
- oauthClientMetadataSchema,
12
- } from '@atproto/oauth-types'
13
- import {
14
- Fetch,
15
- bindFetch,
16
- fetchJsonProcessor,
17
- fetchJsonZodProcessor,
18
- fetchOkProcessor,
19
- } from '@atproto-labs/fetch'
20
- import { pipe } from '@atproto-labs/pipe'
21
- import {
22
- CachedGetter,
23
- GetCachedOptions,
24
- SimpleStore,
25
- } from '@atproto-labs/simple-store'
26
- import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
27
- import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
28
- import { callAsync } from '../lib/util/function.js'
29
- import { Awaitable } from '../lib/util/type.js'
30
- import { OAuthHooks } from '../oauth-hooks.js'
31
- import { ClientId } from './client-id.js'
32
- import { ClientStore } from './client-store.js'
33
- import { parseDiscoverableClientId, parseRedirectUri } from './client-utils.js'
34
- import { Client } from './client.js'
35
-
36
- const fetchMetadataHandler = pipe(
37
- fetchOkProcessor(),
38
- // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html#section-4.1
39
- fetchJsonProcessor('application/json', true),
40
- fetchJsonZodProcessor(oauthClientMetadataSchema),
41
- )
42
-
43
- const fetchJwksHandler = pipe(
44
- fetchOkProcessor(),
45
- fetchJsonProcessor('application/json', false),
46
- fetchJsonZodProcessor(jwksPubSchema),
47
- )
48
-
49
- export type LoopbackMetadataGetter = (
50
- url: string,
51
- ) => Awaitable<OAuthClientMetadataInput>
52
-
53
- export class ClientManager {
54
- protected readonly jwks: CachedGetter<string, Jwks>
55
- protected readonly metadataGetter: CachedGetter<string, OAuthClientMetadata>
56
-
57
- constructor(
58
- protected readonly serverMetadata: OAuthAuthorizationServerMetadata,
59
- protected readonly keyset: Keyset,
60
- protected readonly hooks: OAuthHooks,
61
- protected readonly store: ClientStore | null,
62
- protected readonly loopbackMetadata: LoopbackMetadataGetter | null = null,
63
- safeFetch: Fetch,
64
- clientJwksCache: SimpleStore<string, Jwks>,
65
- clientMetadataCache: SimpleStore<string, OAuthClientMetadata>,
66
- ) {
67
- const fetch = bindFetch(safeFetch)
68
-
69
- this.jwks = new CachedGetter(async (uri, options) => {
70
- const jwks = await fetch(buildJsonGetRequest(uri, options)).then(
71
- fetchJwksHandler,
72
- )
73
-
74
- return jwks
75
- }, clientJwksCache)
76
-
77
- this.metadataGetter = new CachedGetter(async (uri, options) => {
78
- const metadata = await fetch(buildJsonGetRequest(uri, options)).then(
79
- fetchMetadataHandler,
80
- )
81
-
82
- // Validate within the getter to avoid caching invalid metadata
83
- return this.validateClientMetadata(uri, metadata)
84
- }, clientMetadataCache)
85
- }
86
-
87
- /**
88
- *
89
- * @see {@link https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2 OIDC Client Registration}
90
- */
91
- public async getClient(clientId: ClientId) {
92
- const metadata = await this.getClientMetadata(clientId).catch((err) => {
93
- throw InvalidClientMetadataError.from(
94
- err,
95
- `Unable to obtain client metadata for "${clientId}"`,
96
- )
97
- })
98
-
99
- const jwks = metadata.jwks_uri
100
- ? await this.jwks.get(metadata.jwks_uri).catch((err) => {
101
- throw InvalidClientMetadataError.from(
102
- err,
103
- `Unable to obtain jwks from "${metadata.jwks_uri}" for "${clientId}"`,
104
- )
105
- })
106
- : undefined
107
-
108
- const partialInfo = await callAsync(this.hooks.getClientInfo, clientId, {
109
- metadata,
110
- jwks,
111
- }).catch((err) => {
112
- throw InvalidClientMetadataError.from(
113
- err,
114
- `Rejected client information for "${clientId}"`,
115
- )
116
- })
117
-
118
- const isFirstParty = partialInfo?.isFirstParty ?? false
119
- const isTrusted = partialInfo?.isTrusted ?? isFirstParty
120
-
121
- return new Client(clientId, metadata, jwks, { isFirstParty, isTrusted })
122
- }
123
-
124
- public async loadClients(
125
- clientIds: Iterable<ClientId>,
126
- {
127
- onError = (err) => {
128
- throw err
129
- },
130
- }: {
131
- onError?: (
132
- err: unknown,
133
- clientId: ClientId,
134
- ) => Awaitable<Client | null | undefined>
135
- } = {},
136
- ): Promise<Map<ClientId, Client>> {
137
- // Make sure we don't load the same client multiple times
138
- const uniqueClientIds =
139
- clientIds instanceof Set ? clientIds : new Set(clientIds)
140
-
141
- // Load all (unique) clients in parallel
142
- const clients = await Promise.all(
143
- Array.from(uniqueClientIds, async (clientId) =>
144
- this.getClient(clientId).catch((err) => onError(err, clientId)),
145
- ),
146
- )
147
-
148
- // Return a map for easy lookups
149
- return new Map(
150
- clients
151
- .filter((c) => c != null && c instanceof Client)
152
- .map((c) => [c.id, c]),
153
- )
154
- }
155
-
156
- protected async getClientMetadata(
157
- clientId: ClientId,
158
- ): Promise<OAuthClientMetadata> {
159
- if (isOAuthClientIdLoopback(clientId)) {
160
- return this.getLoopbackClientMetadata(clientId)
161
- } else if (isOAuthClientIdDiscoverable(clientId)) {
162
- return this.getDiscoverableClientMetadata(clientId)
163
- } else if (this.store) {
164
- return this.getStoredClientMetadata(clientId)
165
- }
166
-
167
- throw new InvalidClientMetadataError(`Invalid client ID "${clientId}"`)
168
- }
169
-
170
- protected async getLoopbackClientMetadata(
171
- clientId: OAuthClientIdLoopback,
172
- ): Promise<OAuthClientMetadata> {
173
- const { loopbackMetadata } = this
174
- if (!loopbackMetadata) {
175
- throw new InvalidClientMetadataError('Loopback clients are not allowed')
176
- }
177
-
178
- const metadataRaw = await callAsync(loopbackMetadata, clientId).catch(
179
- (err) => {
180
- throw InvalidClientMetadataError.from(
181
- err,
182
- `Invalid loopback client id "${clientId}"`,
183
- )
184
- },
185
- )
186
-
187
- const metadata = await oauthClientMetadataSchema
188
- .parseAsync(metadataRaw)
189
- .catch((err) => {
190
- throw InvalidClientMetadataError.from(
191
- err,
192
- `Invalid loopback client metadata for "${clientId}"`,
193
- )
194
- })
195
-
196
- return this.validateClientMetadata(clientId, metadata)
197
- }
198
-
199
- protected async getDiscoverableClientMetadata(
200
- clientId: OAuthClientIdDiscoverable,
201
- ): Promise<OAuthClientMetadata> {
202
- const metadataUrl = parseDiscoverableClientId(clientId)
203
-
204
- const metadata = await this.metadataGetter.get(metadataUrl.href)
205
-
206
- // Note: we do *not* re-validate the metadata here, as the metadata is
207
- // validated within the getter. This is to avoid double validation.
208
- //
209
- // return this.validateClientMetadata(metadataUrl.href, metadata)
210
- return metadata
211
- }
212
-
213
- protected async getStoredClientMetadata(
214
- clientId: ClientId,
215
- ): Promise<OAuthClientMetadata> {
216
- if (this.store) {
217
- const metadata = await this.store.findClient(clientId)
218
- return this.validateClientMetadata(clientId, metadata)
219
- }
220
-
221
- throw new InvalidClientMetadataError(`Invalid client ID "${clientId}"`)
222
- }
223
-
224
- /**
225
- * This method will ensure that the client metadata is valid w.r.t. the OAuth
226
- * and OIDC specifications. It will also ensure that the metadata is
227
- * compatible with the implementation of this library, and ATPROTO's
228
- * requirements.
229
- */
230
- protected validateClientMetadata(
231
- clientId: ClientId,
232
- metadata: OAuthClientMetadata,
233
- ): OAuthClientMetadata {
234
- // @TODO This method should only check for rules that are specific to this
235
- // implementation or the ATPROTO specification. All generic validation rules
236
- // should be moved to the @atproto/oauth-types package.
237
-
238
- if (metadata.jwks && metadata.jwks_uri) {
239
- throw new InvalidClientMetadataError(
240
- 'jwks_uri and jwks are mutually exclusive',
241
- )
242
- }
243
-
244
- // Known OIDC specific parameters
245
- for (const k of [
246
- 'default_max_age',
247
- 'userinfo_signed_response_alg',
248
- 'id_token_signed_response_alg',
249
- 'userinfo_encrypted_response_alg',
250
- ] as const) {
251
- if (metadata[k] != null) {
252
- throw new InvalidClientMetadataError(`Unsupported "${k}" parameter`)
253
- }
254
- }
255
-
256
- const clientUriUrl = metadata.client_uri
257
- ? new URL(metadata.client_uri)
258
- : null
259
-
260
- if (clientUriUrl && isLocalHostname(clientUriUrl.hostname)) {
261
- throw new InvalidClientMetadataError('client_uri hostname is invalid')
262
- }
263
-
264
- const scopes = metadata.scope?.split(' ')
265
-
266
- if (!scopes) {
267
- throw new InvalidClientMetadataError('Missing scope property')
268
- }
269
-
270
- if (!scopes.includes('atproto')) {
271
- throw new InvalidClientMetadataError('Missing "atproto" scope')
272
- }
273
-
274
- const dupScope = scopes?.find(isDuplicate)
275
- if (dupScope) {
276
- throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`)
277
- }
278
-
279
- const dupGrantType = metadata.grant_types.find(isDuplicate)
280
- if (dupGrantType) {
281
- throw new InvalidClientMetadataError(
282
- `Duplicate grant type "${dupGrantType}"`,
283
- )
284
- }
285
-
286
- for (const grantType of metadata.grant_types) {
287
- switch (grantType) {
288
- case 'implicit':
289
- // Never allowed (unsafe)
290
- throw new InvalidClientMetadataError(
291
- `Grant type "${grantType}" is not allowed`,
292
- )
293
-
294
- // @TODO Add support (e.g. for first party client)
295
- // case 'client_credentials':
296
- // case 'password':
297
- case 'authorization_code':
298
- case 'refresh_token':
299
- if (!this.serverMetadata.grant_types_supported?.includes(grantType)) {
300
- throw new InvalidClientMetadataError(
301
- `Unsupported grant type "${grantType}"`,
302
- )
303
- }
304
- break
305
-
306
- default:
307
- throw new InvalidClientMetadataError(
308
- `Grant type "${grantType}" is not supported`,
309
- )
310
- }
311
- }
312
-
313
- if (metadata.client_id && metadata.client_id !== clientId) {
314
- throw new InvalidClientMetadataError('client_id does not match')
315
- }
316
-
317
- if (metadata.subject_type && metadata.subject_type !== 'public') {
318
- throw new InvalidClientMetadataError(
319
- 'Only "public" subject_type is supported',
320
- )
321
- }
322
-
323
- switch (metadata.token_endpoint_auth_method) {
324
- case 'none':
325
- if (metadata.token_endpoint_auth_signing_alg) {
326
- throw new InvalidClientMetadataError(
327
- `token_endpoint_auth_method "none" must not have token_endpoint_auth_signing_alg`,
328
- )
329
- }
330
- break
331
-
332
- case 'private_key_jwt':
333
- if (!metadata.jwks && !metadata.jwks_uri) {
334
- throw new InvalidClientMetadataError(
335
- `private_key_jwt auth method requires jwks or jwks_uri`,
336
- )
337
- }
338
- if (metadata.jwks?.keys.length === 0) {
339
- throw new InvalidClientMetadataError(
340
- `private_key_jwt auth method requires at least one key in jwks`,
341
- )
342
- }
343
- if (!metadata.token_endpoint_auth_signing_alg) {
344
- throw new InvalidClientMetadataError(
345
- `Missing token_endpoint_auth_signing_alg client metadata`,
346
- )
347
- }
348
- break
349
-
350
- default:
351
- throw new InvalidClientMetadataError(
352
- `Unsupported client authentication method "${metadata.token_endpoint_auth_method}". Make sure "token_endpoint_auth_method" is set to one of: "${Client.AUTH_METHODS_SUPPORTED.join('", "')}"`,
353
- )
354
- }
355
-
356
- if (metadata.authorization_encrypted_response_enc) {
357
- throw new InvalidClientMetadataError(
358
- 'Encrypted authorization response is not supported',
359
- )
360
- }
361
-
362
- if (metadata.tls_client_certificate_bound_access_tokens) {
363
- throw new InvalidClientMetadataError(
364
- 'Mutual-TLS bound access tokens are not supported',
365
- )
366
- }
367
-
368
- if (
369
- metadata.authorization_encrypted_response_enc &&
370
- !metadata.authorization_encrypted_response_alg
371
- ) {
372
- throw new InvalidClientMetadataError(
373
- 'authorization_encrypted_response_enc requires authorization_encrypted_response_alg',
374
- )
375
- }
376
-
377
- // ATPROTO spec requires the use of DPoP (OAuth spec defaults to false)
378
- if (metadata.dpop_bound_access_tokens !== true) {
379
- throw new InvalidClientMetadataError(
380
- '"dpop_bound_access_tokens" must be true',
381
- )
382
- }
383
-
384
- // ATPROTO spec requires the use of PKCE, does not support OIDC
385
- if (!metadata.response_types.includes('code')) {
386
- throw new InvalidClientMetadataError('response_types must include "code"')
387
- } else if (!metadata.grant_types.includes('authorization_code')) {
388
- // Consistency check
389
- throw new InvalidClientMetadataError(
390
- `The "code" response type requires that "grant_types" contains "authorization_code"`,
391
- )
392
- }
393
-
394
- if (metadata.authorization_details_types?.length) {
395
- const dupAuthDetailsType =
396
- metadata.authorization_details_types.find(isDuplicate)
397
- if (dupAuthDetailsType) {
398
- throw new InvalidClientMetadataError(
399
- `Duplicate authorization_details_type "${dupAuthDetailsType}"`,
400
- )
401
- }
402
-
403
- const authorizationDetailsTypesSupported =
404
- this.serverMetadata.authorization_details_types_supported
405
- if (!authorizationDetailsTypesSupported) {
406
- throw new InvalidClientMetadataError(
407
- 'authorization_details_types are not supported',
408
- )
409
- }
410
- for (const type of metadata.authorization_details_types) {
411
- if (!authorizationDetailsTypesSupported.includes(type)) {
412
- throw new InvalidClientMetadataError(
413
- `Unsupported authorization_details_type "${type}"`,
414
- )
415
- }
416
- }
417
- }
418
-
419
- if (!metadata.redirect_uris?.length) {
420
- // ATPROTO spec requires that at least one redirect URI is provided
421
-
422
- throw new InvalidClientMetadataError(
423
- 'At least one redirect_uri is required',
424
- )
425
- }
426
-
427
- if (
428
- metadata.application_type === 'native' &&
429
- metadata.token_endpoint_auth_method !== 'none'
430
- ) {
431
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
432
- //
433
- // > Except when using a mechanism like Dynamic Client Registration
434
- // > [RFC7591] to provision per-instance secrets, native apps are
435
- // > classified as public clients, as defined by Section 2.1 of OAuth 2.0
436
- // > [RFC6749]; they MUST be registered with the authorization server as
437
- // > such. Authorization servers MUST record the client type in the client
438
- // > registration details in order to identify and process requests
439
- // > accordingly.
440
-
441
- // @NOTE We may want to remove this restriction in the future, for example
442
- // if https://github.com/bluesky-social/proposals/tree/main/0010-client-assertion-backend
443
- // gets adopted
444
-
445
- throw new InvalidClientMetadataError(
446
- 'Native clients must authenticate using "none" method',
447
- )
448
- }
449
-
450
- if (
451
- metadata.application_type === 'web' &&
452
- metadata.grant_types.includes('implicit')
453
- ) {
454
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
455
- //
456
- // > Web Clients [as defined by "application_type"] using the OAuth
457
- // > Implicit Grant Type MUST only register URLs using the https
458
- // > scheme as redirect_uris; they MUST NOT use localhost as the
459
- // > hostname.
460
-
461
- for (const redirectUri of metadata.redirect_uris) {
462
- const url = parseRedirectUri(redirectUri)
463
- if (url.protocol !== 'https:') {
464
- throw new InvalidRedirectUriError(
465
- `Web clients must use HTTPS redirect URIs`,
466
- )
467
- }
468
-
469
- if (url.hostname === 'localhost') {
470
- throw new InvalidRedirectUriError(
471
- `Web clients must not use localhost as the hostname`,
472
- )
473
- }
474
- }
475
- }
476
-
477
- for (const redirectUri of metadata.redirect_uris) {
478
- const url = parseRedirectUri(redirectUri)
479
-
480
- if (url.username || url.password) {
481
- // Is this a valid concern? Should we allow credentials in the URI?
482
- throw new InvalidRedirectUriError(
483
- `Redirect URI ${url} must not contain credentials`,
484
- )
485
- }
486
-
487
- switch (true) {
488
- // FIRST: Loopback redirect URI exception (only for native apps)
489
-
490
- case url.hostname === 'localhost': {
491
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.3
492
- //
493
- // > While redirect URIs using localhost (i.e.,
494
- // > "http://localhost:{port}/{path}") function similarly to loopback IP
495
- // > redirects described in Section 7.3, the use of localhost is NOT
496
- // > RECOMMENDED. Specifying a redirect URI with the loopback IP literal
497
- // > rather than localhost avoids inadvertently listening on network
498
- // > interfaces other than the loopback interface. It is also less
499
- // > susceptible to client-side firewalls and misconfigured host name
500
- // > resolution on the user's device.
501
- throw new InvalidRedirectUriError(
502
- `Loopback redirect URI ${url} is not allowed (use explicit IPs instead)`,
503
- )
504
- }
505
-
506
- case url.hostname === '127.0.0.1':
507
- case url.hostname === '[::1]': {
508
- // Only allowed for native apps
509
- if (metadata.application_type !== 'native') {
510
- throw new InvalidRedirectUriError(
511
- `Loopback redirect URIs are only allowed for native apps`,
512
- )
513
- }
514
-
515
- if (url.port) {
516
- // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
517
- //
518
- // > The authorization server MUST allow any port to be specified at
519
- // > the time of the request for loopback IP redirect URIs, to
520
- // > accommodate clients that obtain an available ephemeral port
521
- // > from the operating system at the time of the request.
522
- //
523
- // Note: although validation of the redirect_uri will ignore the
524
- // port we still allow it to be specified, as the spec does not
525
- // forbid it. If a port number is specified, ports will need to
526
- // match when validating authorization requests. See
527
- // "compareRedirectUri()".
528
- }
529
-
530
- if (url.protocol !== 'http:') {
531
- // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
532
- //
533
- // > Loopback redirect URIs use the "http" scheme and are constructed
534
- // > with the loopback IP literal and whatever port the client is
535
- // > listening on. That is, "http://127.0.0.1:{port}/{path}" for IPv4,
536
- // > and "http://[::1]:{port}/{path}" for IPv6.
537
- throw new InvalidRedirectUriError(
538
- `Loopback redirect URI ${url} must use HTTP`,
539
- )
540
- }
541
-
542
- break
543
- }
544
-
545
- // SECOND: Protocol-based URI Redirection
546
-
547
- case url.protocol === 'http:': {
548
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
549
- //
550
- // > request_uri [...] URLs MUST use the https scheme unless the
551
- // > target Request Object is signed in a way that is verifiable by
552
- // > the OP.
553
- //
554
- // OIDC/Request Object are not supported. ATproto spec should not
555
- // allow HTTP redirect URIs either.
556
-
557
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
558
- //
559
- // > Authorization Servers MAY reject Redirection URI values using
560
- // > the http scheme, other than the loopback case for Native
561
- // > Clients.
562
- throw new InvalidRedirectUriError(
563
- 'Only loopback redirect URIs are allowed to use the "http" scheme',
564
- )
565
- }
566
-
567
- case url.protocol === 'https:': {
568
- if (isLocalHostname(url.hostname)) {
569
- throw new InvalidRedirectUriError(
570
- `Redirect URI "${url}"'s domain name must not be a local hostname`,
571
- )
572
- }
573
-
574
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
575
- //
576
- // > In addition to the collision-resistant properties, requiring a
577
- // > URI scheme based on a domain name that is under the control of
578
- // > the app can help to prove ownership in the event of a dispute
579
- // > where two apps claim the same private-use URI scheme (where one
580
- // > app is acting maliciously).
581
- //
582
- // We can't enforce this here (in generic client validation) because
583
- // we don't have a concept of generic proven ownership.
584
- //
585
- // Discoverable clients, however, will have this check covered in the
586
- // `validateDiscoverableClientMetadata`, by using the client_id's
587
- // domain as "proven ownership".
588
-
589
- // The following restriction from OIDC is *not* enforced for clients
590
- // as it prevents "App Links" / "Apple Universal Links" from being
591
- // used as redirect URIs.
592
- //
593
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
594
- //
595
- // > Native Clients [as defined by "application_type"] MUST only
596
- // > register redirect_uris using custom URI schemes or loopback URLs
597
- // > using the http scheme; loopback URLs use localhost or the IP
598
- // > loopback literals 127.0.0.1 or [::1] as the hostname.
599
- //
600
- // if (metadata.application_type === 'native') {
601
- // throw new InvalidRedirectUriError(
602
- // `Native clients must use custom URI schemes or loopback URLs`,
603
- // )
604
- // }
605
-
606
- break
607
- }
608
-
609
- case isPrivateUseUriScheme(url): {
610
- if (metadata.application_type !== 'native') {
611
- throw new InvalidRedirectUriError(
612
- `Private-Use URI Scheme redirect URI are only allowed for native apps`,
613
- )
614
- }
615
-
616
- break
617
- }
618
-
619
- default:
620
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
621
- //
622
- // > At a minimum, any private-use URI scheme that doesn't contain a
623
- // > period character (".") SHOULD be rejected.
624
- throw new InvalidRedirectUriError(
625
- `Invalid redirect URI scheme "${url.protocol}"`,
626
- )
627
- }
628
- }
629
-
630
- if (isOAuthClientIdLoopback(clientId)) {
631
- return this.validateLoopbackClientMetadata(clientId, metadata)
632
- } else if (isOAuthClientIdDiscoverable(clientId)) {
633
- return this.validateDiscoverableClientMetadata(clientId, metadata)
634
- } else {
635
- return metadata
636
- }
637
- }
638
-
639
- validateLoopbackClientMetadata(
640
- clientId: OAuthClientIdLoopback,
641
- metadata: OAuthClientMetadata,
642
- ): OAuthClientMetadata {
643
- if (metadata.client_uri) {
644
- throw new InvalidClientMetadataError(
645
- 'client_uri is not allowed for loopback clients',
646
- )
647
- }
648
-
649
- if (metadata.application_type !== 'native') {
650
- throw new InvalidClientMetadataError(
651
- 'Loopback clients must have application_type "native"',
652
- )
653
- }
654
-
655
- const method = metadata.token_endpoint_auth_method
656
- if (method !== 'none') {
657
- throw new InvalidClientMetadataError(
658
- `Loopback clients are not allowed to use "token_endpoint_auth_method" ${method}`,
659
- )
660
- }
661
-
662
- return metadata
663
- }
664
-
665
- validateDiscoverableClientMetadata(
666
- clientId: OAuthClientIdDiscoverable,
667
- metadata: OAuthClientMetadata,
668
- ): OAuthClientMetadata {
669
- if (!metadata.client_id) {
670
- // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
671
- throw new InvalidClientMetadataError(
672
- `client_id is required for discoverable clients`,
673
- )
674
- }
675
-
676
- const clientIdUrl = parseDiscoverableClientId(clientId)
677
-
678
- if (metadata.client_uri) {
679
- // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
680
- //
681
- // The client_uri must be a parent of the client_id URL. This might be
682
- // relaxed in the future.
683
-
684
- const clientUriUrl = new URL(metadata.client_uri)
685
-
686
- if (clientUriUrl.origin !== clientIdUrl.origin) {
687
- throw new InvalidClientMetadataError(
688
- `client_uri must have the same origin as the client_id`,
689
- )
690
- }
691
-
692
- if (clientIdUrl.pathname !== clientUriUrl.pathname) {
693
- if (
694
- !clientIdUrl.pathname.startsWith(
695
- clientUriUrl.pathname.endsWith('/')
696
- ? clientUriUrl.pathname
697
- : `${clientUriUrl.pathname}/`,
698
- )
699
- ) {
700
- throw new InvalidClientMetadataError(
701
- `client_uri must be a parent URL of the client_id`,
702
- )
703
- }
704
- }
705
- }
706
-
707
- for (const redirectUri of metadata.redirect_uris) {
708
- // @NOTE at this point, all redirect URIs have already been validated by
709
- // oauthRedirectUriSchema
710
-
711
- const url = parseRedirectUri(redirectUri)
712
-
713
- if (isPrivateUseUriScheme(url)) {
714
- // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
715
- //
716
- // > When choosing a URI scheme to associate with the app, apps MUST use
717
- // > a URI scheme based on a domain name under their control, expressed
718
- // > in reverse order, as recommended by Section 3.8 of [RFC7595] for
719
- // > private-use URI schemes.
720
-
721
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
722
- //
723
- // > In addition to the collision-resistant properties, requiring a
724
- // > URI scheme based on a domain name that is under the control of
725
- // > the app can help to prove ownership in the event of a dispute
726
- // > where two apps claim the same private-use URI scheme (where one
727
- // > app is acting maliciously).
728
-
729
- // https://atproto.com/specs/oauth
730
- //
731
- // > Any custom scheme must match the client_id hostname in
732
- // > reverse-domain order. The URI scheme must be followed by a single
733
- // > colon (:) then a single forward slash (/) and then a URI path
734
- // > component. For example, an app with client_id
735
- // > https://app.example.com/client-metadata.json could have a
736
- // > redirect_uri of com.example.app:/callback.
737
- const protocol = `${reverseDomain(clientIdUrl.hostname)}:`
738
- if (url.protocol !== protocol) {
739
- throw new InvalidRedirectUriError(
740
- `Private-Use URI Scheme redirect URI, for discoverable client metadata, must be the fully qualified domain name (FQDN) of the client_id, in reverse order (${protocol})`,
741
- )
742
- }
743
- }
744
- }
745
-
746
- return metadata
747
- }
748
- }
749
-
750
- function isDuplicate<
751
- T extends string | number | boolean | null | undefined | symbol,
752
- >(value: T, index: number, array: T[]) {
753
- return array.includes(value, index + 1)
754
- }
755
-
756
- function reverseDomain(domain: string) {
757
- return domain.split('.').reverse().join('.')
758
- }
759
-
760
- function isPrivateUseUriScheme(uri: URL) {
761
- return uri.protocol.includes('.')
762
- }
763
-
764
- function buildJsonGetRequest(uri: string, options?: GetCachedOptions) {
765
- return new Request(uri, {
766
- headers: { accept: 'application/json' },
767
- // @ts-expect-error invalid types in "undici-types"
768
- cache: options?.noCache ? 'no-cache' : undefined,
769
- signal: options?.signal,
770
- redirect: 'error',
771
- })
772
- }