@atproto/oauth-provider 0.1.2 → 0.2.0

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 (136) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/account/account.d.ts +6 -2
  3. package/dist/account/account.d.ts.map +1 -1
  4. package/dist/assets/app/bundle-manifest.json +3 -3
  5. package/dist/assets/app/main.css +1 -1
  6. package/dist/assets/app/main.js +3 -3
  7. package/dist/assets/app/main.js.map +1 -1
  8. package/dist/assets/assets-middleware.d.ts +2 -1
  9. package/dist/assets/assets-middleware.d.ts.map +1 -1
  10. package/dist/assets/assets-middleware.js +7 -0
  11. package/dist/assets/assets-middleware.js.map +1 -1
  12. package/dist/client/client-manager.d.ts +4 -3
  13. package/dist/client/client-manager.d.ts.map +1 -1
  14. package/dist/client/client-manager.js +91 -77
  15. package/dist/client/client-manager.js.map +1 -1
  16. package/dist/client/client.d.ts +2 -3
  17. package/dist/client/client.d.ts.map +1 -1
  18. package/dist/client/client.js +6 -12
  19. package/dist/client/client.js.map +1 -1
  20. package/dist/constants.d.ts +2 -0
  21. package/dist/constants.d.ts.map +1 -1
  22. package/dist/constants.js +3 -1
  23. package/dist/constants.js.map +1 -1
  24. package/dist/device/device-manager.d.ts +1 -1
  25. package/dist/device/device-manager.d.ts.map +1 -1
  26. package/dist/device/device-manager.js +2 -2
  27. package/dist/device/device-manager.js.map +1 -1
  28. package/dist/dpop/dpop-manager.d.ts +0 -1
  29. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  30. package/dist/dpop/dpop-manager.js +1 -4
  31. package/dist/dpop/dpop-manager.js.map +1 -1
  32. package/dist/errors/invalid-authorization-details-error.d.ts +4 -3
  33. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  34. package/dist/errors/invalid-authorization-details-error.js +4 -4
  35. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  36. package/dist/lib/http/parser.d.ts +13 -7
  37. package/dist/lib/http/parser.d.ts.map +1 -1
  38. package/dist/lib/http/parser.js +29 -9
  39. package/dist/lib/http/parser.js.map +1 -1
  40. package/dist/lib/http/request.d.ts +8 -5
  41. package/dist/lib/http/request.d.ts.map +1 -1
  42. package/dist/lib/http/request.js +24 -12
  43. package/dist/lib/http/request.js.map +1 -1
  44. package/dist/lib/http/stream.d.ts.map +1 -1
  45. package/dist/lib/http/stream.js +3 -2
  46. package/dist/lib/http/stream.js.map +1 -1
  47. package/dist/metadata/build-metadata.d.ts +0 -1
  48. package/dist/metadata/build-metadata.d.ts.map +1 -1
  49. package/dist/metadata/build-metadata.js +9 -49
  50. package/dist/metadata/build-metadata.js.map +1 -1
  51. package/dist/oauth-hooks.d.ts +3 -10
  52. package/dist/oauth-hooks.d.ts.map +1 -1
  53. package/dist/oauth-provider.d.ts +10 -15
  54. package/dist/oauth-provider.d.ts.map +1 -1
  55. package/dist/oauth-provider.js +176 -114
  56. package/dist/oauth-provider.js.map +1 -1
  57. package/dist/oauth-verifier.d.ts +1 -2
  58. package/dist/oauth-verifier.d.ts.map +1 -1
  59. package/dist/oauth-verifier.js.map +1 -1
  60. package/dist/output/build-authorize-data.d.ts +6 -0
  61. package/dist/output/build-authorize-data.d.ts.map +1 -1
  62. package/dist/output/build-authorize-data.js +1 -0
  63. package/dist/output/build-authorize-data.js.map +1 -1
  64. package/dist/replay/replay-manager.d.ts +1 -0
  65. package/dist/replay/replay-manager.d.ts.map +1 -1
  66. package/dist/replay/replay-manager.js +3 -0
  67. package/dist/replay/replay-manager.js.map +1 -1
  68. package/dist/replay/replay-store.d.ts +1 -1
  69. package/dist/request/request-info.d.ts +2 -0
  70. package/dist/request/request-info.d.ts.map +1 -1
  71. package/dist/request/request-manager.d.ts +3 -9
  72. package/dist/request/request-manager.d.ts.map +1 -1
  73. package/dist/request/request-manager.js +52 -77
  74. package/dist/request/request-manager.js.map +1 -1
  75. package/dist/request/types.d.ts +10 -10
  76. package/dist/signer/signed-token-payload.d.ts +88 -88
  77. package/dist/signer/signer.d.ts +24 -31
  78. package/dist/signer/signer.d.ts.map +1 -1
  79. package/dist/signer/signer.js +0 -40
  80. package/dist/signer/signer.js.map +1 -1
  81. package/dist/token/token-claims.d.ts +84 -84
  82. package/dist/token/token-manager.d.ts +1 -2
  83. package/dist/token/token-manager.d.ts.map +1 -1
  84. package/dist/token/token-manager.js +10 -37
  85. package/dist/token/token-manager.js.map +1 -1
  86. package/dist/token/types.d.ts +10 -10
  87. package/package.json +3 -3
  88. package/src/account/account.ts +11 -7
  89. package/src/assets/app/backend-data.ts +9 -2
  90. package/src/assets/app/components/accept-form.tsx +65 -51
  91. package/src/assets/app/components/client-name.tsx +24 -16
  92. package/src/assets/app/components/url-viewer.tsx +3 -3
  93. package/src/assets/app/views/accept-view.tsx +7 -4
  94. package/src/assets/app/views/authorize-view.tsx +2 -1
  95. package/src/assets/assets-middleware.ts +14 -2
  96. package/src/client/client-manager.ts +124 -120
  97. package/src/client/client.ts +5 -17
  98. package/src/constants.ts +3 -0
  99. package/src/device/device-manager.ts +7 -1
  100. package/src/dpop/dpop-manager.ts +1 -6
  101. package/src/errors/invalid-authorization-details-error.ts +9 -4
  102. package/src/lib/http/parser.ts +37 -13
  103. package/src/lib/http/request.ts +61 -15
  104. package/src/lib/http/stream.ts +5 -2
  105. package/src/metadata/build-metadata.ts +9 -56
  106. package/src/oauth-hooks.ts +3 -13
  107. package/src/oauth-provider.ts +187 -177
  108. package/src/oauth-verifier.ts +1 -2
  109. package/src/output/build-authorize-data.ts +8 -0
  110. package/src/replay/replay-manager.ts +9 -0
  111. package/src/replay/replay-store.ts +1 -1
  112. package/src/request/request-info.ts +2 -0
  113. package/src/request/request-manager.ts +81 -107
  114. package/src/signer/signer.ts +0 -63
  115. package/src/token/token-manager.ts +8 -41
  116. package/dist/oidc/claims.d.ts +0 -16
  117. package/dist/oidc/claims.d.ts.map +0 -1
  118. package/dist/oidc/claims.js +0 -29
  119. package/dist/oidc/claims.js.map +0 -1
  120. package/dist/oidc/userinfo.d.ts +0 -7
  121. package/dist/oidc/userinfo.d.ts.map +0 -1
  122. package/dist/oidc/userinfo.js +0 -3
  123. package/dist/oidc/userinfo.js.map +0 -1
  124. package/dist/parameters/claims-requested.d.ts +0 -3
  125. package/dist/parameters/claims-requested.d.ts.map +0 -1
  126. package/dist/parameters/claims-requested.js +0 -77
  127. package/dist/parameters/claims-requested.js.map +0 -1
  128. package/dist/parameters/oidc-payload.d.ts +0 -31
  129. package/dist/parameters/oidc-payload.d.ts.map +0 -1
  130. package/dist/parameters/oidc-payload.js +0 -25
  131. package/dist/parameters/oidc-payload.js.map +0 -1
  132. package/src/assets/app/components/client-identifier.tsx +0 -31
  133. package/src/oidc/claims.ts +0 -35
  134. package/src/oidc/userinfo.ts +0 -11
  135. package/src/parameters/claims-requested.ts +0 -106
  136. package/src/parameters/oidc-payload.ts +0 -28
