@atproto/oauth-provider 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/account/account-store.d.ts +2 -2
  3. package/dist/assets/app/bundle-manifest.json +3 -3
  4. package/dist/assets/app/main.css +1 -1
  5. package/dist/assets/app/main.js +3 -3
  6. package/dist/assets/app/main.js.map +1 -1
  7. package/dist/assets/assets-middleware.d.ts.map +1 -1
  8. package/dist/assets/assets-middleware.js +4 -2
  9. package/dist/assets/assets-middleware.js.map +1 -1
  10. package/dist/client/client-manager.d.ts.map +1 -1
  11. package/dist/client/client-manager.js +127 -118
  12. package/dist/client/client-manager.js.map +1 -1
  13. package/dist/client/client-utils.d.ts +1 -2
  14. package/dist/client/client-utils.d.ts.map +1 -1
  15. package/dist/client/client-utils.js +3 -12
  16. package/dist/client/client-utils.js.map +1 -1
  17. package/dist/client/client.d.ts +8 -3
  18. package/dist/client/client.d.ts.map +1 -1
  19. package/dist/client/client.js +70 -1
  20. package/dist/client/client.js.map +1 -1
  21. package/dist/constants.d.ts +0 -1
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/constants.js +1 -2
  24. package/dist/constants.js.map +1 -1
  25. package/dist/errors/access-denied-error.d.ts +4 -4
  26. package/dist/errors/access-denied-error.d.ts.map +1 -1
  27. package/dist/errors/access-denied-error.js +2 -2
  28. package/dist/errors/access-denied-error.js.map +1 -1
  29. package/dist/errors/account-selection-required-error.d.ts +2 -2
  30. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  31. package/dist/errors/account-selection-required-error.js.map +1 -1
  32. package/dist/errors/consent-required-error.d.ts +2 -2
  33. package/dist/errors/consent-required-error.d.ts.map +1 -1
  34. package/dist/errors/consent-required-error.js.map +1 -1
  35. package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
  36. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  37. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  38. package/dist/errors/invalid-client-id-error.d.ts +1 -1
  39. package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
  40. package/dist/errors/invalid-client-id-error.js +12 -6
  41. package/dist/errors/invalid-client-id-error.js.map +1 -1
  42. package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
  43. package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
  44. package/dist/errors/invalid-client-metadata-error.js +11 -3
  45. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  46. package/dist/errors/invalid-parameters-error.d.ts +2 -2
  47. package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
  48. package/dist/errors/invalid-parameters-error.js.map +1 -1
  49. package/dist/errors/invalid-scope-error.d.ts +9 -0
  50. package/dist/errors/invalid-scope-error.d.ts.map +1 -0
  51. package/dist/errors/invalid-scope-error.js +14 -0
  52. package/dist/errors/invalid-scope-error.js.map +1 -0
  53. package/dist/errors/login-required-error.d.ts +2 -2
  54. package/dist/errors/login-required-error.d.ts.map +1 -1
  55. package/dist/errors/login-required-error.js.map +1 -1
  56. package/dist/lib/html/html.d.ts +1 -1
  57. package/dist/lib/html/html.d.ts.map +1 -1
  58. package/dist/lib/html/html.js +14 -11
  59. package/dist/lib/html/html.js.map +1 -1
  60. package/dist/lib/http/parser.d.ts +9 -2
  61. package/dist/lib/http/parser.d.ts.map +1 -1
  62. package/dist/lib/http/parser.js +15 -7
  63. package/dist/lib/http/parser.js.map +1 -1
  64. package/dist/lib/http/request.d.ts +0 -23
  65. package/dist/lib/http/request.d.ts.map +1 -1
  66. package/dist/lib/http/request.js +1 -11
  67. package/dist/lib/http/request.js.map +1 -1
  68. package/dist/lib/http/stream.d.ts +28 -6
  69. package/dist/lib/http/stream.d.ts.map +1 -1
  70. package/dist/lib/http/stream.js +21 -32
  71. package/dist/lib/http/stream.js.map +1 -1
  72. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  73. package/dist/lib/util/authorization-header.js +1 -1
  74. package/dist/lib/util/authorization-header.js.map +1 -1
  75. package/dist/lib/util/hostname.d.ts +3 -2
  76. package/dist/lib/util/hostname.d.ts.map +1 -1
  77. package/dist/lib/util/hostname.js +12 -8
  78. package/dist/lib/util/hostname.js.map +1 -1
  79. package/dist/metadata/build-metadata.d.ts.map +1 -1
  80. package/dist/metadata/build-metadata.js +2 -1
  81. package/dist/metadata/build-metadata.js.map +1 -1
  82. package/dist/oauth-errors.d.ts +1 -0
  83. package/dist/oauth-errors.d.ts.map +1 -1
  84. package/dist/oauth-errors.js +3 -1
  85. package/dist/oauth-errors.js.map +1 -1
  86. package/dist/oauth-hooks.d.ts +3 -3
  87. package/dist/oauth-hooks.d.ts.map +1 -1
  88. package/dist/oauth-provider.d.ts +20 -22
  89. package/dist/oauth-provider.d.ts.map +1 -1
  90. package/dist/oauth-provider.js +234 -176
  91. package/dist/oauth-provider.js.map +1 -1
  92. package/dist/oauth-verifier.d.ts +2 -2
  93. package/dist/oauth-verifier.d.ts.map +1 -1
  94. package/dist/oauth-verifier.js.map +1 -1
  95. package/dist/output/build-authorize-data.d.ts +2 -2
  96. package/dist/output/build-authorize-data.d.ts.map +1 -1
  97. package/dist/output/send-authorize-redirect.d.ts +2 -4
  98. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  99. package/dist/output/send-authorize-redirect.js +5 -2
  100. package/dist/output/send-authorize-redirect.js.map +1 -1
  101. package/dist/request/request-data.d.ts +2 -2
  102. package/dist/request/request-data.d.ts.map +1 -1
  103. package/dist/request/request-info.d.ts +2 -2
  104. package/dist/request/request-info.d.ts.map +1 -1
  105. package/dist/request/request-manager.d.ts +4 -4
  106. package/dist/request/request-manager.d.ts.map +1 -1
  107. package/dist/request/request-manager.js +94 -60
  108. package/dist/request/request-manager.js.map +1 -1
  109. package/dist/signer/signed-token-payload.d.ts +122 -122
  110. package/dist/signer/signer.d.ts +41 -40
  111. package/dist/signer/signer.d.ts.map +1 -1
  112. package/dist/signer/signer.js +13 -15
  113. package/dist/signer/signer.js.map +1 -1
  114. package/dist/token/token-claims.d.ts +121 -121
  115. package/dist/token/token-data.d.ts +3 -3
  116. package/dist/token/token-data.d.ts.map +1 -1
  117. package/dist/token/token-manager.d.ts +4 -5
  118. package/dist/token/token-manager.d.ts.map +1 -1
  119. package/dist/token/token-manager.js +96 -72
  120. package/dist/token/token-manager.js.map +1 -1
  121. package/dist/token/verify-token-claims.d.ts +3 -3
  122. package/dist/token/verify-token-claims.d.ts.map +1 -1
  123. package/dist/token/verify-token-claims.js.map +1 -1
  124. package/package.json +7 -6
  125. package/src/assets/app/components/sign-in-form.tsx +31 -2
  126. package/src/assets/app/components/url-viewer.tsx +3 -3
  127. package/src/assets/assets-middleware.ts +4 -2
  128. package/src/client/client-manager.ts +163 -161
  129. package/src/client/client-utils.ts +7 -12
  130. package/src/client/client.ts +112 -3
  131. package/src/constants.ts +0 -2
  132. package/src/errors/access-denied-error.ts +10 -4
  133. package/src/errors/account-selection-required-error.ts +2 -2
  134. package/src/errors/consent-required-error.ts +2 -2
  135. package/src/errors/invalid-authorization-details-error.ts +2 -2
  136. package/src/errors/invalid-client-id-error.ts +15 -4
  137. package/src/errors/invalid-client-metadata-error.ts +15 -3
  138. package/src/errors/invalid-parameters-error.ts +2 -2
  139. package/src/errors/invalid-scope-error.ts +15 -0
  140. package/src/errors/login-required-error.ts +2 -2
  141. package/src/lib/html/html.ts +14 -12
  142. package/src/lib/http/parser.ts +21 -8
  143. package/src/lib/http/request.ts +1 -23
  144. package/src/lib/http/stream.ts +29 -60
  145. package/src/lib/util/authorization-header.ts +5 -2
  146. package/src/lib/util/hostname.ts +9 -5
  147. package/src/metadata/build-metadata.ts +3 -1
  148. package/src/oauth-errors.ts +1 -0
  149. package/src/oauth-hooks.ts +3 -3
  150. package/src/oauth-provider.ts +368 -269
  151. package/src/oauth-verifier.ts +2 -2
  152. package/src/output/build-authorize-data.ts +2 -2
  153. package/src/output/send-authorize-redirect.ts +7 -6
  154. package/src/request/request-data.ts +2 -2
  155. package/src/request/request-info.ts +2 -2
  156. package/src/request/request-manager.ts +129 -103
  157. package/src/signer/signer.ts +24 -25
  158. package/src/token/token-data.ts +3 -3
  159. package/src/token/token-manager.ts +141 -99
  160. package/src/token/verify-token-claims.ts +3 -3
  161. package/dist/request/types.d.ts +0 -328
  162. package/dist/request/types.d.ts.map +0 -1
  163. package/dist/request/types.js +0 -27
  164. package/dist/request/types.js.map +0 -1
  165. package/dist/token/types.d.ts +0 -250
  166. package/dist/token/types.d.ts.map +0 -1
  167. package/dist/token/types.js +0 -36
  168. package/dist/token/types.js.map +0 -1
  169. package/src/request/types.ts +0 -48
  170. package/src/token/types.ts +0 -86
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  bindFetch,
3
3
  Fetch,
