@atproto/oauth-provider 0.9.2 → 0.9.3
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.
- package/CHANGELOG.md +16 -0
- package/dist/client/client.js +6 -6
- package/dist/client/client.js.map +1 -1
- package/dist/device/device-manager.js +1 -1
- package/dist/device/device-manager.js.map +1 -1
- package/dist/dpop/dpop-manager.js +15 -15
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/errors/access-denied-error.d.ts +4 -7
- package/dist/errors/access-denied-error.d.ts.map +1 -1
- package/dist/errors/access-denied-error.js +4 -13
- package/dist/errors/access-denied-error.js.map +1 -1
- package/dist/errors/account-selection-required-error.d.ts +2 -2
- package/dist/errors/account-selection-required-error.d.ts.map +1 -1
- package/dist/errors/account-selection-required-error.js +2 -2
- package/dist/errors/account-selection-required-error.js.map +1 -1
- package/dist/errors/authorization-error.d.ts +10 -0
- package/dist/errors/authorization-error.d.ts.map +1 -0
- package/dist/errors/authorization-error.js +31 -0
- package/dist/errors/authorization-error.js.map +1 -0
- package/dist/errors/consent-required-error.d.ts +2 -2
- package/dist/errors/consent-required-error.d.ts.map +1 -1
- package/dist/errors/consent-required-error.js +2 -2
- package/dist/errors/consent-required-error.js.map +1 -1
- package/dist/errors/error-parser.d.ts.map +1 -1
- package/dist/errors/error-parser.js +2 -1
- package/dist/errors/error-parser.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
- package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js +2 -2
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/errors/invalid-scope-error.d.ts +2 -2
- package/dist/errors/invalid-scope-error.d.ts.map +1 -1
- package/dist/errors/invalid-scope-error.js +2 -2
- package/dist/errors/invalid-scope-error.js.map +1 -1
- package/dist/errors/login-required-error.d.ts +2 -3
- package/dist/errors/login-required-error.d.ts.map +1 -1
- package/dist/errors/login-required-error.js +2 -7
- package/dist/errors/login-required-error.js.map +1 -1
- package/dist/lib/http/response.d.ts +4 -4
- package/dist/lib/http/response.d.ts.map +1 -1
- package/dist/lib/http/response.js +8 -7
- package/dist/lib/http/response.js.map +1 -1
- package/dist/lib/http/stream.d.ts +1 -0
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +6 -0
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/util/error.d.ts +2 -0
- package/dist/lib/util/error.d.ts.map +1 -0
- package/dist/lib/util/error.js +11 -0
- package/dist/lib/util/error.js.map +1 -0
- package/dist/lib/util/zod-error.d.ts +3 -1
- package/dist/lib/util/zod-error.d.ts.map +1 -1
- package/dist/lib/util/zod-error.js +20 -10
- package/dist/lib/util/zod-error.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -1
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +1 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -2
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-hooks.js +4 -3
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +18 -21
- package/dist/oauth-provider.js.map +1 -1
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +12 -12
- package/dist/request/request-manager.js.map +1 -1
- package/dist/router/create-api-middleware.d.ts.map +1 -1
- package/dist/router/create-api-middleware.js +60 -45
- package/dist/router/create-api-middleware.js.map +1 -1
- package/dist/router/create-authorization-page-middleware.d.ts.map +1 -1
- package/dist/router/create-authorization-page-middleware.js +19 -17
- package/dist/router/create-authorization-page-middleware.js.map +1 -1
- package/dist/router/create-oauth-middleware.d.ts.map +1 -1
- package/dist/router/create-oauth-middleware.js +21 -18
- package/dist/router/create-oauth-middleware.js.map +1 -1
- package/dist/router/send-redirect.js +2 -2
- package/dist/router/send-redirect.js.map +1 -1
- package/dist/token/token-manager.js +1 -1
- package/dist/types/authorization-response-error.d.ts +5 -0
- package/dist/types/authorization-response-error.d.ts.map +1 -0
- package/dist/types/authorization-response-error.js +21 -0
- package/dist/types/authorization-response-error.js.map +1 -0
- package/dist/types/par-response-error.d.ts +5 -0
- package/dist/types/par-response-error.d.ts.map +1 -0
- package/dist/types/par-response-error.js +22 -0
- package/dist/types/par-response-error.js.map +1 -0
- package/package.json +5 -5
- package/src/client/client.ts +6 -6
- package/src/device/device-manager.ts +1 -1
- package/src/dpop/dpop-manager.ts +16 -16
- package/src/errors/access-denied-error.ts +6 -33
- package/src/errors/account-selection-required-error.ts +2 -2
- package/src/errors/authorization-error.ts +45 -0
- package/src/errors/consent-required-error.ts +2 -2
- package/src/errors/error-parser.ts +2 -1
- package/src/errors/invalid-authorization-details-error.ts +2 -2
- package/src/errors/invalid-scope-error.ts +2 -2
- package/src/errors/login-required-error.ts +2 -12
- package/src/lib/http/response.ts +14 -13
- package/src/lib/http/stream.ts +6 -0
- package/src/lib/util/error.ts +7 -0
- package/src/lib/util/zod-error.ts +23 -11
- package/src/oauth-errors.ts +1 -1
- package/src/oauth-hooks.ts +3 -2
- package/src/oauth-provider.ts +18 -28
- package/src/request/request-manager.ts +12 -18
- package/src/router/create-api-middleware.ts +84 -62
- package/src/router/create-authorization-page-middleware.ts +19 -21
- package/src/router/create-oauth-middleware.ts +28 -27
- package/src/router/send-redirect.ts +2 -2
- package/src/token/token-manager.ts +1 -1
- package/src/types/authorization-response-error.ts +27 -0
- package/src/types/par-response-error.ts +25 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/errors/invalid-parameters-error.d.ts +0 -6
- package/dist/errors/invalid-parameters-error.d.ts.map +0 -1
- package/dist/errors/invalid-parameters-error.js +0 -11
- package/dist/errors/invalid-parameters-error.js.map +0 -1
- package/src/errors/invalid-parameters-error.ts +0 -12
@@ -20,16 +20,22 @@ import {
|
|
20
20
|
import { signInDataSchema } from '../account/sign-in-data.js'
|
21
21
|
import { signUpInputSchema } from '../account/sign-up-input.js'
|
22
22
|
import { DeviceId, deviceIdSchema } from '../device/device-id.js'
|
23
|
-
import {
|
24
|
-
import {
|
23
|
+
import { AuthorizationError } from '../errors/authorization-error.js'
|
24
|
+
import {
|
25
|
+
ErrorPayload,
|
26
|
+
buildErrorPayload,
|
27
|
+
buildErrorStatus,
|
28
|
+
} from '../errors/error-parser.js'
|
25
29
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
26
30
|
import { WWWAuthenticateError } from '../errors/www-authenticate-error.js'
|
27
31
|
import {
|
32
|
+
JsonResponse,
|
28
33
|
Middleware,
|
29
34
|
RequestMetadata,
|
30
35
|
Router,
|
31
36
|
RouterCtx,
|
32
37
|
SubCtx,
|
38
|
+
flushStream,
|
33
39
|
jsonHandler,
|
34
40
|
parseHttpRequest,
|
35
41
|
subCtx,
|
@@ -84,7 +90,7 @@ export function createApiMiddleware<
|
|
84
90
|
schema: verifyHandleSchema,
|
85
91
|
async handler() {
|
86
92
|
await server.accountManager.verifyHandleAvailability(this.input.handle)
|
87
|
-
return { available: true }
|
93
|
+
return { json: { available: true } }
|
88
94
|
},
|
89
95
|
}),
|
90
96
|
)
|
@@ -121,7 +127,8 @@ export function createApiMiddleware<
|
|
121
127
|
requestUri: this.requestUri,
|
122
128
|
})
|
123
129
|
|
124
|
-
|
130
|
+
const json = { account, ephemeralToken }
|
131
|
+
return { json }
|
125
132
|
},
|
126
133
|
}),
|
127
134
|
)
|
@@ -173,7 +180,7 @@ export function createApiMiddleware<
|
|
173
180
|
account.sub,
|
174
181
|
)
|
175
182
|
|
176
|
-
|
183
|
+
const json = {
|
177
184
|
account,
|
178
185
|
ephemeralToken,
|
179
186
|
consentRequired: server.checkConsentRequired(
|
@@ -181,9 +188,12 @@ export function createApiMiddleware<
|
|
181
188
|
authorizedClients.get(clientId),
|
182
189
|
),
|
183
190
|
}
|
191
|
+
|
192
|
+
return { json }
|
184
193
|
}
|
185
194
|
|
186
|
-
|
195
|
+
const json = { account, ephemeralToken }
|
196
|
+
return { json }
|
187
197
|
},
|
188
198
|
}),
|
189
199
|
)
|
@@ -205,7 +215,7 @@ export function createApiMiddleware<
|
|
205
215
|
await server.accountManager.removeDeviceAccount(this.deviceId, sub)
|
206
216
|
}
|
207
217
|
|
208
|
-
return { success: true as const }
|
218
|
+
return { json: { success: true as const } }
|
209
219
|
},
|
210
220
|
}),
|
211
221
|
)
|
@@ -222,7 +232,7 @@ export function createApiMiddleware<
|
|
222
232
|
.strict(),
|
223
233
|
async handler() {
|
224
234
|
await server.accountManager.resetPasswordRequest(this.input)
|
225
|
-
return { success: true }
|
235
|
+
return { json: { success: true } }
|
226
236
|
},
|
227
237
|
}),
|
228
238
|
)
|
@@ -239,7 +249,7 @@ export function createApiMiddleware<
|
|
239
249
|
.strict(),
|
240
250
|
async handler() {
|
241
251
|
await server.accountManager.resetPasswordConfirm(this.input)
|
242
|
-
return { success: true }
|
252
|
+
return { json: { success: true } }
|
243
253
|
},
|
244
254
|
}),
|
245
255
|
)
|
@@ -254,12 +264,14 @@ export function createApiMiddleware<
|
|
254
264
|
this.deviceId,
|
255
265
|
)
|
256
266
|
|
257
|
-
|
267
|
+
const json = deviceAccounts.map(
|
258
268
|
(deviceAccount): ActiveDeviceSession => ({
|
259
269
|
account: deviceAccount.account,
|
260
270
|
loginRequired: server.checkLoginRequired(deviceAccount),
|
261
271
|
}),
|
262
272
|
)
|
273
|
+
|
274
|
+
return { json }
|
263
275
|
},
|
264
276
|
}),
|
265
277
|
)
|
@@ -289,7 +301,7 @@ export function createApiMiddleware<
|
|
289
301
|
// expose the expiration date). This requires a change to the way
|
290
302
|
// TokenInfo are stored (see TokenManager#isTokenExpired and
|
291
303
|
// TokenManager#isTokenInactive).
|
292
|
-
|
304
|
+
const json = tokenInfos.map(({ id, data }): ActiveOAuthSession => {
|
293
305
|
return {
|
294
306
|
tokenId: id,
|
295
307
|
|
@@ -302,6 +314,8 @@ export function createApiMiddleware<
|
|
302
314
|
scope: data.parameters.scope,
|
303
315
|
}
|
304
316
|
})
|
317
|
+
|
318
|
+
return { json }
|
305
319
|
},
|
306
320
|
}),
|
307
321
|
)
|
@@ -318,7 +332,7 @@ export function createApiMiddleware<
|
|
318
332
|
account.sub,
|
319
333
|
)
|
320
334
|
|
321
|
-
|
335
|
+
const json = deviceAccounts.map(
|
322
336
|
(accountSession): ActiveAccountSession => ({
|
323
337
|
deviceId: accountSession.deviceId,
|
324
338
|
deviceMetadata: {
|
@@ -331,6 +345,8 @@ export function createApiMiddleware<
|
|
331
345
|
isCurrentDevice: accountSession.deviceId === this.deviceId,
|
332
346
|
}),
|
333
347
|
)
|
348
|
+
|
349
|
+
return { json }
|
334
350
|
},
|
335
351
|
}),
|
336
352
|
)
|
@@ -350,7 +366,7 @@ export function createApiMiddleware<
|
|
350
366
|
this.input.sub,
|
351
367
|
)
|
352
368
|
|
353
|
-
return { success: true }
|
369
|
+
return { json: { success: true } }
|
354
370
|
},
|
355
371
|
}),
|
356
372
|
)
|
@@ -374,7 +390,7 @@ export function createApiMiddleware<
|
|
374
390
|
|
375
391
|
await server.tokenManager.deleteToken(tokenInfo.id)
|
376
392
|
|
377
|
-
return { success: true }
|
393
|
+
return { json: { success: true } }
|
378
394
|
},
|
379
395
|
}),
|
380
396
|
)
|
@@ -391,7 +407,7 @@ export function createApiMiddleware<
|
|
391
407
|
)
|
392
408
|
}
|
393
409
|
|
394
|
-
// Any
|
410
|
+
// Any AuthorizationError caught in this block will result in a redirect
|
395
411
|
// to the client's redirect_uri with an error.
|
396
412
|
try {
|
397
413
|
const { clientId, parameters } = await server.requestManager.get(
|
@@ -400,7 +416,7 @@ export function createApiMiddleware<
|
|
400
416
|
)
|
401
417
|
|
402
418
|
// Any error thrown in this block will be transformed into an
|
403
|
-
//
|
419
|
+
// AuthorizationError.
|
404
420
|
try {
|
405
421
|
const { account, authorizedClients } = await authenticate.call(
|
406
422
|
this,
|
@@ -436,13 +452,15 @@ export function createApiMiddleware<
|
|
436
452
|
|
437
453
|
const url = buildRedirectUrl(server.issuer, parameters, { code })
|
438
454
|
|
439
|
-
return { url }
|
455
|
+
return { json: { url } }
|
440
456
|
} catch (err) {
|
441
457
|
// Since we have access to the parameters, we can re-throw an
|
442
|
-
//
|
443
|
-
throw
|
458
|
+
// AuthorizationError with the redirect_uri parameter.
|
459
|
+
throw AuthorizationError.from(parameters, err)
|
444
460
|
}
|
445
461
|
} catch (err) {
|
462
|
+
onError?.(req, res, err, 'Failed to accept authorization request')
|
463
|
+
|
446
464
|
// If any error happened (unauthenticated, invalid request, etc.),
|
447
465
|
// lets make sure the request can no longer be used.
|
448
466
|
try {
|
@@ -451,20 +469,24 @@ export function createApiMiddleware<
|
|
451
469
|
onError?.(req, res, err, 'Failed to delete request')
|
452
470
|
}
|
453
471
|
|
454
|
-
if (err instanceof
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
err.toJSON(),
|
462
|
-
)
|
472
|
+
if (err instanceof AuthorizationError) {
|
473
|
+
try {
|
474
|
+
const url = buildRedirectUrl(
|
475
|
+
server.issuer,
|
476
|
+
err.parameters,
|
477
|
+
err.toJSON(),
|
478
|
+
)
|
463
479
|
|
464
|
-
|
480
|
+
return { json: { url } }
|
481
|
+
} catch {
|
482
|
+
// Unable to build redirect URL, ignore
|
483
|
+
}
|
465
484
|
}
|
466
485
|
|
467
|
-
|
486
|
+
// @NOTE Not re-throwing the error here, as the error was already
|
487
|
+
// handled by the `onError` callback, and apiRoute (`apiMiddleware`)
|
488
|
+
// would call `onError` again.
|
489
|
+
return buildErrorJsonResponse(err)
|
468
490
|
}
|
469
491
|
},
|
470
492
|
}),
|
@@ -507,22 +529,25 @@ export function createApiMiddleware<
|
|
507
529
|
error_description: 'The user rejected the request',
|
508
530
|
})
|
509
531
|
|
510
|
-
return { url }
|
532
|
+
return { json: { url } }
|
511
533
|
} catch (err) {
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
534
|
+
onError?.(req, res, err, 'Failed to reject authorization request')
|
535
|
+
|
536
|
+
if (err instanceof AuthorizationError) {
|
537
|
+
try {
|
538
|
+
const url = buildRedirectUrl(
|
539
|
+
server.issuer,
|
540
|
+
err.parameters,
|
541
|
+
err.toJSON(),
|
542
|
+
)
|
521
543
|
|
522
|
-
|
544
|
+
return { json: { url } }
|
545
|
+
} catch {
|
546
|
+
// Unable to build redirect URL, ignore
|
547
|
+
}
|
523
548
|
}
|
524
549
|
|
525
|
-
|
550
|
+
return buildErrorJsonResponse(err)
|
526
551
|
} finally {
|
527
552
|
await server.requestManager.delete(requestUri).catch((err) => {
|
528
553
|
onError?.(req, res, err, 'Failed to delete request')
|
@@ -637,7 +662,7 @@ export function createApiMiddleware<
|
|
637
662
|
this: ApiContext<RouteCtx<C>, InferValidation<S>>,
|
638
663
|
req: Req,
|
639
664
|
res: Res,
|
640
|
-
) => Awaitable<ApiEndpoints[E]['output']
|
665
|
+
) => Awaitable<JsonResponse<ErrorPayload | ApiEndpoints[E]['output']>>
|
641
666
|
}): Middleware<C, Req, Res> {
|
642
667
|
return createRoute(
|
643
668
|
options.method,
|
@@ -659,12 +684,12 @@ export function createApiMiddleware<
|
|
659
684
|
this: ApiContext<C, InferValidation<S>>,
|
660
685
|
req: Req,
|
661
686
|
res: Res,
|
662
|
-
) =>
|
687
|
+
) => Awaitable<JsonResponse>
|
663
688
|
}): Middleware<C, Req, Res> {
|
664
689
|
const parseInput: (this: C, req: Req) => Promise<InferValidation<S>> =
|
665
690
|
schema == null // No schema means endpoint doesn't accept any input
|
666
691
|
? async function (req) {
|
667
|
-
req
|
692
|
+
await flushStream(req)
|
668
693
|
return undefined
|
669
694
|
}
|
670
695
|
: method === 'POST'
|
@@ -673,12 +698,7 @@ export function createApiMiddleware<
|
|
673
698
|
return schema.parseAsync(body, { path: ['body'] })
|
674
699
|
}
|
675
700
|
: async function (req) {
|
676
|
-
|
677
|
-
req.resume().once('error', (_err) => {
|
678
|
-
// Ignore errors when flushing the request body
|
679
|
-
// (e.g. client closed connection)
|
680
|
-
})
|
681
|
-
|
701
|
+
await flushStream(req)
|
682
702
|
const query = Object.fromEntries(this.url.searchParams)
|
683
703
|
return schema.parseAsync(query, { path: ['query'] })
|
684
704
|
}
|
@@ -726,30 +746,32 @@ export function createApiMiddleware<
|
|
726
746
|
rotateDeviceCookies,
|
727
747
|
)
|
728
748
|
|
729
|
-
const context = subCtx(this, {
|
749
|
+
const context: ApiContext<C, InferValidation<S>> = subCtx(this, {
|
730
750
|
input,
|
731
751
|
requestUri,
|
732
752
|
deviceId,
|
733
753
|
deviceMetadata,
|
734
754
|
})
|
735
755
|
|
736
|
-
|
737
|
-
const payload = await handler.call(context, req, res)
|
738
|
-
|
739
|
-
return { payload, status: 200 }
|
756
|
+
return await handler.call(context, req, res)
|
740
757
|
} catch (err) {
|
741
|
-
onError?.(req, res, err,
|
742
|
-
|
743
|
-
// @TODO Rework the API error responses (relying on codes)
|
744
|
-
const payload = buildErrorPayload(err)
|
745
|
-
const status = buildErrorStatus(err)
|
758
|
+
onError?.(req, res, err, `Failed to handle API request`)
|
746
759
|
|
747
|
-
return
|
760
|
+
// Make sore to always return a JSON response
|
761
|
+
return buildErrorJsonResponse(err)
|
748
762
|
}
|
749
763
|
})
|
750
764
|
}
|
751
765
|
}
|
752
766
|
|
767
|
+
function buildErrorJsonResponse(err: unknown) {
|
768
|
+
// @TODO Rework the API error responses (relying on codes)
|
769
|
+
const json = buildErrorPayload(err)
|
770
|
+
const status = buildErrorStatus(err)
|
771
|
+
|
772
|
+
return { json, status }
|
773
|
+
}
|
774
|
+
|
753
775
|
function buildRedirectUrl(
|
754
776
|
iss: string,
|
755
777
|
parameters: OAuthAuthorizationRequestParameters,
|
@@ -3,7 +3,7 @@ import {
|
|
3
3
|
oauthAuthorizationRequestQuerySchema,
|
4
4
|
oauthClientCredentialsSchema,
|
5
5
|
} from '@atproto/oauth-types'
|
6
|
-
import {
|
6
|
+
import { AuthorizationError } from '../errors/authorization-error.js'
|
7
7
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
8
8
|
import {
|
9
9
|
Middleware,
|
@@ -15,8 +15,8 @@ import {
|
|
15
15
|
validateOrigin,
|
16
16
|
validateReferrer,
|
17
17
|
} from '../lib/http/index.js'
|
18
|
+
import { formatError } from '../lib/util/error.js'
|
18
19
|
import type { Awaitable } from '../lib/util/type.js'
|
19
|
-
import { extractZodErrorMessage } from '../lib/util/zod-error.js'
|
20
20
|
import type { OAuthProvider } from '../oauth-provider.js'
|
21
21
|
import { requestUriSchema } from '../request/request-uri.js'
|
22
22
|
import { AuthorizationResultRedirect } from '../result/authorization-result-redirect.js'
|
@@ -62,7 +62,7 @@ export function createAuthorizationPageMiddleware<
|
|
62
62
|
|
63
63
|
const clientCredentials = await oauthClientCredentialsSchema
|
64
64
|
.parseAsync(query, { path: ['query'] })
|
65
|
-
.catch(throwInvalidRequest)
|
65
|
+
.catch((err) => throwInvalidRequest(err, 'Invalid client credentials'))
|
66
66
|
|
67
67
|
if ('client_secret' in clientCredentials) {
|
68
68
|
throw new InvalidRequestError('Client secret must not be provided')
|
@@ -70,7 +70,7 @@ export function createAuthorizationPageMiddleware<
|
|
70
70
|
|
71
71
|
const authorizationRequest = await oauthAuthorizationRequestQuerySchema
|
72
72
|
.parseAsync(query, { path: ['query'] })
|
73
|
-
.catch(throwInvalidRequest)
|
73
|
+
.catch((err) => throwInvalidRequest(err, 'Invalid request parameters'))
|
74
74
|
|
75
75
|
const deviceInfo = await server.deviceManager.load(req, res)
|
76
76
|
|
@@ -88,20 +88,21 @@ export function createAuthorizationPageMiddleware<
|
|
88
88
|
return sendAuthorizePage(req, res, result)
|
89
89
|
}
|
90
90
|
} catch (err) {
|
91
|
-
|
92
|
-
|
93
|
-
if (err instanceof
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
91
|
+
onError?.(req, res, err, 'Authorization request denied')
|
92
|
+
|
93
|
+
if (err instanceof AuthorizationError) {
|
94
|
+
try {
|
95
|
+
return sendAuthorizeRedirect(res, {
|
96
|
+
issuer: server.issuer,
|
97
|
+
parameters: err.parameters,
|
98
|
+
redirect: err.toJSON(),
|
99
|
+
})
|
100
|
+
} catch {
|
101
|
+
// If we fail to send the redirect, we fall back to sending an error
|
102
|
+
}
|
102
103
|
}
|
103
104
|
|
104
|
-
|
105
|
+
return sendErrorPage(req, res, err)
|
105
106
|
}
|
106
107
|
}),
|
107
108
|
)
|
@@ -155,11 +156,8 @@ export function createAuthorizationPageMiddleware<
|
|
155
156
|
}
|
156
157
|
}
|
157
158
|
|
158
|
-
function throwInvalidRequest(err: unknown): never {
|
159
|
-
throw new InvalidRequestError(
|
160
|
-
extractZodErrorMessage(err) ?? 'Input validation error',
|
161
|
-
err,
|
162
|
-
)
|
159
|
+
function throwInvalidRequest(err: unknown, prefix: string): never {
|
160
|
+
throw new InvalidRequestError(formatError(err, prefix), err)
|
163
161
|
}
|
164
162
|
|
165
163
|
function sendAuthorizeRedirect(
|
@@ -19,7 +19,8 @@ import {
|
|
19
19
|
parseHttpRequest,
|
20
20
|
staticJsonMiddleware,
|
21
21
|
} from '../lib/http/index.js'
|
22
|
-
import {
|
22
|
+
import { formatError } from '../lib/util/error.js'
|
23
|
+
import { OAuthError } from '../oauth-errors.js'
|
23
24
|
import type { OAuthProvider } from '../oauth-provider.js'
|
24
25
|
import type { MiddlewareOptions } from './middleware-options.js'
|
25
26
|
|
@@ -98,11 +99,13 @@ export function createOAuthMiddleware<
|
|
98
99
|
|
99
100
|
const credentials = await oauthClientCredentialsSchema
|
100
101
|
.parseAsync(payload, { path: ['body'] })
|
101
|
-
.catch(throwInvalidClient)
|
102
|
+
.catch((err) => throwInvalidClient(err, 'Client credentials missing'))
|
102
103
|
|
103
104
|
const authorizationRequest = await oauthAuthorizationRequestParSchema
|
104
105
|
.parseAsync(payload, { path: ['body'] })
|
105
|
-
.catch(
|
106
|
+
.catch((err) =>
|
107
|
+
throwInvalidRequest(err, 'Invalid authorization request'),
|
108
|
+
)
|
106
109
|
|
107
110
|
const dpopProof = await server.checkDpopProof(
|
108
111
|
req.method!,
|
@@ -135,11 +138,11 @@ export function createOAuthMiddleware<
|
|
135
138
|
|
136
139
|
const clientCredentials = await oauthClientCredentialsSchema
|
137
140
|
.parseAsync(payload, { path: ['body'] })
|
138
|
-
.catch(throwInvalidGrant)
|
141
|
+
.catch((err) => throwInvalidGrant(err, 'Client credentials missing'))
|
139
142
|
|
140
143
|
const tokenRequest = await oauthTokenRequestSchema
|
141
144
|
.parseAsync(payload, { path: ['body'] })
|
142
|
-
.catch(throwInvalidGrant)
|
145
|
+
.catch((err) => throwInvalidGrant(err, 'Invalid request payload'))
|
143
146
|
|
144
147
|
const dpopProof = await server.checkDpopProof(
|
145
148
|
req.method!,
|
@@ -165,11 +168,11 @@ export function createOAuthMiddleware<
|
|
165
168
|
|
166
169
|
const credentials = await oauthClientCredentialsSchema
|
167
170
|
.parseAsync(payload, { path: ['body'] })
|
168
|
-
.catch(throwInvalidRequest)
|
171
|
+
.catch((err) => throwInvalidRequest(err, 'Client credentials missing'))
|
169
172
|
|
170
173
|
const tokenIdentification = await oauthTokenIdentificationSchema
|
171
174
|
.parseAsync(payload, { path: ['body'] })
|
172
|
-
.catch(throwInvalidRequest)
|
175
|
+
.catch((err) => throwInvalidRequest(err, 'Invalid request payload'))
|
173
176
|
|
174
177
|
const dpopProof = await server.checkDpopProof(
|
175
178
|
req.method!,
|
@@ -214,10 +217,17 @@ export function createOAuthMiddleware<
|
|
214
217
|
res.appendHeader('Access-Control-Expose-Headers', name)
|
215
218
|
}
|
216
219
|
|
217
|
-
const
|
218
|
-
return {
|
220
|
+
const json = await buildOAuthResponse.call(this, req, res)
|
221
|
+
return { json, status }
|
219
222
|
} catch (err) {
|
220
|
-
onError?.(
|
223
|
+
onError?.(
|
224
|
+
req,
|
225
|
+
res,
|
226
|
+
err,
|
227
|
+
err instanceof OAuthError
|
228
|
+
? `OAuth "${err.error}" error`
|
229
|
+
: 'Unexpected error',
|
230
|
+
)
|
221
231
|
|
222
232
|
if (!res.headersSent && err instanceof WWWAuthenticateError) {
|
223
233
|
const name = 'WWW-Authenticate'
|
@@ -226,31 +236,22 @@ export function createOAuthMiddleware<
|
|
226
236
|
}
|
227
237
|
|
228
238
|
const status = buildErrorStatus(err)
|
229
|
-
const
|
239
|
+
const json = buildErrorPayload(err)
|
230
240
|
|
231
|
-
return {
|
241
|
+
return { json, status }
|
232
242
|
}
|
233
243
|
})
|
234
244
|
}
|
235
245
|
}
|
236
246
|
|
237
|
-
function throwInvalidGrant(err: unknown): never {
|
238
|
-
throw new InvalidGrantError(
|
239
|
-
extractZodErrorMessage(err) ?? 'Invalid grant',
|
240
|
-
err,
|
241
|
-
)
|
247
|
+
function throwInvalidGrant(err: unknown, prefix: string): never {
|
248
|
+
throw new InvalidGrantError(formatError(err, prefix), err)
|
242
249
|
}
|
243
250
|
|
244
|
-
function throwInvalidClient(err: unknown): never {
|
245
|
-
throw new InvalidClientError(
|
246
|
-
extractZodErrorMessage(err) ?? 'Client authentication failed',
|
247
|
-
err,
|
248
|
-
)
|
251
|
+
function throwInvalidClient(err: unknown, prefix: string): never {
|
252
|
+
throw new InvalidClientError(formatError(err, prefix), err)
|
249
253
|
}
|
250
254
|
|
251
|
-
function throwInvalidRequest(err: unknown): never {
|
252
|
-
throw new InvalidRequestError(
|
253
|
-
extractZodErrorMessage(err) ?? 'Input validation error',
|
254
|
-
err,
|
255
|
-
)
|
255
|
+
function throwInvalidRequest(err: unknown, prefix: string): never {
|
256
|
+
throw new InvalidRequestError(formatError(err, prefix), err)
|
256
257
|
}
|
@@ -3,7 +3,7 @@ import {
|
|
3
3
|
OAuthAuthorizationRequestParameters,
|
4
4
|
OAuthResponseMode,
|
5
5
|
} from '@atproto/oauth-types'
|
6
|
-
import {
|
6
|
+
import { AuthorizationError } from '../errors/authorization-error.js'
|
7
7
|
import { html, js } from '../lib/html/index.js'
|
8
8
|
import { sendWebPage } from '../lib/send-web-page.js'
|
9
9
|
import { AuthorizationRedirectParameters } from '../result/authorization-redirect-parameters.js'
|
@@ -37,7 +37,7 @@ export function buildRedirectUri(
|
|
37
37
|
const uri = parameters.redirect_uri
|
38
38
|
if (uri) return uri
|
39
39
|
|
40
|
-
throw new
|
40
|
+
throw new AuthorizationError(parameters, 'No redirect_uri', 'invalid_request')
|
41
41
|
}
|
42
42
|
|
43
43
|
export function buildRedirectMode(
|
@@ -299,7 +299,7 @@ export class TokenManager {
|
|
299
299
|
// @TODO Add another store method that atomically consumes the refresh token
|
300
300
|
// with a lock.
|
301
301
|
const tokenInfo = await this.findByRefreshToken(token).catch((err) => {
|
302
|
-
throw
|
302
|
+
throw InvalidGrantError.from(err, `Invalid refresh token`)
|
303
303
|
})
|
304
304
|
|
305
305
|
if (!tokenInfo) {
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import { z } from 'zod'
|
2
|
+
import {
|
3
|
+
oauthAuthorizationResponseErrorSchema,
|
4
|
+
oidcAuthorizationResponseErrorSchema,
|
5
|
+
} from '@atproto/oauth-types'
|
6
|
+
|
7
|
+
export const authorizationResponseErrorSchema = z.union([
|
8
|
+
oauthAuthorizationResponseErrorSchema,
|
9
|
+
// OIDC authentication error response are not part of the ATproto flavoured
|
10
|
+
// OAuth but we allow them because they provide better feedback to the client
|
11
|
+
// (in particular when SSO is used).
|
12
|
+
oidcAuthorizationResponseErrorSchema,
|
13
|
+
// This error is defined by rfc9396 (not part of the OAuth 2.1 or OIDC). But
|
14
|
+
// since, in ATproto flavoured OAuth, client registration is a dynamic part of
|
15
|
+
// the authorization process, we allow it.
|
16
|
+
z.literal('invalid_authorization_details'),
|
17
|
+
])
|
18
|
+
|
19
|
+
export type AuthorizationResponseError = z.infer<
|
20
|
+
typeof authorizationResponseErrorSchema
|
21
|
+
>
|
22
|
+
|
23
|
+
export function isAuthorizationResponseError<T>(
|
24
|
+
value: T,
|
25
|
+
): value is T & AuthorizationResponseError {
|
26
|
+
return authorizationResponseErrorSchema.safeParse(value).success
|
27
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { z } from 'zod'
|
2
|
+
import { authorizationResponseErrorSchema } from './authorization-response-error.js'
|
3
|
+
|
4
|
+
// https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1
|
5
|
+
// > Since initial processing of the pushed authorization request does not
|
6
|
+
// > involve resource owner interaction, error codes related to user
|
7
|
+
// > interaction, such as "access_denied", are never returned.
|
8
|
+
|
9
|
+
export const parResponseErrorSchema = z.intersection(
|
10
|
+
authorizationResponseErrorSchema,
|
11
|
+
z.enum([
|
12
|
+
'invalid_request',
|
13
|
+
'unauthorized_client',
|
14
|
+
'unsupported_response_type',
|
15
|
+
'invalid_scope',
|
16
|
+
'server_error',
|
17
|
+
'temporarily_unavailable',
|
18
|
+
]),
|
19
|
+
)
|
20
|
+
|
21
|
+
export type PARResponseError = z.infer<typeof parResponseErrorSchema>
|
22
|
+
|
23
|
+
export function isPARResponseError<T>(value: T): value is T & PARResponseError {
|
24
|
+
return parResponseErrorSchema.safeParse(value).success
|
25
|
+
}
|
@@ -1 +1 @@
|
|
1
|
-
{"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-
|
1
|
+
{"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/authorization-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lib/hcaptcha.ts","./src/lib/redis.ts","./src/lib/send-web-page.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/error.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-info.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/send-redirect.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-error-page.ts","./src/signer/api-token-payload.ts","./src/signer/signed-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/token/verify-token-claims.ts","./src/types/authorization-response-error.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/par-response-error.ts","./src/types/password.ts","./src/types/rgb-color.ts"],"version":"5.8.3"}
|