@@ -1,22 +1,23 @@
1
1
  import { OAuthClientMetadata } from '@atproto/oauth-types'
2
2
  import { FormEvent } from 'react'
3
3
 
4
- import { Account } from '../backend-data'
4
+ import { Account, ScopeDetail } from '../backend-data'
5
5
  import { Override } from '../lib/util'
6
6
  import { AccountIdentifier } from './account-identifier'
7
7
  import { Button } from './button'
8
- import { ClientIdentifier } from './client-identifier'
9
8
  import { ClientName } from './client-name'
10
9
  import { FormCard, FormCardProps } from './form-card'
11
- import { Fieldset } from './fieldset'
12
10
 
13
11
  export type AcceptFormProps = Override<
14
12
  FormCardProps,
15
13
  {
16
- account: Account
17
14
  clientId: string
18
15
  clientMetadata: OAuthClientMetadata
19
16
  clientTrusted: boolean
17
+
18
+ account: Account
19
+ scopeDetails?: ScopeDetail[]
20
+
20
21
  onAccept: () => void
21
22
  acceptLabel?: string
22
23
 
@@ -29,10 +30,13 @@ export type AcceptFormProps = Override<
29
30
  >
30
31
 
31
32
  export function AcceptForm({
32
- account,
33
33
  clientId,
34
34
  clientMetadata,
35
35
  clientTrusted,
36
+
37
+ account,
38
+ scopeDetails,
39
+
36
40
  onAccept,
37
41
  acceptLabel = 'Accept',
38
42
  onReject,
@@ -62,54 +66,64 @@ export function AcceptForm({
62
66
  }
63
67
  {...props}
64
68
  >
65
- <Fieldset
66
- title={
67
- <ClientName clientId={clientId} clientMetadata={clientMetadata} />
68
- }
69
- >
70
- {clientTrusted && clientMetadata.logo_uri && (
71
- <div key="logo" className="flex items-center justify-center">
72
- <img
73
- crossOrigin="anonymous"
74
- src={clientMetadata.logo_uri}
75
- alt={clientMetadata.client_name}
76
- className="w-16 h-16 rounded-full"
77
- />
78
- </div>
79
- )}
69
+ {clientTrusted && clientMetadata.logo_uri && (
70
+ <div key="logo" className="flex items-center justify-center">
71
+ <img
72
+ crossOrigin="anonymous"
73
+ src={clientMetadata.logo_uri}
74
+ alt={clientMetadata.client_name}
75
+ className="w-16 h-16 rounded-full"
76
+ />
77
+ </div>
78
+ )}
79
+ <p>
80
+ <ClientName clientId={clientId} clientMetadata={clientMetadata} /> is
81
+ asking for permission to access your account (
82
+ <AccountIdentifier account={account} />
83
+ ).
84
+ </p>
80
85
 
81
- <p>
82
- <ClientIdentifier
83
- clientId={clientId}
84
- clientMetadata={clientMetadata}
85
- />{' '}
86
- is asking for permission to access your{' '}
87
- <AccountIdentifier account={account} /> account.
88
- </p>
86
+ <p>
87
+ By clicking <b>{acceptLabel}</b>, you allow this application to perform
88
+ the following actions in accordance to their{' '}
89
+ <a
90
+ href={clientMetadata.tos_uri}
91
+ rel="nofollow noopener"
92
+ target="_blank"
93
+ className="text-brand underline"
94
+ >
95
+ terms of service
96
+ </a>
97
+ {' and '}
98
+ <a
99
+ href={clientMetadata.policy_uri}
100
+ rel="nofollow noopener"
101
+ target="_blank"
102
+ className="text-brand underline"
103
+ >
104
+ privacy policy
105
+ </a>
106
+ :
107
+ </p>
89
108
 
90
- <p>
91
- By clicking <b>{acceptLabel}</b>, you allow this application to access
92
- your information in accordance to their{' '}
93
- <a
94
- href={clientMetadata.tos_uri}
95
- rel="nofollow noopener"
96
- target="_blank"
97
- className="text-brand underline"
98
- >
99
- terms of service
100
- </a>
101
- {' and '}
102
- <a
103
- href={clientMetadata.policy_uri}
104
- rel="nofollow noopener"
105
- target="_blank"
106
- className="text-brand underline"
107
- >
108
- privacy policy
109
- </a>
110
- .
111
- </p>
112
- </Fieldset>
109
+ {scopeDetails?.length ? (
110
+ <ul className="list-disc list-inside">
111
+ {scopeDetails.map(
112
+ ({ scope, description = getScopeDescription(scope) }) => (
113
+ <li key={scope}>{description}</li>
114
+ ),
115
+ )}
116
+ </ul>
117
+ ) : null}
113
118
  </FormCard>
114
119
  )
115
120
  }
121
+
122
+ function getScopeDescription(scope: string): string {
123
+ switch (scope) {
124
+ case 'atproto':
125
+ return 'Uniquely identify you'
126
+ default:
127
+ return scope
128
+ }
129
+ }
@@ -1,30 +1,38 @@
1
- import { OAuthClientMetadata } from '@atproto/oauth-types'
1
+ import {
2
+ isOAuthClientIdDiscoverable,
3
+ isOAuthClientIdLoopback,
4
+ OAuthClientMetadata,
5
+ } from '@atproto/oauth-types'
2
6
  import { HTMLAttributes } from 'react'
3
7
 
4
- import { ClientIdentifier } from './client-identifier'
8
+ import { UrlViewer } from './url-viewer'
5
9
 
6
10
  export type ClientNameProps = {
7
11
  clientId: string
8
12
  clientMetadata: OAuthClientMetadata
9
- as?: keyof JSX.IntrinsicElements
10
- }
13
+ } & HTMLAttributes<Element>
11
14
 
12
15
  export function ClientName({
13
16
  clientId,
14
17
  clientMetadata,
15
- as: As = 'span',
16
18
  ...attrs
17
- }: ClientNameProps & HTMLAttributes<Element>) {
18
- if (clientMetadata.client_name) {
19
- return <As {...attrs}>{clientMetadata.client_name}</As>
19
+ }: ClientNameProps) {
20
+ if (isOAuthClientIdLoopback(clientId)) {
21
+ return <span {...attrs}>An application on your device</span>
22
+ }
23
+
24
+ if (isOAuthClientIdDiscoverable(clientId)) {
25
+ if (clientMetadata.client_name) {
26
+ return (
27
+ <span {...attrs}>
28
+ {clientMetadata.client_name} (
29
+ <UrlViewer url={clientId} path />)
30
+ </span>
31
+ )
32
+ }
33
+
34
+ return <UrlViewer {...attrs} url={clientId} path />
20
35
  }
21
36
 
22
- return (
23
- <ClientIdentifier
24
- clientId={clientId}
25
- clientMetadata={clientMetadata}
26
- as={As}
27
- {...attrs}
28
- />
29
- )
37
+ return <span {...attrs}>{clientMetadata.client_name || clientId}</span>
30
38
  }
@@ -1,4 +1,4 @@
1
- import { HTMLAttributes, useMemo } from 'react'
1
+ import { Component, HTMLAttributes, useMemo } from 'react'
2
2
 
3
3
  export type UrlPartRenderingOptions = {
4
4
  faded?: boolean
@@ -28,7 +28,7 @@ export function UrlViewer({
28
28
  const urlObj = useMemo(() => new URL(url), [url])
29
29
 
30
30
  return (
31
- <As {...attrs}>
31
+ <Component as={As} {...attrs}>
32
32
  {proto && (
33
33
  <UrlPartViewer
34
34
  value={`${urlObj.protocol}//`}
@@ -56,7 +56,7 @@ export function UrlViewer({
56
56
  {hash && (
57
57
  <UrlPartViewer value={urlObj.hash} {...(hash === true ? null : hash)} />
58
58
  )}
59
- </As>
59
+ </Component>
60
60
  )
61
61
  }
62
62
 
@@ -1,6 +1,6 @@
1
1
  import { OAuthClientMetadata } from '@atproto/oauth-types'
2
2
 
3
- import { Session } from '../backend-data'
3
+ import { Account, ScopeDetail } from '../backend-data'
4
4
  import { AcceptForm } from '../components/accept-form'
5
5
  import { LayoutTitlePage } from '../components/layout-title-page'
6
6
 
@@ -8,7 +8,9 @@ export type AcceptViewProps = {
8
8
  clientId: string
9
9
  clientMetadata: OAuthClientMetadata
10
10
  clientTrusted: boolean
11
- session: Session
11
+
12
+ account: Account
13
+ scopeDetails?: ScopeDetail[]
12
14
 
13
15
  onAccept: () => void
14
16
  onReject: () => void
@@ -19,12 +21,12 @@ export function AcceptView({
19
21
  clientId,
20
22
  clientMetadata,
21
23
  clientTrusted,
22
- session,
24
+ account,
25
+ scopeDetails,
23
26
  onAccept,
24
27
  onReject,
25
28
  onBack,
26
29
  }: AcceptViewProps) {
27
- const { account } = session
28
30
  return (
29
31
  <LayoutTitlePage
30
32
  title="Authorize"
@@ -43,6 +45,7 @@ export function AcceptView({
43
45
  clientMetadata={clientMetadata}
44
46
  clientTrusted={clientTrusted}
45
47
  account={account}
48
+ scopeDetails={scopeDetails}
46
49
  onBack={onBack}
47
50
  onAccept={onAccept}
48
51
  onReject={onReject}
@@ -79,10 +79,11 @@ export function AuthorizeView({
79
79
  if (view === 'accept' && session) {
80
80
  return (
81
81
  <AcceptView
82
- session={session}
83
82
  clientId={authorizeData.clientId}
84
83
  clientMetadata={authorizeData.clientMetadata}
85
84
  clientTrusted={authorizeData.clientTrusted}
85
+ account={session.account}
86
+ scopeDetails={authorizeData.scopeDetails}
86
87
  onAccept={() => doAccept(session.account)}
87
88
  onReject={doReject}
88
89
  onBack={
@@ -1,8 +1,13 @@
1
- import { writeStream } from '../lib/http/index.js'
1
+ import {
2
+ Middleware,
3
+ validateFetchDest,
4
+ validateFetchSite,
5
+ writeStream,
6
+ } from '../lib/http/index.js'
2
7
 
3
8
  import { ASSETS_URL_PREFIX, getAsset } from './index.js'
4
9
 
5
- export function authorizeAssetsMiddleware() {
10
+ export function authorizeAssetsMiddleware(): Middleware {
6
11
  return async function assetsMiddleware(req, res, next): Promise<void> {
7
12
  if (req.method !== 'GET' && req.method !== 'HEAD') return next()
8
13
  if (!req.url?.startsWith(ASSETS_URL_PREFIX)) return next()
@@ -17,6 +22,13 @@ export function authorizeAssetsMiddleware() {
17
22
  const asset = await getAsset(filename).catch(() => null)
18
23
  if (!asset) return next()
19
24
 
25
+ try {
26
+ validateFetchSite(req, res, ['same-origin'])
27
+ validateFetchDest(req, res, ['style', 'script'])
28
+ } catch (err) {
29
+ return next(err)
30
+ }
31
+
20
32
  if (req.headers['if-none-match'] === asset.sha256) {
21
33
  return void res.writeHead(304).end()
22
34
  }
@@ -17,7 +17,7 @@ import {
17
17
  isLoopbackUrl,
18
18
  isOAuthClientIdDiscoverable,
19
19
  isOAuthClientIdLoopback,
20
- OAUTH_AUTHENTICATED_ENDPOINT_NAMES,
20
+ OAuthAuthorizationServerMetadata,
21
21
  OAuthClientIdDiscoverable,
22
22
  OAuthClientIdLoopback,
23
23
  OAuthClientMetadata,
@@ -56,9 +56,10 @@ export type LoopbackMetadataGetter = (
56
56
 
57
57
  export class ClientManager {
58
58
  protected readonly jwks: CachedGetter<string, Jwks>
59
- protected readonly metadata: CachedGetter<string, OAuthClientMetadata>
59
+ protected readonly metadataGetter: CachedGetter<string, OAuthClientMetadata>
60
60
 
61
61
  constructor(
62
+ protected readonly serverMetadata: OAuthAuthorizationServerMetadata,
62
63
  protected readonly keyset: Keyset,
63
64
  protected readonly hooks: OAuthHooks,
64
65
  protected readonly store: ClientStore | null,
@@ -77,7 +78,7 @@ export class ClientManager {
77
78
  return jwks
78
79
  }, clientJwksCache)
79
80
 
80
- this.metadata = new CachedGetter(async (uri, options) => {
81
+ this.metadataGetter = new CachedGetter(async (uri, options) => {
81
82
  const metadata = await fetch(buildJsonGetRequest(uri, options)).then(
82
83
  fetchMetadataHandler,
83
84
  )
@@ -160,7 +161,7 @@ export class ClientManager {
160
161
  ): Promise<OAuthClientMetadata> {
161
162
  const metadataUrl = parseDiscoverableClientId(clientId)
162
163
 
163
- const metadata = await this.metadata.get(metadataUrl.href)
164
+ const metadata = await this.metadataGetter.get(metadataUrl.href)
164
165
 
165
166
  // Note: we do *not* re-validate the metadata here, as the metadata is
166
167
  // validated within the getter. This is to avoid double validation.
@@ -196,6 +197,18 @@ export class ClientManager {
196
197
  )
197
198
  }
198
199
 
200
+ // Known OIDC specific parameters
201
+ for (const k of [
202
+ 'default_max_age',
203
+ 'userinfo_signed_response_alg',
204
+ 'id_token_signed_response_alg',
205
+ 'userinfo_encrypted_response_alg',
206
+ ] as const) {
207
+ if (metadata[k] != null) {
208
+ throw new InvalidClientMetadataError(`Unsupported "${k}" parameter`)
209
+ }
210
+ }
211
+
199
212
  const clientUriUrl = metadata.client_uri
200
213
  ? new URL(metadata.client_uri)
201
214
  : null
@@ -205,13 +218,27 @@ export class ClientManager {
205
218
  throw new InvalidClientMetadataError('client_uri must be a valid URL')
206
219
  }
207
220
 
208
- const scopes = metadata.scope?.split(' ')
209
- if (
210
- metadata.grant_types.includes('refresh_token') !==
211
- (scopes?.includes('offline_access') ?? false)
212
- ) {
221
+ const scopes = metadata.scope?.split(' ').filter(Boolean)
222
+
223
+ const dupScope = scopes?.find(isDuplicate)
224
+ if (dupScope) {
225
+ throw new InvalidClientMetadataError(`Duplicate scope "${dupScope}"`)
226
+ }
227
+
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
+ }
235
+ }
236
+ }
237
+
238
+ const dupGrantType = metadata.grant_types.find(isDuplicate)
239
+ if (dupGrantType) {
213
240
  throw new InvalidClientMetadataError(
214
- 'Grant type "refresh_token" requires scope "offline_access" (and vice versa)',
241
+ `Duplicate grant type "${dupGrantType}"`,
215
242
  )
216
243
  }
217
244
 
@@ -219,8 +246,8 @@ export class ClientManager {
219
246
  switch (grantType) {
220
247
  case 'authorization_code':
221
248
  case 'refresh_token':
222
- case 'implicit': // Required by OIDC (for id_token)
223
249
  continue
250
+ case 'implicit':
224
251
  case 'password':
225
252
  throw new InvalidClientMetadataError(
226
253
  `Grant type "${grantType}" is not allowed`,
@@ -242,78 +269,43 @@ export class ClientManager {
242
269
  )
243
270
  }
244
271
 
245
- if (
246
- metadata.userinfo_signed_response_alg &&
247
- !this.keyset.signAlgorithms.includes(
248
- metadata.userinfo_signed_response_alg,
249
- )
250
- ) {
251
- throw new InvalidClientMetadataError(
252
- `Unsupported "userinfo_signed_response_alg" ${metadata.userinfo_signed_response_alg}`,
253
- )
254
- }
255
-
256
- if (
257
- metadata.id_token_signed_response_alg &&
258
- !this.keyset.signAlgorithms.includes(
259
- metadata.id_token_signed_response_alg,
260
- )
261
- ) {
262
- throw new InvalidClientMetadataError(
263
- `Unsupported "id_token_signed_response_alg" ${metadata.id_token_signed_response_alg}`,
264
- )
265
- }
266
-
267
- if (metadata.userinfo_encrypted_response_alg) {
268
- // We only support signature for now.
269
- throw new InvalidClientMetadataError(
270
- 'Encrypted userinfo response is not supported',
271
- )
272
- }
273
-
274
- if (!metadata[`token_endpoint_auth_method`]) {
275
- throw new InvalidClientMetadataError(
276
- 'Missing token_endpoint_auth_method client metadata',
277
- )
278
- }
279
-
280
- for (const endpoint of OAUTH_AUTHENTICATED_ENDPOINT_NAMES) {
281
- const method =
282
- metadata[`${endpoint}_endpoint_auth_method`] ||
283
- metadata[`token_endpoint_auth_method`]
284
-
285
- switch (method) {
286
- case 'none':
287
- if (metadata.token_endpoint_auth_signing_alg) {
288
- throw new InvalidClientMetadataError(
289
- `${endpoint}_endpoint_auth_method "none" must not have ${endpoint}_endpoint_auth_signing_alg`,
290
- )
291
- }
292
- break
272
+ const method = metadata[`token_endpoint_auth_method`]
273
+ switch (method) {
274
+ case undefined:
275
+ throw new InvalidClientMetadataError(
276
+ 'Missing token_endpoint_auth_method client metadata',
277
+ )
293
278
 
294
- case 'private_key_jwt':
295
- if (!metadata.jwks && !metadata.jwks_uri) {
296
- throw new InvalidClientMetadataError(
297
- `private_key_jwt auth method requires jwks or jwks_uri`,
298
- )
299
- }
300
- if (metadata.jwks?.keys.length === 0) {
301
- throw new InvalidClientMetadataError(
302
- `private_key_jwt auth method requires at least one key in jwks`,
303
- )
304
- }
305
- if (!metadata.token_endpoint_auth_signing_alg) {
306
- throw new InvalidClientMetadataError(
307
- `Missing token_endpoint_auth_signing_alg client metadata`,
308
- )
309
- }
310
- break
279
+ case 'none':
280
+ if (metadata.token_endpoint_auth_signing_alg) {
281
+ throw new InvalidClientMetadataError(
282
+ `token_endpoint_auth_method "none" must not have token_endpoint_auth_signing_alg`,
283
+ )
284
+ }
285
+ break
311
286
 
312
- default:
287
+ case 'private_key_jwt':
288
+ if (!metadata.jwks && !metadata.jwks_uri) {
313
289
  throw new InvalidClientMetadataError(
314
- `${method} is not a supported "${endpoint}_endpoint_auth_method". Use "private_key_jwt" or "none".`,
290
+ `private_key_jwt auth method requires jwks or jwks_uri`,
315
291
  )
316
- }
292
+ }
293
+ if (metadata.jwks?.keys.length === 0) {
294
+ throw new InvalidClientMetadataError(
295
+ `private_key_jwt auth method requires at least one key in jwks`,
296
+ )
297
+ }
298
+ if (!metadata.token_endpoint_auth_signing_alg) {
299
+ throw new InvalidClientMetadataError(
300
+ `Missing token_endpoint_auth_signing_alg client metadata`,
301
+ )
302
+ }
303
+ break
304
+
305
+ default:
306
+ throw new InvalidClientMetadataError(
307
+ `${method} is not a supported "token_endpoint_auth_method". Use "private_key_jwt" or "none".`,
308
+ )
317
309
  }
318
310
 
319
311
  if (metadata.authorization_encrypted_response_enc) {
@@ -345,37 +337,28 @@ export class ClientManager {
345
337
  }
346
338
 
347
339
  for (const responseType of metadata.response_types) {
348
- const rt = responseType.split(' ')
340
+ if (responseType.includes('id_token')) {
341
+ throw new InvalidClientMetadataError(
342
+ `OpenID Connect response type "${responseType}" is not supported`,
343
+ )
344
+ }
349
345
 
350
346
  // ATPROTO spec requires the use of PKCE
351
- if (rt.includes('token')) {
347
+ if (responseType !== 'code') {
352
348
  throw new InvalidClientMetadataError(
353
- '"token" response type is not compatible with PKCE (use "code" instead)',
349
+ `Unsupported response type "${responseType}"`,
354
350
  )
355
351
  }
356
352
 
357
353
  // Consistency check
358
354
  if (
359
- rt.includes('code') &&
355
+ responseType === 'code' &&
360
356
  !metadata.grant_types.includes('authorization_code')
361
357
  ) {
362
358
  throw new InvalidClientMetadataError(
363
359
  `Response type "${responseType}" requires the "authorization_code" grant type`,
364
360
  )
365
361
  }
366
-
367
- // Asking for "code token" or "code id_token" is fine (as long as the
368
- // grant_types includes "authorization_code" and the scope includes
369
- // "openid"). Asking for "token" or "id_token" (without "code") requires
370
- // the "implicit" grant type.
371
- if (
372
- (rt.includes('token') || rt.includes('id_token')) &&
373
- !metadata.grant_types.includes('implicit')
374
- ) {
375
- throw new InvalidClientMetadataError(
376
- `Response type "${responseType}" requires the "implicit" grant type`,
377
- )
378
- }
379
362
  }
380
363
 
381
364
  if (metadata.application_type === 'native') {
@@ -390,11 +373,33 @@ export class ClientManager {
390
373
  // > accordingly.
391
374
  }
392
375
 
376
+ if (metadata.authorization_details_types?.length) {
377
+ const dupAuthDetailsType =
378
+ metadata.authorization_details_types.find(isDuplicate)
379
+ if (dupAuthDetailsType) {
380
+ throw new InvalidClientMetadataError(
381
+ `Duplicate authorization_details_type "${dupAuthDetailsType}"`,
382
+ )
383
+ }
384
+
385
+ const authorizationDetailsTypesSupported =
386
+ this.serverMetadata.authorization_details_types_supported
387
+ if (!authorizationDetailsTypesSupported) {
388
+ throw new InvalidClientMetadataError(
389
+ 'authorization_details_types are not supported',
390
+ )
391
+ }
392
+ for (const type of metadata.authorization_details_types) {
393
+ if (!authorizationDetailsTypesSupported.includes(type)) {
394
+ throw new InvalidClientMetadataError(
395
+ `Unsupported authorization_details_type "${type}"`,
396
+ )
397
+ }
398
+ }
399
+ }
400
+
393
401
  if (!metadata.redirect_uris?.length) {
394
- // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
395
- //
396
- // > OPs can require that request_uri values used be pre-registered with
397
- // > the require_request_uri_registration discovery parameter.
402
+ // ATPROTO spec requires that at least one redirect URI is provided
398
403
 
399
404
  throw new InvalidClientMetadataError(
400
405
  'At least one redirect_uri is required',
@@ -693,16 +698,11 @@ export class ClientManager {
693
698
  )
694
699
  }
695
700
 
696
- for (const endpoint of OAUTH_AUTHENTICATED_ENDPOINT_NAMES) {
697
- const method =
698
- metadata[`${endpoint}_endpoint_auth_method`] ||
699
- metadata[`token_endpoint_auth_method`]
700
-
701
- if (method !== 'none') {
702
- throw new InvalidClientMetadataError(
703
- `Loopback clients are not allowed to use "${endpoint}_endpoint_auth_method" ${method}`,
704
- )
705
- }
701
+ const method = metadata[`token_endpoint_auth_method`]
702
+ if (method !== 'none') {
703
+ throw new InvalidClientMetadataError(
704
+ `Loopback clients are not allowed to use "token_endpoint_auth_method" ${method}`,
705
+ )
706
706
  }
707
707
 
708
708
  for (const redirectUri of metadata.redirect_uris) {
@@ -766,16 +766,14 @@ export class ClientManager {
766
766
  }
767
767
  }
768
768
 
769
- for (const endpoint of OAUTH_AUTHENTICATED_ENDPOINT_NAMES) {
770
- const method = metadata[`${endpoint}_endpoint_auth_method`]
771
- switch (method) {
772
- case 'client_secret_post':
773
- case 'client_secret_basic':
774
- case 'client_secret_jwt':
775
- throw new InvalidClientMetadataError(
776
- `Client authentication method "${method}" is not allowed for discoverable clients`,
777
- )
778
- }
769
+ const method = metadata[`token_endpoint_auth_method`]
770
+ switch (method) {
771
+ case 'client_secret_post':
772
+ case 'client_secret_basic':
773
+ case 'client_secret_jwt':
774
+ throw new InvalidClientMetadataError(
775
+ `Client authentication method "${method}" is not allowed for discoverable clients`,
776
+ )
779
777
  }
780
778
 
781
779
  for (const redirectUri of metadata.redirect_uris) {
@@ -800,6 +798,12 @@ export class ClientManager {
800
798
  }
801
799
  }
802
800
 
801
+ function isDuplicate<
802
+ T extends string | number | boolean | null | undefined | symbol,
803
+ >(value: T, index: number, array: T[]) {
804
+ return array.includes(value, index + 1)
805
+ }
806
+
803
807
  function reverseDomain(domain: string) {
804
808
  return domain.split('.').reverse().join('.')
805
809
  }