4
+ FetchError,
4
5
  fetchJsonProcessor,
5
6
  fetchJsonZodProcessor,
6
7
  fetchOkProcessor,
8
+ FetchResponseError,
7
9
  } from '@atproto-labs/fetch'
8
10
  import { pipe } from '@atproto-labs/pipe'
9
11
  import {
@@ -14,7 +16,6 @@ import {
14
16
  import { Jwks, jwksSchema, Keyset } from '@atproto/jwk'
15
17
  import {
16
18
  isLoopbackHost,
17
- isLoopbackUrl,
18
19
  isOAuthClientIdDiscoverable,
19
20
  isOAuthClientIdLoopback,
20
21
  OAuthAuthorizationServerMetadata,
@@ -24,12 +25,16 @@ import {
24
25
  OAuthClientMetadataInput,
25
26
  oauthClientMetadataSchema,
26
27
  } from '@atproto/oauth-types'
28
+ import { ZodError } from 'zod'
27
29
 
28
- import { ALLOW_LOOPBACK_CLIENT_REFRESH_TOKEN } from '../constants.js'
29
30
  import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
30
31
  import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
31
32
  import { OAuthError } from '../errors/oauth-error.js'
32
- import { parseDomain, parseUrlDomain } from '../lib/util/hostname.js'
33
+ import {
34
+ isInternetHost,
35
+ isInternetUrl,
36
+ parseUrlPublicSuffix,
37
+ } from '../lib/util/hostname.js'
33
38
  import { Awaitable } from '../lib/util/type.js'
34
39
  import { OAuthHooks } from '../oauth-hooks.js'
35
40
  import { ClientId } from './client-id.js'
@@ -106,20 +111,38 @@ export class ClientManager {
106
111
  })
107
112
 
108
113
  const isFirstParty = partialInfo?.isFirstParty ?? false
109
- const isTrusted =
110
- partialInfo?.isTrusted ??
111
- (isFirstParty ||
112
- // If the client was loaded from the store, we consider it trusted:
113
- (!isOAuthClientIdLoopback(clientId) &&
114
- !isOAuthClientIdDiscoverable(clientId)))
114
+ const isTrusted = partialInfo?.isTrusted ?? isFirstParty
115
115
 
116
116
  return new Client(clientId, metadata, jwks, { isFirstParty, isTrusted })
117
117
  } catch (err) {
118
- if (err instanceof OAuthError) throw err
118
+ if (err instanceof OAuthError) {
119
+ throw err
120
+ }
121
+ if (err instanceof FetchError) {
122
+ const message =
123
+ err instanceof FetchResponseError || err.statusCode !== 500
124
+ ? // Only expose 500 message if it was generated on another server
125
+ `Failed to fetch client information: ${err.message}`
126
+ : `Failed to fetch client information due to an internal error`
127
+ throw new InvalidClientMetadataError(message, err)
128
+ }
129
+ if (err instanceof ZodError) {
130
+ const issues = err.issues
131
+ .map(
132
+ ({ path, message }) =>
133
+ `Validation${path.length ? ` of "${path.join('.')}"` : ''} failed with error: ${message}`,
134
+ )
135
+ .join(' ')
136
+ throw new InvalidClientMetadataError(issues || err.message, err)
137
+ }
119
138
  if (err?.['code'] === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
120
139
  throw new InvalidClientMetadataError('Self-signed certificate', err)
121
140
  }
122
- throw InvalidClientMetadataError.from(err)
141
+
142
+ throw InvalidClientMetadataError.from(
143
+ err,
144
+ `Unable to load client information for "${clientId}"`,
145
+ )
123
146
  }
124
147
  }
