@atproto/oauth-provider 0.9.2 → 0.10.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.
- package/CHANGELOG.md +38 -0
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +0 -7
- package/dist/client/client-manager.js.map +1 -1
- 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/metadata/build-metadata.d.ts +0 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +6 -2
- package/dist/metadata/build-metadata.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 +17 -3
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-hooks.js +7 -4
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-provider.d.ts +1 -0
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +35 -49
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +2 -1
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/request/request-manager.d.ts +5 -5
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +63 -45
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store.d.ts +6 -6
- package/dist/request/request-store.d.ts.map +1 -1
- package/dist/result/authorization-result-authorize-page.d.ts +2 -3
- package/dist/result/authorization-result-authorize-page.d.ts.map +1 -1
- package/dist/router/assets/send-authorization-page.js +3 -2
- package/dist/router/assets/send-authorization-page.js.map +1 -1
- package/dist/router/create-api-middleware.d.ts.map +1 -1
- package/dist/router/create-api-middleware.js +68 -48
- 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/token/verify-token-claims.d.ts +1 -0
- package/dist/token/verify-token-claims.d.ts.map +1 -1
- package/dist/token/verify-token-claims.js.map +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 +7 -6
- package/src/client/client-manager.ts +0 -8
- 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/metadata/build-metadata.ts +8 -3
- package/src/oauth-errors.ts +1 -1
- package/src/oauth-hooks.ts +18 -2
- package/src/oauth-provider.ts +37 -58
- package/src/oauth-verifier.ts +3 -1
- package/src/request/request-manager.ts +76 -55
- package/src/request/request-store.ts +6 -6
- package/src/result/authorization-result-authorize-page.ts +2 -3
- package/src/router/assets/send-authorization-page.ts +3 -3
- package/src/router/create-api-middleware.ts +92 -64
- 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/token/verify-token-claims.ts +8 -0
- 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/dist/request/request-info.d.ts +0 -14
- package/dist/request/request-info.d.ts.map +0 -1
- package/dist/request/request-info.js +0 -3
- package/dist/request/request-info.js.map +0 -1
- package/src/errors/invalid-parameters-error.ts +0 -12
- package/src/request/request-info.ts +0 -14
@@ -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
|
)
|
@@ -382,8 +398,13 @@ export function createApiMiddleware<
|
|
382
398
|
router.use(
|
383
399
|
apiRoute({
|
384
400
|
method: 'POST',
|
385
|
-
endpoint: '/
|
386
|
-
schema: z
|
401
|
+
endpoint: '/consent',
|
402
|
+
schema: z
|
403
|
+
.object({
|
404
|
+
sub: z.union([subSchema, signedJwtSchema]),
|
405
|
+
scope: z.string().optional(),
|
406
|
+
})
|
407
|
+
.strict(),
|
387
408
|
async handler(req, res) {
|
388
409
|
if (!this.requestUri) {
|
389
410
|
throw new InvalidRequestError(
|
@@ -391,7 +412,7 @@ export function createApiMiddleware<
|
|
391
412
|
)
|
392
413
|
}
|
393
414
|
|
394
|
-
// Any
|
415
|
+
// Any AuthorizationError caught in this block will result in a redirect
|
395
416
|
// to the client's redirect_uri with an error.
|
396
417
|
try {
|
397
418
|
const { clientId, parameters } = await server.requestManager.get(
|
@@ -400,7 +421,7 @@ export function createApiMiddleware<
|
|
400
421
|
)
|
401
422
|
|
402
423
|
// Any error thrown in this block will be transformed into an
|
403
|
-
//
|
424
|
+
// AuthorizationError.
|
404
425
|
try {
|
405
426
|
const { account, authorizedClients } = await authenticate.call(
|
406
427
|
this,
|
@@ -416,6 +437,7 @@ export function createApiMiddleware<
|
|
416
437
|
account,
|
417
438
|
this.deviceId,
|
418
439
|
this.deviceMetadata,
|
440
|
+
this.input.scope,
|
419
441
|
)
|
420
442
|
|
421
443
|
const clientData = authorizedClients.get(clientId)
|
@@ -436,13 +458,15 @@ export function createApiMiddleware<
|
|
436
458
|
|
437
459
|
const url = buildRedirectUrl(server.issuer, parameters, { code })
|
438
460
|
|
439
|
-
return { url }
|
461
|
+
return { json: { url } }
|
440
462
|
} catch (err) {
|
441
463
|
// Since we have access to the parameters, we can re-throw an
|
442
|
-
//
|
443
|
-
throw
|
464
|
+
// AuthorizationError with the redirect_uri parameter.
|
465
|
+
throw AuthorizationError.from(parameters, err)
|
444
466
|
}
|
445
467
|
} catch (err) {
|
468
|
+
onError?.(req, res, err, 'Failed to consent authorization request')
|
469
|
+
|
446
470
|
// If any error happened (unauthenticated, invalid request, etc.),
|
447
471
|
// lets make sure the request can no longer be used.
|
448
472
|
try {
|
@@ -451,20 +475,24 @@ export function createApiMiddleware<
|
|
451
475
|
onError?.(req, res, err, 'Failed to delete request')
|
452
476
|
}
|
453
477
|
|
454
|
-
if (err instanceof
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
err.toJSON(),
|
462
|
-
)
|
478
|
+
if (err instanceof AuthorizationError) {
|
479
|
+
try {
|
480
|
+
const url = buildRedirectUrl(
|
481
|
+
server.issuer,
|
482
|
+
err.parameters,
|
483
|
+
err.toJSON(),
|
484
|
+
)
|
463
485
|
|
464
|
-
|
486
|
+
return { json: { url } }
|
487
|
+
} catch {
|
488
|
+
// Unable to build redirect URL, ignore
|
489
|
+
}
|
465
490
|
}
|
466
491
|
|
467
|
-
|
492
|
+
// @NOTE Not re-throwing the error here, as the error was already
|
493
|
+
// handled by the `onError` callback, and apiRoute (`apiMiddleware`)
|
494
|
+
// would call `onError` again.
|
495
|
+
return buildErrorJsonResponse(err)
|
468
496
|
}
|
469
497
|
},
|
470
498
|
}),
|
@@ -507,22 +535,25 @@ export function createApiMiddleware<
|
|
507
535
|
error_description: 'The user rejected the request',
|
508
536
|
})
|
509
537
|
|
510
|
-
return { url }
|
538
|
+
return { json: { url } }
|
511
539
|
} catch (err) {
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
540
|
+
onError?.(req, res, err, 'Failed to reject authorization request')
|
541
|
+
|
542
|
+
if (err instanceof AuthorizationError) {
|
543
|
+
try {
|
544
|
+
const url = buildRedirectUrl(
|
545
|
+
server.issuer,
|
546
|
+
err.parameters,
|
547
|
+
err.toJSON(),
|
548
|
+
)
|
521
549
|
|
522
|
-
|
550
|
+
return { json: { url } }
|
551
|
+
} catch {
|
552
|
+
// Unable to build redirect URL, ignore
|
553
|
+
}
|
523
554
|
}
|
524
555
|
|
525
|
-
|
556
|
+
return buildErrorJsonResponse(err)
|
526
557
|
} finally {
|
527
558
|
await server.requestManager.delete(requestUri).catch((err) => {
|
528
559
|
onError?.(req, res, err, 'Failed to delete request')
|
@@ -637,7 +668,7 @@ export function createApiMiddleware<
|
|
637
668
|
this: ApiContext<RouteCtx<C>, InferValidation<S>>,
|
638
669
|
req: Req,
|
639
670
|
res: Res,
|
640
|
-
) => Awaitable<ApiEndpoints[E]['output']
|
671
|
+
) => Awaitable<JsonResponse<ErrorPayload | ApiEndpoints[E]['output']>>
|
641
672
|
}): Middleware<C, Req, Res> {
|
642
673
|
return createRoute(
|
643
674
|
options.method,
|
@@ -659,12 +690,12 @@ export function createApiMiddleware<
|
|
659
690
|
this: ApiContext<C, InferValidation<S>>,
|
660
691
|
req: Req,
|
661
692
|
res: Res,
|
662
|
-
) =>
|
693
|
+
) => Awaitable<JsonResponse>
|
663
694
|
}): Middleware<C, Req, Res> {
|
664
695
|
const parseInput: (this: C, req: Req) => Promise<InferValidation<S>> =
|
665
696
|
schema == null // No schema means endpoint doesn't accept any input
|
666
697
|
? async function (req) {
|
667
|
-
req
|
698
|
+
await flushStream(req)
|
668
699
|
return undefined
|
669
700
|
}
|
670
701
|
: method === 'POST'
|
@@ -673,12 +704,7 @@ export function createApiMiddleware<
|
|
673
704
|
return schema.parseAsync(body, { path: ['body'] })
|
674
705
|
}
|
675
706
|
: 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
|
-
|
707
|
+
await flushStream(req)
|
682
708
|
const query = Object.fromEntries(this.url.searchParams)
|
683
709
|
return schema.parseAsync(query, { path: ['query'] })
|
684
710
|
}
|
@@ -726,30 +752,32 @@ export function createApiMiddleware<
|
|
726
752
|
rotateDeviceCookies,
|
727
753
|
)
|
728
754
|
|
729
|
-
const context = subCtx(this, {
|
755
|
+
const context: ApiContext<C, InferValidation<S>> = subCtx(this, {
|
730
756
|
input,
|
731
757
|
requestUri,
|
732
758
|
deviceId,
|
733
759
|
deviceMetadata,
|
734
760
|
})
|
735
761
|
|
736
|
-
|
737
|
-
const payload = await handler.call(context, req, res)
|
738
|
-
|
739
|
-
return { payload, status: 200 }
|
762
|
+
return await handler.call(context, req, res)
|
740
763
|
} 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)
|
764
|
+
onError?.(req, res, err, `Failed to handle API request`)
|
746
765
|
|
747
|
-
return
|
766
|
+
// Make sore to always return a JSON response
|
767
|
+
return buildErrorJsonResponse(err)
|
748
768
|
}
|
749
769
|
})
|
750
770
|
}
|
751
771
|
}
|
752
772
|
|
773
|
+
function buildErrorJsonResponse(err: unknown) {
|
774
|
+
// @TODO Rework the API error responses (relying on codes)
|
775
|
+
const json = buildErrorPayload(err)
|
776
|
+
const status = buildErrorStatus(err)
|
777
|
+
|
778
|
+
return { json, status }
|
779
|
+
}
|
780
|
+
|
753
781
|
function buildRedirectUrl(
|
754
782
|
iss: string,
|
755
783
|
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) {
|
@@ -10,6 +10,14 @@ import { TokenId } from './token-id.js'
|
|
10
10
|
const BEARER = 'Bearer' satisfies OAuthTokenType
|
11
11
|
const DPOP = 'DPoP' satisfies OAuthTokenType
|
12
12
|
|
13
|
+
export type {
|
14
|
+
DpopProof,
|
15
|
+
OAuthAccessToken,
|
16
|
+
OAuthTokenType,
|
17
|
+
SignedTokenPayload,
|
18
|
+
TokenId,
|
19
|
+
}
|
20
|
+
|
13
21
|
export type VerifyTokenClaimsOptions = {
|
14
22
|
/** One of these audience must be included in the token audience(s) */
|
15
23
|
audience?: [string, ...string[]]
|
@@ -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
|
+
}
|