@atproto/oauth-provider 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 (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
  }