125
148
 
@@ -145,15 +168,11 @@ export class ClientManager {
145
168
  throw new InvalidClientMetadataError('Loopback clients are not allowed')
146
169
  }
147
170
 
148
- const result = oauthClientMetadataSchema.safeParse(
171
+ const metadata = oauthClientMetadataSchema.parse(
149
172
  await loopbackMetadata(clientId),
150
173
  )
151
174
 
152
- if (!result.success) {
153
- throw InvalidClientMetadataError.from(result.error)
154
- }
155
-
156
- return this.validateClientMetadata(clientId, result.data)
175
+ return this.validateClientMetadata(clientId, metadata)
157
176
  }
158
177
 
159
178
  protected async getDiscoverableClientMetadata(
@@ -212,26 +231,34 @@ export class ClientManager {
212
231
  const clientUriUrl = metadata.client_uri
213
232
  ? new URL(metadata.client_uri)
214
233
  : null
215
- const clientUriParsed = clientUriUrl ? parseUrlDomain(clientUriUrl) : null
234
+ const clientUriDomain = clientUriUrl
235
+ ? parseUrlPublicSuffix(clientUriUrl)
236
+ : null
216
237
 
217
- if (clientUriUrl && !clientUriParsed) {
218
- throw new InvalidClientMetadataError('client_uri must be a valid URL')
238
+ if (clientUriUrl && !clientUriDomain) {
239
+ throw new InvalidClientMetadataError('client_uri hostname is invalid')
219
240
  }
220
241
 
221
- const scopes = metadata.scope?.split(' ').filter(Boolean)
242
+ const scopes = metadata.scope?.split(' ')
243
+
244
+ if (!scopes) {
245
+ throw new InvalidClientMetadataError('Missing scope property')
246
+ }
247
+
248
+ if (!scopes.includes('atproto')) {
249
+ throw new InvalidClientMetadataError('Missing "atproto" scope')
250
+ }
222
251
 
223
252
  const dupScope = scopes?.find(isDuplicate)
224
253
  if (dupScope) {
225
254
  throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`)
226
255
  }
227
256
 
228
- if (scopes) {
229
- for (const scope of scopes) {
230
- // Note, once we have dynamic scopes, this check will need to be
231
- // updated to check against the server's supported scopes.
232
- if (!this.serverMetadata.scopes_supported?.includes(scope)) {
233
- throw new InvalidClientMetadataError(`Unsupported scope "${scope}"`)
234
- }
257
+ for (const scope of scopes) {
258
+ // Note, once we have dynamic scopes, this check will need to be
259
+ // updated to check against the server's supported scopes.
260
+ if (!this.serverMetadata.scopes_supported?.includes(scope)) {
261
+ throw new InvalidClientMetadataError(`Unsupported scope "${scope}"`)
235
262
  }
236
263
  }
237
264
 
@@ -244,14 +271,24 @@ export class ClientManager {
244
271
 
245
272
  for (const grantType of metadata.grant_types) {
246
273
  switch (grantType) {
247
- case 'authorization_code':
248
- case 'refresh_token':
249
- continue
250
274
  case 'implicit':
251
- case 'password':
275
+ // Never allowed (unsafe)
252
276
  throw new InvalidClientMetadataError(
253
277
  `Grant type "${grantType}" is not allowed`,
254
278
  )
279
+
280
+ // @TODO: Add support (e.g. for first party client)
281
+ // case 'client_credentials':
282
+ // case 'password':
283
+ case 'authorization_code':
284
+ case 'refresh_token':
285
+ if (!this.serverMetadata.grant_types_supported?.includes(grantType)) {
286
+ throw new InvalidClientMetadataError(
287
+ `Unsupported grant type "${grantType}"`,
288
+ )
289
+ }
290
+ break
291
+
255
292
  default:
256
293
  throw new InvalidClientMetadataError(
257
294
  `Grant type "${grantType}" is not supported`,
@@ -336,41 +373,14 @@ export class ClientManager {
336
373
  )
337
374
  }
338
375
 
339
- for (const responseType of metadata.response_types) {
340
- if (responseType.includes('id_token')) {
341
- throw new InvalidClientMetadataError(
342
- `OpenID Connect response type "${responseType}" is not supported`,
343
- )
344
- }
345
-
346
- // ATPROTO spec requires the use of PKCE
347
- if (responseType !== 'code') {
348
- throw new InvalidClientMetadataError(
349
- `Unsupported response type "${responseType}"`,
350
- )
351
- }
352
-
376
+ // ATPROTO spec requires the use of PKCE, does not support OIDC
377
+ if (!metadata.response_types.includes('code')) {
378
+ throw new InvalidClientMetadataError('response_types must include "code"')
379
+ } else if (!metadata.grant_types.includes('authorization_code')) {
353
380
  // Consistency check
354
- if (
355
- responseType === 'code' &&
356
- !metadata.grant_types.includes('authorization_code')
357
- ) {
358
- throw new InvalidClientMetadataError(
359
- `Response type "${responseType}" requires the "authorization_code" grant type`,
360
- )
361
- }
362
- }
363
-
364
- if (metadata.application_type === 'native') {
365
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
366
- //
367
- // > Except when using a mechanism like Dynamic Client Registration
368
- // > [RFC7591] to provision per-instance secrets, native apps are
369
- // > classified as public clients, as defined by Section 2.1 of OAuth 2.0
370
- // > [RFC6749]; they MUST be registered with the authorization server as
371
- // > such. Authorization servers MUST record the client type in the
372
- // > client registration details in order to identify and process requests
373
- // > accordingly.
381
+ throw new InvalidClientMetadataError(
382
+ `The "code" response type requires that "grant_types" contains "authorization_code"`,
383
+ )
374
384
  }
375
385
 
376
386
  if (metadata.authorization_details_types?.length) {
@@ -433,47 +443,6 @@ export class ClientManager {
433
443
  }
434
444
  }
435
445
 
436
- if (metadata.application_type === 'native') {
437
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
438
- //
439
- // > Native Clients [as defined by "application_type"] MUST only
440
- // > register redirect_uris using custom URI schemes or loopback URLs
441
- // > using the http scheme; loopback URLs use localhost or the IP
442
- // > loopback literals 127.0.0.1 or [::1] as the hostname.
443
-
444
- for (const redirectUri of metadata.redirect_uris) {
445
- const url = parseRedirectUri(redirectUri)
446
- if (url.protocol !== 'http:') {
447
- throw new InvalidRedirectUriError(
448
- `Native clients must use HTTP redirect URIs (got ${url})`,
449
- )
450
- }
451
-
452
- if (!isLoopbackHost(url.hostname) && !isPrivateUseUriScheme(url)) {
453
- throw new InvalidRedirectUriError(
454
- 'Loopback redirect URIs are only allowed for native apps',
455
- )
456
- }
457
- }
458
- }
459
-
460
- if (metadata.application_type === 'native') {
461
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
462
- //
463
- // > Authorization Servers MAY reject Redirection URI values using
464
- // > the http scheme, other than the loopback case for Native
465
- // > Clients.
466
-
467
- for (const redirectUri of metadata.redirect_uris) {
468
- const url = parseRedirectUri(redirectUri)
469
- if (url.protocol === 'http:' && !isLoopbackUrl(url)) {
470
- throw new InvalidRedirectUriError(
471
- `Native clients must not use HTTP redirect URIs (got ${url})`,
472
- )
473
- }
474
- }
475
- }
476
-
477
446
  for (const redirectUri of metadata.redirect_uris) {
478
447
  const url = parseRedirectUri(redirectUri)
479
448
 
@@ -502,16 +471,10 @@ export class ClientManager {
502
471
  `Loopback redirect URI ${url} is not allowed (use explicit IPs instead)`,
503
472
  )
504
473
  }
505
- // falls through
474
+
506
475
  case url.hostname === '127.0.0.1':
507
476
  case url.hostname === '[::1]': {
508
- // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
509
- //
510
- // > Loopback redirect URIs use the "http" scheme and are constructed
511
- // > with the loopback IP literal and whatever port the client is
512
- // > listening on. That is, "http://127.0.0.1:{port}/{path}" for IPv4,
513
- // > and "http://[::1]:{port}/{path}" for IPv6.
514
-
477
+ // Only allowed for native apps
515
478
  if (metadata.application_type !== 'native') {
516
479
  throw new InvalidRedirectUriError(
517
480
  `Loopback redirect URIs are only allowed for native apps`,
@@ -534,6 +497,12 @@ export class ClientManager {
534
497
  }
535
498
 
536
499
  if (url.protocol !== 'http:') {
500
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
501
+ //
502
+ // > Loopback redirect URIs use the "http" scheme and are constructed
503
+ // > with the loopback IP literal and whatever port the client is
504
+ // > listening on. That is, "http://127.0.0.1:{port}/{path}" for IPv4,
505
+ // > and "http://[::1]:{port}/{path}" for IPv6.
537
506
  throw new InvalidRedirectUriError(
538
507
  `Loopback redirect URI ${url} must use HTTP`,
539
508
  )
@@ -551,17 +520,23 @@ export class ClientManager {
551
520
  // > target Request Object is signed in a way that is verifiable by
552
521
  // > the OP.
553
522
  //
554
- // TODO: Should we allow this (and check for signed request objects)?
523
+ // OIDC/Request Object are not supported. ATproto spec should not
524
+ // allow HTTP redirect URIs either.
525
+
526
+ // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
527
+ //
528
+ // > Authorization Servers MAY reject Redirection URI values using
529
+ // > the http scheme, other than the loopback case for Native
530
+ // > Clients.
555
531
  throw new InvalidRedirectUriError(
556
- `Non loopback redirect URI ${url} must use HTTPS`,
532
+ 'Only loopback redirect URIs are allowed to use the "http" scheme',
557
533
  )
558
534
  }
559
535
 
560
536
  case url.protocol === 'https:': {
561
- const redirectUriDomain = parseUrlDomain(url)
562
- if (!redirectUriDomain) {
537
+ if (!isInternetUrl(url)) {
563
538
  throw new InvalidRedirectUriError(
564
- `Redirect URI ${url} must be a valid URL`,
539
+ `Redirect URI "${url}"'s domain name must belong to the Public Suffix List (PSL)`,
565
540
  )
566
541
  }
567
542
 
@@ -573,19 +548,29 @@ export class ClientManager {
573
548
  // > where two apps claim the same private-use URI scheme (where one
574
549
  // > app is acting maliciously).
575
550
  //
576
- // Although this only applies to "native" clients (extract being from
577
- // rfc8252), we apply this rule to "web" clients as well.
578
- if (!clientUriParsed) {
579
- throw new InvalidClientMetadataError(
580
- 'client_uri is required for HTTPS redirect URIs',
581
- )
582
- } else {
583
- if (redirectUriDomain.domain !== clientUriParsed.domain) {
584
- throw new InvalidRedirectUriError(
585
- `Redirect URI ${url} must be under the same domain as client_uri ${metadata.client_uri}`,
586
- )
587
- }
588
- }
551
+ // We can't enforce this here (in generic client validation) because
552
+ // we don't have a concept of generic proven ownership.
553
+ //
554
+ // Discoverable clients, however, will have this check covered in the
555
+ // `validateDiscoverableClientMetadata`, by using the client_id's
556
+ // domain as "proven ownership".
557
+
558
+ // The following restriction from OIDC is *not* enforced for clients
559
+ // as it prevents "App Links" / "Apple Universal Links" from being
560
+ // used as redirect URIs.
561
+ //
562
+ // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
563
+ //
564
+ // > Native Clients [as defined by "application_type"] MUST only
565
+ // > register redirect_uris using custom URI schemes or loopback URLs
566
+ // > using the http scheme; loopback URLs use localhost or the IP
567
+ // > loopback literals 127.0.0.1 or [::1] as the hostname.
568
+ //
569
+ // if (metadata.application_type === 'native') {
570
+ // throw new InvalidRedirectUriError(
571
+ // `Native clients must use custom URI schemes or loopback URLs`,
572
+ // )
573
+ // }
589
574
 
590
575
  break
591
576
  }
@@ -604,16 +589,6 @@ export class ClientManager {
604
589
  )
605
590
  }
606
591
 
607
- const redirectUriDomain = parseDomain(
608
- reverseDomain(url.protocol.slice(0, -1)),
609
- )
610
-
611
- if (!redirectUriDomain) {
612
- throw new InvalidRedirectUriError(
613
- `Private-use URI Scheme redirect URI must be based on a valid domain name`,
614
- )
615
- }
616
-
617
592
  // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
618
593
  //
619
594
  // > In addition to the collision-resistant properties, requiring a
@@ -621,16 +596,17 @@ export class ClientManager {
621
596
  // > the app can help to prove ownership in the event of a dispute
622
597
  // > where two apps claim the same private-use URI scheme (where one
623
598
  // > app is acting maliciously).
624
- if (!clientUriParsed) {
625
- throw new InvalidClientMetadataError(
626
- 'client_uri is required for native apps using private-use URI Scheme redirect URIs',
599
+ //
600
+ // We can't check for ownership here (as there is no concept of
601
+ // proven ownership in the generic client validation), but we can
602
+ // check that the domain is a valid domain name.
603
+
604
+ const urlDomain = reverseDomain(url.protocol.slice(0, -1))
605
+
606
+ if (!isInternetHost(urlDomain)) {
607
+ throw new InvalidRedirectUriError(
608
+ `Private-use URI Scheme redirect URI must be based on a valid domain name`,
627
609
  )
628
- } else {
629
- if (redirectUriDomain.domain !== clientUriParsed.domain) {
630
- throw new InvalidRedirectUriError(
631
- `Private-Use URI Scheme redirect URI ${url} must be under the same domain as client_uri ${metadata.client_uri}`,
632
- )
633
- }
634
610
  }
635
611
 
636
612
  // https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
@@ -689,15 +665,6 @@ export class ClientManager {
689
665
  )
690
666
  }
691
667
 
692
- if (
693
- !ALLOW_LOOPBACK_CLIENT_REFRESH_TOKEN &&
694
- metadata.grant_types.includes('refresh_token')
695
- ) {
696
- throw new InvalidClientMetadataError(
697
- 'Loopback clients are not allowed to use the "refresh_token" grant type',
698
- )
699
- }
700
-
701
668
  const method = metadata[`token_endpoint_auth_method`]
702
669
  if (method !== 'none') {
703
670
  throw new InvalidClientMetadataError(
@@ -768,18 +735,34 @@ export class ClientManager {
768
735
 
769
736
  const method = metadata[`token_endpoint_auth_method`]
770
737
  switch (method) {
738
+ case 'none':
739
+ case 'private_key_jwt':
740
+ case undefined:
741
+ break
771
742
  case 'client_secret_post':
772
743
  case 'client_secret_basic':
773
744
  case 'client_secret_jwt':
774
745
  throw new InvalidClientMetadataError(
775
746
  `Client authentication method "${method}" is not allowed for discoverable clients`,
776
747
  )
748
+ default:
749
+ throw new InvalidClientMetadataError(
750
+ `Unsupported client authentication method "${method}"`,
751
+ )
777
752
  }
778
753
 
779
754
  for (const redirectUri of metadata.redirect_uris) {
780
755
  const url = parseRedirectUri(redirectUri)
781
756
 
782
757
  if (isPrivateUseUriScheme(url)) {
758
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
759
+ //
760
+ // > In addition to the collision-resistant properties, requiring a
761
+ // > URI scheme based on a domain name that is under the control of
762
+ // > the app can help to prove ownership in the event of a dispute
763
+ // > where two apps claim the same private-use URI scheme (where one
764
+ // > app is acting maliciously).
765
+
783
766
  // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
784
767
  //
785
768
  // Fully qualified domain name (FQDN) of the client_id, in reverse
@@ -792,6 +775,25 @@ export class ClientManager {
792
775
  )
793
776
  }
794
777
  }
778
+
779
+ if (url.protocol === 'https:') {
780
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
781
+ //
782
+ // > In addition to the collision-resistant properties, requiring a
783
+ // > URI scheme based on a domain name that is under the control of
784
+ // > the app can help to prove ownership in the event of a dispute
785
+ // > where two apps claim the same private-use URI scheme (where one
786
+ // > app is acting maliciously).
787
+ //
788
+ // Although this only applies to "native" clients (extract being from
789
+ // rfc8252), we apply this rule to "web" clients as well.
790
+
791
+ if (url.hostname !== clientIdUrl.hostname) {
792
+ throw new InvalidRedirectUriError(
793
+ `Redirect URI ${url} must be under the same domain as client_id ${metadata.client_uri}`,
794
+ )
795
+ }
796
+ }
795
797
  }
796
798
 
797
799
  return metadata
@@ -1,7 +1,5 @@
1
1
  import {
2
2
  OAuthClientIdDiscoverable,
3
- OAuthClientIdLoopback,
4
- parseOAuthLoopbackClientId,
5
3
  parseOAuthDiscoverableClientId,
6
4
  } from '@atproto/oauth-types'
7
5
 
@@ -25,19 +23,16 @@ export function parseDiscoverableClientId(
25
23
 
26
24
  // Extra validation, prevent usage of invalid internet domain names.
27
25
  if (!isInternetHost(url.hostname)) {
28
- throw new InvalidClientIdError('ClientID is not a valid internet address')
26
+ throw new InvalidClientIdError(
27
+ "The client_id's TLD must belong to the Public Suffix List (PSL)",
28
+ )
29
29
  }
30
30
 
31
31
  return url
32
32
  } catch (err) {
33
- throw InvalidClientIdError.from(err)
34
- }
35
- }
36
-
37
- export function parseLoopbackClientId(clientId: OAuthClientIdLoopback): URL {
38
- try {
39
- return parseOAuthLoopbackClientId(clientId)
40
- } catch (err) {
41
- throw InvalidClientIdError.from(err)
33
+ throw InvalidClientIdError.from(
34
+ err,
35
+ 'Invalid discoverable client identifier',
36
+ )
42
37
  }
43
38
  }
@@ -1,7 +1,8 @@
1
1
  import { Jwks } from '@atproto/jwk'
2
2
  import {
3
3
  CLIENT_ASSERTION_TYPE_JWT_BEARER,
4
- OAuthClientIdentification,
4
+ OAuthAuthorizationRequestParameters,
5
+ OAuthClientCredentials,
5
6
  OAuthClientMetadata,
6
7
  } from '@atproto/oauth-types'
7
8
  import {
@@ -20,9 +21,13 @@ import {
20
21
  } from 'jose'
21
22
 
22
23
  import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
24
+ import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
23
25
  import { InvalidClientError } from '../errors/invalid-client-error.js'
24
26
  import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
27
+ import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
25
28
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
29
+ import { InvalidScopeError } from '../errors/invalid-scope-error.js'
30
+ import { compareRedirectUri } from '../lib/util/redirect-uri.js'
26
31
  import { ClientAuth, authJwkThumbprint } from './client-auth.js'
27
32
  import { ClientId } from './client-id.js'
28
33
  import { ClientInfo } from './client-info.js'
@@ -102,11 +107,11 @@ export class Client {
102
107
 
103
108
  /**
104
109
  * @see {@link https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}
105
- * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-bearer-11#section-3}
110
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}
106
111
  * @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
107
112
  */
108
113
  public async verifyCredentials(
109
- input: OAuthClientIdentification,
114
+ input: OAuthClientCredentials,
110
115
  checks: {
111
116
  audience: string
112
117
  },
@@ -218,4 +223,108 @@ export class Client {
218
223
  // @ts-expect-error
219
224
  throw new Error(`Invalid method "${clientAuth.method}"`)
220
225
  }
226
+
227
+ /**
228
+ * Validates the request parameters against the client metadata.
229
+ */
230
+ public validateRequest(
231
+ parameters: Readonly<OAuthAuthorizationRequestParameters>,
232
+ ): Readonly<OAuthAuthorizationRequestParameters> {
233
+ if (parameters.client_id !== this.id) {
234
+ throw new InvalidParametersError(
235
+ parameters,
236
+ 'The "client_id" parameter field does not match the value used to authenticate the client',
237
+ )
238
+ }
239
+
240
+ if (parameters.scope !== undefined) {
241
+ // Any scope requested by the client must be registered in the client
242
+ // metadata.
243
+ const declaredScopes = this.metadata.scope?.split(' ')
244
+
245
+ if (!declaredScopes) {
246
+ throw new InvalidScopeError(
247
+ parameters,
248
+ 'Client has no declared scopes in its metadata',
249
+ )
250
+ }
251
+
252
+ for (const scope of parameters.scope.split(' ')) {
253
+ if (!declaredScopes.includes(scope)) {
254
+ throw new InvalidScopeError(
255
+ parameters,
256
+ `Scope "${scope}" is not declared in the client metadata`,
257
+ )
258
+ }
259
+ }
260
+ }
261
+
262
+ if (!this.metadata.response_types.includes(parameters.response_type)) {
263
+ throw new InvalidParametersError(
264
+ parameters,
265
+ `Invalid response_type "${parameters.response_type}" requested by the client`,
266
+ )
267
+ }
268
+
269
+ if (parameters.response_type.includes('code')) {
270
+ if (!this.metadata.grant_types.includes('authorization_code')) {
271
+ throw new InvalidParametersError(
272
+ parameters,
273
+ `This client is not allowed to use the "authorization_code" grant type`,
274
+ )
275
+ }
276
+ }
277
+
278
+ const { redirect_uri } = parameters
279
+ if (redirect_uri) {
280
+ if (
281
+ !this.metadata.redirect_uris.some((uri) =>
282
+ compareRedirectUri(uri, redirect_uri),
283
+ )
284
+ ) {
285
+ throw new InvalidParametersError(
286
+ parameters,
287
+ `Invalid redirect_uri ${redirect_uri}`,
288
+ )
289
+ }
290
+ } else {
291
+ const { defaultRedirectUri } = this
292
+ if (defaultRedirectUri) {
293
+ parameters = { ...parameters, redirect_uri: defaultRedirectUri }
294
+ } else {
295
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#authorization-request
296
+ //
297
+ // > "redirect_uri": OPTIONAL if only one redirect URI is registered for
298
+ // > this client. REQUIRED if multiple redirect URIs are registered for this
299
+ // > client.
300
+ throw new InvalidParametersError(parameters, 'redirect_uri is required')
301
+ }
302
+ }
303
+
304
+ if (parameters.authorization_details) {
305
+ const { authorization_details_types } = this.metadata
306
+ if (!authorization_details_types) {
307
+ throw new InvalidAuthorizationDetailsError(
308
+ parameters,
309
+ 'Client Metadata does not declare any "authorization_details"',
310
+ )
311
+ }
312
+
313
+ for (const detail of parameters.authorization_details) {
314
+ if (!authorization_details_types?.includes(detail.type)) {
315
+ throw new InvalidAuthorizationDetailsError(
316
+ parameters,
317
+ `Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
318
+ )
319
+ }
320
+ }
321
+ }
322
+
323
+ return parameters
324
+ }
325
+
326
+ get defaultRedirectUri(): string | undefined {
327
+ const { redirect_uris } = this.metadata
328
+ return redirect_uris.length === 1 ? redirect_uris[0] : undefined
329
+ }
221
330
  }