@atproto/oauth-provider 0.19.7 → 0.19.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,521 +0,0 @@
1
- import { isAtprotoDid } from '@atproto/did'
2
- import { LexResolverError } from '@atproto/lex-resolver'
3
- import type { Account } from '@atproto/oauth-provider-api'
4
- import { isAtprotoOauthScope } from '@atproto/oauth-scopes'
5
- import {
6
- OAuthAuthorizationRequestParameters,
7
- OAuthAuthorizationServerMetadata,
8
- } from '@atproto/oauth-types'
9
- import { isValidHandle } from '@atproto/syntax'
10
- import { ClientAuth } from '../client/client-auth.js'
11
- import { ClientId } from '../client/client-id.js'
12
- import { Client } from '../client/client.js'
13
- import {
14
- AUTHORIZATION_INACTIVITY_TIMEOUT,
15
- NODE_ENV,
16
- PAR_EXPIRES_IN,
17
- TOKEN_MAX_AGE,
18
- } from '../constants.js'
19
- import { DeviceId } from '../device/device-id.js'
20
- import { AccessDeniedError } from '../errors/access-denied-error.js'
21
- import { AuthorizationError } from '../errors/authorization-error.js'
22
- import { ConsentRequiredError } from '../errors/consent-required-error.js'
23
- import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
24
- import { InvalidGrantError } from '../errors/invalid-grant-error.js'
25
- import { InvalidRequestError } from '../errors/invalid-request-error.js'
26
- import { InvalidScopeError } from '../errors/invalid-scope-error.js'
27
- import { LexiconManager } from '../lexicon/lexicon-manager.js'
28
- import { RequestMetadata } from '../lib/http/request.js'
29
- import { OAuthHooks } from '../oauth-hooks.js'
30
- import { Signer } from '../signer/signer.js'
31
- import { Code, generateCode } from './code.js'
32
- import {
33
- RequestDataAuthorized,
34
- isRequestDataAuthorized,
35
- } from './request-data.js'
36
- import { generateRequestId } from './request-id.js'
37
- import { RequestStore, UpdateRequestData } from './request-store.js'
38
- import {
39
- RequestUri,
40
- decodeRequestUri,
41
- encodeRequestUri,
42
- } from './request-uri.js'
43
-
44
- export class RequestManager {
45
- constructor(
46
- protected readonly store: RequestStore,
47
- protected readonly lexiconManager: LexiconManager,
48
- protected readonly signer: Signer,
49
- protected readonly metadata: OAuthAuthorizationServerMetadata,
50
- protected readonly hooks: OAuthHooks,
51
- protected readonly tokenMaxAge = TOKEN_MAX_AGE,
52
- ) {}
53
-
54
- protected createTokenExpiry() {
55
- return new Date(Date.now() + this.tokenMaxAge)
56
- }
57
-
58
- async createAuthorizationRequest(
59
- client: Client,
60
- clientAuth: null | ClientAuth,
61
- input: Readonly<OAuthAuthorizationRequestParameters>,
62
- deviceId: null | DeviceId,
63
- ) {
64
- const parameters = await this.validate(client, clientAuth, input)
65
-
66
- await this.hooks.onAuthorizationRequest?.call(null, {
67
- client,
68
- clientAuth,
69
- parameters,
70
- })
71
-
72
- const expiresAt = new Date(Date.now() + PAR_EXPIRES_IN)
73
- const requestId = await generateRequestId()
74
-
75
- await this.store.createRequest(requestId, {
76
- clientId: client.id,
77
- clientAuth,
78
- parameters,
79
- expiresAt,
80
- deviceId,
81
- did: null,
82
- code: null,
83
- })
84
-
85
- const requestUri = encodeRequestUri(requestId)
86
- return { requestUri, expiresAt, parameters }
87
- }
88
-
89
- protected async validate(
90
- client: Client,
91
- clientAuth: null | ClientAuth,
92
- parameters: Readonly<OAuthAuthorizationRequestParameters>,
93
- ): Promise<Readonly<OAuthAuthorizationRequestParameters>> {
94
- // -------------------------------
95
- // Validate unsupported parameters
96
- // -------------------------------
97
-
98
- for (const k of [
99
- // Known unsupported OIDC parameters
100
- 'claims',
101
- 'id_token_hint',
102
- 'nonce', // note that OIDC "nonce" is redundant with PKCE
103
- ] as const) {
104
- if (parameters[k] !== undefined) {
105
- throw new AuthorizationError(parameters, `Unsupported "${k}" parameter`)
106
- }
107
- }
108
-
109
- // -----------------------
110
- // Validate against server
111
- // -----------------------
112
-
113
- if (
114
- !this.metadata.response_types_supported?.includes(
115
- parameters.response_type,
116
- )
117
- ) {
118
- throw new AuthorizationError(
119
- parameters,
120
- `Unsupported response_type "${parameters.response_type}"`,
121
- 'unsupported_response_type',
122
- )
123
- }
124
-
125
- if (
126
- parameters.response_type === 'code' &&
127
- !this.metadata.grant_types_supported?.includes('authorization_code')
128
- ) {
129
- throw new AuthorizationError(
130
- parameters,
131
- `Unsupported grant_type "authorization_code"`,
132
- 'invalid_request',
133
- )
134
- }
135
-
136
- if (parameters.authorization_details) {
137
- for (const detail of parameters.authorization_details) {
138
- if (
139
- !this.metadata.authorization_details_types_supported?.includes(
140
- detail.type,
141
- )
142
- ) {
143
- throw new InvalidAuthorizationDetailsError(
144
- parameters,
145
- `Unsupported "authorization_details" type "${detail.type}"`,
146
- )
147
- }
148
- }
149
- }
150
-
151
- // -----------------------
152
- // Validate against client
153
- // -----------------------
154
-
155
- parameters = client.validateRequest(parameters)
156
-
157
- // -------------------
158
- // Validate parameters
159
- // -------------------
160
-
161
- if (!parameters.redirect_uri) {
162
- // Should already be ensured by client.validateRequest(). Adding here for
163
- // clarity & extra safety.
164
- throw new AuthorizationError(parameters, 'Missing "redirect_uri"')
165
- }
166
-
167
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
168
- // > The authorization server MAY fully or partially ignore the scope
169
- // > requested by the client, based on the authorization server policy or
170
- // > the resource owner's instructions. If the issued access token scope is
171
- // > different from the one requested by the client, the authorization
172
- // > server MUST include the scope response parameter in the token response
173
- // > (Section 3.2.3) to inform the client of the actual scope granted.
174
-
175
- // Let's make sure the scopes are unique (to reduce the token & storage
176
- // size).
177
- const scopes = new Set(parameters.scope?.split(' '))
178
-
179
- // @NOTE An app requesting a not yet supported list of scopes will need to
180
- // re-authenticate the user once the scopes are supported. This is due to
181
- // the fact that the AS does not know how to properly display those scopes
182
- // to the user, so it cannot properly ask for consent.
183
- const scope =
184
- Array.from(scopes).filter(isAtprotoOauthScope).join(' ') || undefined
185
- parameters = { ...parameters, scope }
186
-
187
- if (parameters.code_challenge) {
188
- switch (parameters.code_challenge_method) {
189
- case undefined:
190
- // https://datatracker.ietf.org/doc/html/rfc7636#section-4.3
191
- parameters = { ...parameters, code_challenge_method: 'plain' }
192
- // falls through
193
- case 'plain':
194
- case 'S256':
195
- break
196
- default: {
197
- throw new AuthorizationError(
198
- parameters,
199
- `Unsupported code_challenge_method "${parameters.code_challenge_method}"`,
200
- )
201
- }
202
- }
203
- } else {
204
- if (parameters.code_challenge_method) {
205
- // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
206
- throw new AuthorizationError(
207
- parameters,
208
- 'code_challenge is required when code_challenge_method is provided',
209
- )
210
- }
211
-
212
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1
213
- //
214
- // > An AS MUST reject requests without a code_challenge from public
215
- // > clients, and MUST reject such requests from other clients unless
216
- // > there is reasonable assurance that the client mitigates
217
- // > authorization code injection in other ways. See Section 7.5.1 for
218
- // > details.
219
- //
220
- // > [...] In the specific deployment and the specific request, there is
221
- // > reasonable assurance by the authorization server that the client
222
- // > implements the OpenID Connect nonce mechanism properly.
223
- //
224
- // atproto does not implement the OpenID Connect nonce mechanism, so we
225
- // require the use of PKCE for all clients.
226
-
227
- throw new AuthorizationError(parameters, 'Use of PKCE is required')
228
- }
229
-
230
- // -----------------
231
- // atproto extension
232
- // -----------------
233
-
234
- if (parameters.response_type !== 'code') {
235
- throw new AuthorizationError(
236
- parameters,
237
- 'atproto only supports the "code" response_type',
238
- )
239
- }
240
-
241
- if (!scopes.has('atproto')) {
242
- throw new InvalidScopeError(parameters, 'The "atproto" scope is required')
243
- } else if (scopes.has('openid')) {
244
- throw new InvalidScopeError(
245
- parameters,
246
- 'OpenID Connect is not compatible with atproto',
247
- )
248
- }
249
-
250
- if (parameters.code_challenge_method !== 'S256') {
251
- throw new AuthorizationError(
252
- parameters,
253
- 'atproto requires use of "S256" code_challenge_method',
254
- )
255
- }
256
-
257
- // atproto extension: if the client is not trusted, and not authenticated,
258
- // force users to consent to authorization requests. We do this to avoid
259
- // unauthenticated clients from being able to silently re-authenticate
260
- // users.
261
- if (
262
- !client.info.isTrusted &&
263
- !client.info.isFirstParty &&
264
- client.metadata.token_endpoint_auth_method === 'none'
265
- ) {
266
- if (parameters.prompt === 'none') {
267
- throw new ConsentRequiredError(
268
- parameters,
269
- 'Public clients are not allowed to use silent-sign-on',
270
- )
271
- }
272
-
273
- // force "consent" for unauthenticated third party clients, unless they
274
- // are trying to create accounts:
275
- if (parameters.prompt !== 'create') {
276
- parameters = { ...parameters, prompt: 'consent' }
277
- }
278
- }
279
-
280
- // atproto extension: ensure that the login_hint is a valid handle or DID
281
- // @NOTE we to allow invalid case here, which is not spec'd anywhere.
282
- const hint = parameters.login_hint?.toLowerCase()
283
- if (hint) {
284
- if (!isAtprotoDid(hint) && !isValidHandle(hint)) {
285
- throw new AuthorizationError(parameters, `Invalid login_hint "${hint}"`)
286
- }
287
-
288
- // @TODO: ensure that the account actually exists on this server (there is
289
- // no point in showing the UI to the user if the account does not exist).
290
-
291
- // Update the parameters to ensure the right case is used
292
- parameters = { ...parameters, login_hint: hint }
293
- }
294
-
295
- // Make sure that every nsid in the scope resolves to a valid permission set
296
- // lexicon
297
- if (parameters.scope) {
298
- try {
299
- await this.lexiconManager.getPermissionSetsFromScope(parameters.scope)
300
- } catch (err) {
301
- // Parse expected errors
302
- if (err instanceof LexResolverError) {
303
- throw new AuthorizationError(
304
- parameters,
305
- err.message,
306
- 'invalid_scope',
307
- err,
308
- )
309
- }
310
-
311
- // Unexpected error
312
- throw err
313
- }
314
- }
315
-
316
- return parameters
317
- }
318
-
319
- /**
320
- * Reads the {@link ClientId} associated with a request URI without any of
321
- * the validation or side-effects performed by {@link RequestManager.get}
322
- *
323
- * Returns `undefined` when no such request exists.
324
- */
325
- async peekClientId(requestUri: RequestUri): Promise<ClientId | undefined> {
326
- const requestId = decodeRequestUri(requestUri)
327
- const data = await this.store.readRequest(requestId)
328
- return data?.clientId
329
- }
330
-
331
- async get(requestUri: RequestUri, deviceId?: DeviceId, clientId?: ClientId) {
332
- const requestId = decodeRequestUri(requestUri)
333
-
334
- const data = await this.store.readRequest(requestId)
335
- if (!data) throw new InvalidRequestError('Unknown request_uri')
336
-
337
- const updates: UpdateRequestData = {}
338
-
339
- try {
340
- if (data.did || data.code) {
341
- // If an account was linked to the request, the next step is to exchange
342
- // the code for a token.
343
- throw new AccessDeniedError(
344
- data.parameters,
345
- 'This request was already authorized',
346
- )
347
- }
348
-
349
- if (data.expiresAt < new Date()) {
350
- throw new AccessDeniedError(data.parameters, 'This request has expired')
351
- } else {
352
- updates.expiresAt = new Date(
353
- Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT,
354
- )
355
- }
356
-
357
- if (clientId != null && data.clientId !== clientId) {
358
- throw new AccessDeniedError(
359
- data.parameters,
360
- 'This request was initiated for another client',
361
- )
362
- }
363
-
364
- if (deviceId != null) {
365
- if (!data.deviceId) {
366
- updates.deviceId = deviceId
367
- } else if (data.deviceId !== deviceId) {
368
- throw new AccessDeniedError(
369
- data.parameters,
370
- 'This request was initiated from another device',
371
- )
372
- }
373
- }
374
- } catch (err) {
375
- await this.store.deleteRequest(requestId)
376
- throw err
377
- }
378
-
379
- if (Object.keys(updates).length > 0) {
380
- await this.store.updateRequest(requestId, updates)
381
- }
382
-
383
- return {
384
- requestUri,
385
- expiresAt: updates.expiresAt || data.expiresAt,
386
- parameters: data.parameters,
387
- clientId: data.clientId,
388
- }
389
- }
390
-
391
- async setAuthorized(
392
- requestUri: RequestUri,
393
- client: Client,
394
- account: Account,
395
- deviceId: DeviceId,
396
- deviceMetadata: RequestMetadata,
397
- scopeOverride?: string,
398
- ): Promise<Code> {
399
- const requestId = decodeRequestUri(requestUri)
400
-
401
- const data = await this.store.readRequest(requestId)
402
- if (!data) throw new InvalidRequestError('Unknown request_uri')
403
-
404
- let { parameters } = data
405
-
406
- try {
407
- if (account.deactivated) {
408
- throw new AccessDeniedError(
409
- parameters,
410
- 'This account has been deactivated',
411
- )
412
- }
413
-
414
- if (data.expiresAt < new Date()) {
415
- throw new AccessDeniedError(parameters, 'This request has expired')
416
- }
417
- if (!data.deviceId) {
418
- throw new AccessDeniedError(
419
- parameters,
420
- 'This request was not initiated',
421
- )
422
- }
423
- if (data.deviceId !== deviceId) {
424
- throw new AccessDeniedError(
425
- parameters,
426
- 'This request was initiated from another device',
427
- )
428
- }
429
- if (data.did || data.code) {
430
- throw new AccessDeniedError(
431
- parameters,
432
- 'This request was already authorized',
433
- )
434
- }
435
-
436
- // If a new scope value is provided, update the parameters by ensuring
437
- // that every existing scope in the parameters is also present in the
438
- // override value. This allows the user to remove scopes from the request,
439
- // but not to add new ones.
440
- if (scopeOverride != null) {
441
- const allowedScopes = new Set(scopeOverride.split(' '))
442
- const existingScopes = parameters.scope?.split(' ')
443
-
444
- // Compute the intersection of the existing scopes and the overrides.
445
- const newScopes = existingScopes?.filter((s) => allowedScopes.has(s))
446
-
447
- // Validate: make sure the new scopes are valid
448
- if (!newScopes?.includes('atproto')) {
449
- throw new AccessDeniedError(
450
- parameters,
451
- 'The "atproto" scope is required',
452
- )
453
- }
454
-
455
- parameters = { ...parameters, scope: newScopes.join(' ') }
456
- }
457
-
458
- // Only response_type=code is supported
459
- const code = await generateCode()
460
-
461
- // Bind the request to the account, preventing it from being used again.
462
- await this.store.updateRequest(requestId, {
463
- did: account.did,
464
- code,
465
- // Allow the client to exchange the code for a token within the next 60 seconds.
466
- expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
467
- parameters,
468
- })
469
-
470
- await this.hooks.onAuthorized?.call(null, {
471
- client,
472
- account,
473
- parameters,
474
- deviceId,
475
- deviceMetadata,
476
- requestId,
477
- })
478
-
479
- return code
480
- } catch (err) {
481
- await this.store.deleteRequest(requestId)
482
- throw err
483
- }
484
- }
485
-
486
- /**
487
- * @note If this method throws an error, any token previously generated from
488
- * the same `code` **must** me revoked.
489
- */
490
- public async consumeCode(code: Code): Promise<RequestDataAuthorized> {
491
- const result = await this.store.consumeRequestCode(code)
492
- if (!result) throw new InvalidGrantError('Invalid code')
493
-
494
- const { requestId, data } = result
495
-
496
- // Fool-proofing the store implementation against code replay attacks (in
497
- // case consumeRequestCode() does not delete the request).
498
- if (NODE_ENV !== 'production') {
499
- const result = await this.store.readRequest(requestId)
500
- if (result) {
501
- throw new Error('Invalid store implementation: request not deleted')
502
- }
503
- }
504
-
505
- if (!isRequestDataAuthorized(data) || data.code !== code) {
506
- // Should never happen: maybe the store implementation is faulty ?
507
- throw new Error('Unexpected request state')
508
- }
509
-
510
- if (data.expiresAt < new Date()) {
511
- throw new InvalidGrantError('This code has expired')
512
- }
513
-
514
- return data
515
- }
516
-
517
- async delete(requestUri: RequestUri): Promise<void> {
518
- const requestId = decodeRequestUri(requestUri)
519
- await this.store.deleteRequest(requestId)
520
- }
521
- }
@@ -1,60 +0,0 @@
1
- import { InvalidGrantError } from '../errors/invalid-grant-error.js'
2
- import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
3
- import { Code } from './code.js'
4
- import { RequestData } from './request-data.js'
5
- import { RequestId } from './request-id.js'
6
-
7
- // Export all types needed to implement the RequestStore interface
8
- export * from './code.js'
9
- export * from './request-data.js'
10
- export * from './request-id.js'
11
- export type { Awaitable }
12
-
13
- export type UpdateRequestData = Pick<
14
- Partial<RequestData>,
15
- 'did' | 'code' | 'deviceId' | 'expiresAt' | 'parameters'
16
- >
17
-
18
- export type FoundRequestResult = {
19
- requestId: RequestId
20
- data: RequestData
21
- }
22
-
23
- export { InvalidGrantError }
24
-
25
- export interface RequestStore {
26
- createRequest(requestId: RequestId, data: RequestData): Awaitable<void>
27
- /**
28
- * Note that expired requests **can** be returned to yield a different error
29
- * message than if the request was not found.
30
- */
31
- readRequest(requestId: RequestId): Awaitable<RequestData | null>
32
- updateRequest(requestId: RequestId, data: UpdateRequestData): Awaitable<void>
33
- deleteRequest(requestId: RequestId): void | Awaitable<void>
34
- /**
35
- * @note it is **IMPORTANT** that this method prevents concurrent retrieval of
36
- * the same code. If two requests are made with the same code, only one of
37
- * them should succeed and return the request data.
38
- *
39
- * @throws {InvalidGrantError} - When the request is not found or has expired
40
- * (allows to provide an error message instead of returning `null`).
41
- */
42
- consumeRequestCode(code: Code): Awaitable<FoundRequestResult | null>
43
- }
44
-
45
- export const isRequestStore = buildInterfaceChecker<RequestStore>([
46
- 'consumeRequestCode',
47
- 'createRequest',
48
- 'deleteRequest',
49
- 'readRequest',
50
- 'updateRequest',
51
- ])
52
-
53
- export function asRequestStore<V extends Partial<RequestStore>>(
54
- implementation?: V,
55
- ): V & RequestStore {
56
- if (!implementation || !isRequestStore(implementation)) {
57
- throw new Error('Invalid RequestStore implementation')
58
- }
59
- return implementation
60
- }
@@ -1,42 +0,0 @@
1
- import { z } from 'zod'
2
- import { InvalidRequestError } from '../errors/invalid-request-error.js'
3
- import { formatError } from '../lib/util/error.js'
4
- import { RequestId, requestIdSchema } from './request-id.js'
5
-
6
- export const REQUEST_URI_PREFIX = 'urn:ietf:params:oauth:request_uri:'
7
-
8
- export const requestUriSchema = z
9
- .string()
10
- .refinement(
11
- (data): data is `${typeof REQUEST_URI_PREFIX}${RequestId}` =>
12
- data.startsWith(REQUEST_URI_PREFIX) &&
13
- requestIdSchema.safeParse(decodeRequestUri(data as any)).success,
14
- {
15
- code: z.ZodIssueCode.custom,
16
- message: 'Invalid request_uri format',
17
- },
18
- )
19
-
20
- export type RequestUri = z.infer<typeof requestUriSchema>
21
-
22
- export function encodeRequestUri(requestId: RequestId): RequestUri {
23
- return `${REQUEST_URI_PREFIX}${encodeURIComponent(requestId) as RequestId}`
24
- }
25
-
26
- export function decodeRequestUri(requestUri: RequestUri): RequestId {
27
- const requestIdEnc = requestUri.slice(REQUEST_URI_PREFIX.length)
28
- return decodeURIComponent(requestIdEnc) as RequestId
29
- }
30
-
31
- export function parseRequestUri(
32
- requestUri: string,
33
- parseParams?: { path?: (string | number)[] },
34
- ): RequestUri {
35
- const parseResult = requestUriSchema.safeParse(requestUri, parseParams)
36
- if (!parseResult.success) {
37
- const err = parseResult.error
38
- const msg = formatError(err, 'Invalid "request_uri" query parameter')
39
- throw new InvalidRequestError(msg, err)
40
- }
41
- return parseResult.data
42
- }
@@ -1,24 +0,0 @@
1
- import { OAuthTokenType } from '@atproto/oauth-types'
2
- import { Code } from '../request/code.js'
3
-
4
- /**
5
- * @note `iss` and `state` will be added from the
6
- * {@link AuthorizationResultRedirect} object.
7
- */
8
- export type AuthorizationRedirectParameters =
9
- | {
10
- // iss: string
11
- // state?: string
12
- code: Code
13
- id_token?: string
14
- access_token?: string
15
- token_type?: OAuthTokenType
16
- expires_in?: string
17
- }
18
- | {
19
- // iss: string
20
- // state?: string
21
- error: string
22
- error_description?: string
23
- error_uri?: string
24
- }
@@ -1,16 +0,0 @@
1
- import type { LexiconPermissionSet } from '@atproto/lex-document'
2
- import type { Account, Session } from '@atproto/oauth-provider-api'
3
- import type { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
4
- import type { Client } from '../client/client.js'
5
- import type { RequestUri } from '../request/request-uri.js'
6
-
7
- export type AuthorizationResultAuthorizePage = {
8
- issuer: string
9
- client: Client
10
- parameters: OAuthAuthorizationRequestParameters
11
- permissionSets: Map<string, LexiconPermissionSet>
12
-
13
- requestUri: RequestUri
14
- sessions: readonly Session[]
15
- selectedDid?: Account['did']
16
- }
@@ -1,8 +0,0 @@
1
- import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
2
- import { AuthorizationRedirectParameters } from './authorization-redirect-parameters.js'
3
-
4
- export type AuthorizationResultRedirect = {
5
- issuer: string
6
- parameters: OAuthAuthorizationRequestParameters
7
- redirect: AuthorizationRedirectParameters
8
- }