@atproto/oauth-provider 0.2.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +45 -0
- package/dist/account/account-store.d.ts +2 -2
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +3 -3
- package/dist/assets/app/main.js.map +1 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +4 -2
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +127 -118
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client-utils.d.ts +1 -2
- package/dist/client/client-utils.d.ts.map +1 -1
- package/dist/client/client-utils.js +3 -12
- package/dist/client/client-utils.js.map +1 -1
- package/dist/client/client.d.ts +8 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +70 -1
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -2
- package/dist/constants.js.map +1 -1
- package/dist/errors/access-denied-error.d.ts +4 -4
- package/dist/errors/access-denied-error.d.ts.map +1 -1
- package/dist/errors/access-denied-error.js +2 -2
- 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.map +1 -1
- 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.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.map +1 -1
- package/dist/errors/invalid-client-id-error.d.ts +1 -1
- package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
- package/dist/errors/invalid-client-id-error.js +12 -6
- package/dist/errors/invalid-client-id-error.js.map +1 -1
- package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
- package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
- package/dist/errors/invalid-client-metadata-error.js +11 -3
- package/dist/errors/invalid-client-metadata-error.js.map +1 -1
- package/dist/errors/invalid-parameters-error.d.ts +2 -2
- package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
- package/dist/errors/invalid-parameters-error.js.map +1 -1
- package/dist/errors/invalid-scope-error.d.ts +9 -0
- package/dist/errors/invalid-scope-error.d.ts.map +1 -0
- package/dist/errors/invalid-scope-error.js +14 -0
- package/dist/errors/invalid-scope-error.js.map +1 -0
- package/dist/errors/login-required-error.d.ts +2 -2
- package/dist/errors/login-required-error.d.ts.map +1 -1
- package/dist/errors/login-required-error.js.map +1 -1
- package/dist/lib/html/html.d.ts +1 -1
- package/dist/lib/html/html.d.ts.map +1 -1
- package/dist/lib/html/html.js +14 -11
- package/dist/lib/html/html.js.map +1 -1
- package/dist/lib/http/parser.d.ts +9 -2
- package/dist/lib/http/parser.d.ts.map +1 -1
- package/dist/lib/http/parser.js +15 -7
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/request.d.ts +0 -23
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +1 -11
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/stream.d.ts +28 -6
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +21 -32
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/util/authorization-header.d.ts.map +1 -1
- package/dist/lib/util/authorization-header.js +1 -1
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/lib/util/hostname.d.ts +3 -2
- package/dist/lib/util/hostname.d.ts.map +1 -1
- package/dist/lib/util/hostname.js +12 -8
- package/dist/lib/util/hostname.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +2 -1
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -3
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts +20 -22
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +234 -176
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +2 -2
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/output/build-authorize-data.d.ts +2 -2
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.d.ts +2 -4
- package/dist/output/send-authorize-redirect.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.js +5 -2
- package/dist/output/send-authorize-redirect.js.map +1 -1
- package/dist/request/request-data.d.ts +2 -2
- package/dist/request/request-data.d.ts.map +1 -1
- package/dist/request/request-info.d.ts +2 -2
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +4 -4
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +94 -60
- package/dist/request/request-manager.js.map +1 -1
- package/dist/signer/signed-token-payload.d.ts +122 -122
- package/dist/signer/signer.d.ts +41 -40
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js +13 -15
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +121 -121
- package/dist/token/token-data.d.ts +3 -3
- package/dist/token/token-data.d.ts.map +1 -1
- package/dist/token/token-manager.d.ts +4 -5
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +96 -72
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/verify-token-claims.d.ts +3 -3
- package/dist/token/verify-token-claims.d.ts.map +1 -1
- package/dist/token/verify-token-claims.js.map +1 -1
- package/package.json +5 -4
- package/src/assets/app/components/accept-form.tsx +6 -2
- package/src/assets/app/components/client-name.tsx +10 -11
- package/src/assets/app/components/sign-in-form.tsx +31 -2
- package/src/assets/assets-middleware.ts +4 -2
- package/src/client/client-manager.ts +163 -161
- package/src/client/client-utils.ts +7 -12
- package/src/client/client.ts +112 -3
- package/src/constants.ts +0 -2
- package/src/errors/access-denied-error.ts +10 -4
- package/src/errors/account-selection-required-error.ts +2 -2
- package/src/errors/consent-required-error.ts +2 -2
- package/src/errors/invalid-authorization-details-error.ts +2 -2
- package/src/errors/invalid-client-id-error.ts +15 -4
- package/src/errors/invalid-client-metadata-error.ts +15 -3
- package/src/errors/invalid-parameters-error.ts +2 -2
- package/src/errors/invalid-scope-error.ts +15 -0
- package/src/errors/login-required-error.ts +2 -2
- package/src/lib/html/html.ts +14 -12
- package/src/lib/http/parser.ts +21 -8
- package/src/lib/http/request.ts +1 -23
- package/src/lib/http/stream.ts +29 -60
- package/src/lib/util/authorization-header.ts +5 -2
- package/src/lib/util/hostname.ts +9 -5
- package/src/metadata/build-metadata.ts +3 -1
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-hooks.ts +3 -3
- package/src/oauth-provider.ts +368 -269
- package/src/oauth-verifier.ts +2 -2
- package/src/output/build-authorize-data.ts +2 -2
- package/src/output/send-authorize-redirect.ts +7 -6
- package/src/request/request-data.ts +2 -2
- package/src/request/request-info.ts +2 -2
- package/src/request/request-manager.ts +129 -103
- package/src/signer/signer.ts +24 -25
- package/src/token/token-data.ts +3 -3
- package/src/token/token-manager.ts +141 -99
- package/src/token/verify-token-claims.ts +3 -3
- package/dist/request/types.d.ts +0 -328
- package/dist/request/types.d.ts.map +0 -1
- package/dist/request/types.js +0 -27
- package/dist/request/types.js.map +0 -1
- package/dist/token/types.d.ts +0 -250
- package/dist/token/types.d.ts.map +0 -1
- package/dist/token/types.js +0 -36
- package/dist/token/types.js.map +0 -1
- package/src/request/types.ts +0 -48
- package/src/token/types.ts +0 -86
package/src/oauth-provider.ts
CHANGED
@@ -3,16 +3,31 @@ import { SimpleStore } from '@atproto-labs/simple-store'
|
|
3
3
|
import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
|
4
4
|
import { Jwks, Keyset } from '@atproto/jwk'
|
5
5
|
import {
|
6
|
-
AccessToken,
|
7
6
|
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
8
|
-
|
7
|
+
OAuthAccessToken,
|
8
|
+
OAuthAuthorizationCodeGrantTokenRequest,
|
9
|
+
OAuthAuthorizationRequestJar,
|
10
|
+
OAuthAuthorizationRequestPar,
|
11
|
+
OAuthAuthorizationRequestParameters,
|
12
|
+
OAuthAuthorizationRequestQuery,
|
9
13
|
OAuthAuthorizationServerMetadata,
|
10
|
-
|
14
|
+
OAuthClientCredentials,
|
15
|
+
OAuthClientCredentialsNone,
|
11
16
|
OAuthClientMetadata,
|
17
|
+
OAuthIntrospectionResponse,
|
18
|
+
OAuthParResponse,
|
19
|
+
OAuthRefreshTokenGrantTokenRequest,
|
20
|
+
OAuthTokenIdentification,
|
21
|
+
OAuthTokenRequest,
|
12
22
|
OAuthTokenResponse,
|
13
23
|
OAuthTokenType,
|
14
24
|
atprotoLoopbackClientMetadata,
|
15
|
-
|
25
|
+
oauthAuthorizationRequestParSchema,
|
26
|
+
oauthAuthorizationRequestParametersSchema,
|
27
|
+
oauthAuthorizationRequestQuerySchema,
|
28
|
+
oauthClientCredentialsSchema,
|
29
|
+
oauthTokenIdentificationSchema,
|
30
|
+
oauthTokenRequestSchema,
|
16
31
|
} from '@atproto/oauth-types'
|
17
32
|
import { Redis, type RedisOptions } from 'ioredis'
|
18
33
|
import z, { ZodError } from 'zod'
|
@@ -58,6 +73,7 @@ import {
|
|
58
73
|
Router,
|
59
74
|
ServerResponse,
|
60
75
|
combineMiddlewares,
|
76
|
+
parseHttpRequest,
|
61
77
|
setupCsrfToken,
|
62
78
|
staticJsonHandler,
|
63
79
|
validateCsrfToken,
|
@@ -65,7 +81,6 @@ import {
|
|
65
81
|
validateFetchMode,
|
66
82
|
validateFetchSite,
|
67
83
|
validateReferer,
|
68
|
-
validateRequestPayload,
|
69
84
|
validateSameOrigin,
|
70
85
|
writeJson,
|
71
86
|
} from './lib/http/index.js'
|
@@ -86,33 +101,16 @@ import {
|
|
86
101
|
sendAuthorizeRedirect,
|
87
102
|
} from './output/send-authorize-redirect.js'
|
88
103
|
import { ReplayStore, ifReplayStore } from './replay/replay-store.js'
|
104
|
+
import { codeSchema } from './request/code.js'
|
89
105
|
import { RequestInfo } from './request/request-info.js'
|
90
106
|
import { RequestManager } from './request/request-manager.js'
|
91
107
|
import { RequestStoreMemory } from './request/request-store-memory.js'
|
92
108
|
import { RequestStoreRedis } from './request/request-store-redis.js'
|
93
109
|
import { RequestStore, ifRequestStore } from './request/request-store.js'
|
94
110
|
import { RequestUri, requestUriSchema } from './request/request-uri.js'
|
95
|
-
import {
|
96
|
-
AuthorizationRequestJar,
|
97
|
-
AuthorizationRequestQuery,
|
98
|
-
PushedAuthorizationRequest,
|
99
|
-
authorizationRequestQuerySchema,
|
100
|
-
pushedAuthorizationRequestSchema,
|
101
|
-
} from './request/types.js'
|
102
111
|
import { isTokenId } from './token/token-id.js'
|
103
112
|
import { TokenManager } from './token/token-manager.js'
|
104
113
|
import { TokenStore, asTokenStore } from './token/token-store.js'
|
105
|
-
import {
|
106
|
-
CodeGrantRequest,
|
107
|
-
Introspect,
|
108
|
-
IntrospectionResponse,
|
109
|
-
RefreshGrantRequest,
|
110
|
-
Revoke,
|
111
|
-
TokenRequest,
|
112
|
-
introspectSchema,
|
113
|
-
revokeSchema,
|
114
|
-
tokenRequestSchema,
|
115
|
-
} from './token/types.js'
|
116
114
|
import { VerifyTokenClaimsOptions } from './token/verify-token-claims.js'
|
117
115
|
|
118
116
|
export type OAuthProviderStore = Partial<
|
@@ -312,7 +310,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
312
310
|
|
313
311
|
protected loginRequired(
|
314
312
|
client: Client,
|
315
|
-
parameters:
|
313
|
+
parameters: OAuthAuthorizationRequestParameters,
|
316
314
|
info: DeviceAccountInfo,
|
317
315
|
) {
|
318
316
|
/** in seconds */
|
@@ -327,38 +325,57 @@ export class OAuthProvider extends OAuthVerifier {
|
|
327
325
|
}
|
328
326
|
|
329
327
|
protected async authenticateClient(
|
330
|
-
|
331
|
-
|
332
|
-
|
328
|
+
credentials: OAuthClientCredentials,
|
329
|
+
): Promise<[Client, ClientAuth]> {
|
330
|
+
const client = await this.clientManager.getClient(credentials.client_id)
|
333
331
|
const { clientAuth, nonce } = await client.verifyCredentials(credentials, {
|
334
332
|
audience: this.issuer,
|
335
333
|
})
|
336
334
|
|
335
|
+
if (
|
336
|
+
client.metadata.application_type === 'native' &&
|
337
|
+
clientAuth.method !== 'none'
|
338
|
+
) {
|
339
|
+
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
340
|
+
//
|
341
|
+
// > Except when using a mechanism like Dynamic Client Registration
|
342
|
+
// > [RFC7591] to provision per-instance secrets, native apps are
|
343
|
+
// > classified as public clients, as defined by Section 2.1 of OAuth 2.0
|
344
|
+
// > [RFC6749]; they MUST be registered with the authorization server as
|
345
|
+
// > such. Authorization servers MUST record the client type in the client
|
346
|
+
// > registration details in order to identify and process requests
|
347
|
+
// > accordingly.
|
348
|
+
|
349
|
+
throw new InvalidGrantError(
|
350
|
+
'Native clients must authenticate using "none" method',
|
351
|
+
)
|
352
|
+
}
|
353
|
+
|
337
354
|
if (nonce != null) {
|
338
355
|
const unique = await this.replayManager.uniqueAuth(nonce, client.id)
|
339
356
|
if (!unique) {
|
340
|
-
throw new
|
357
|
+
throw new InvalidGrantError(`${clientAuth.method} jti reused`)
|
341
358
|
}
|
342
359
|
}
|
343
360
|
|
344
|
-
return clientAuth
|
361
|
+
return [client, clientAuth]
|
345
362
|
}
|
346
363
|
|
347
364
|
protected async decodeJAR(
|
348
365
|
client: Client,
|
349
|
-
input:
|
366
|
+
input: OAuthAuthorizationRequestJar,
|
350
367
|
): Promise<
|
351
368
|
| {
|
352
|
-
payload:
|
369
|
+
payload: OAuthAuthorizationRequestParameters
|
353
370
|
}
|
354
371
|
| {
|
355
|
-
payload:
|
372
|
+
payload: OAuthAuthorizationRequestParameters
|
356
373
|
protectedHeader: { kid: string; alg: string }
|
357
374
|
jkt: string
|
358
375
|
}
|
359
376
|
> {
|
360
377
|
const result = await client.decodeRequestObject(input.request)
|
361
|
-
const payload =
|
378
|
+
const payload = oauthAuthorizationRequestParametersSchema.parse(
|
362
379
|
result.payload,
|
363
380
|
)
|
364
381
|
|
@@ -405,17 +422,17 @@ export class OAuthProvider extends OAuthVerifier {
|
|
405
422
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc9126}
|
406
423
|
*/
|
407
424
|
protected async pushedAuthorizationRequest(
|
408
|
-
|
425
|
+
credentials: OAuthClientCredentials,
|
426
|
+
authorizationRequest: OAuthAuthorizationRequestPar,
|
409
427
|
dpopJkt: null | string,
|
410
|
-
) {
|
428
|
+
): Promise<OAuthParResponse> {
|
411
429
|
try {
|
412
|
-
const client = await this.
|
413
|
-
const clientAuth = await this.authenticateClient(client, input)
|
430
|
+
const [client, clientAuth] = await this.authenticateClient(credentials)
|
414
431
|
|
415
432
|
const { payload: parameters } =
|
416
|
-
'request' in
|
417
|
-
? await this.decodeJAR(client,
|
418
|
-
: { payload:
|
433
|
+
'request' in authorizationRequest // Handle JAR
|
434
|
+
? await this.decodeJAR(client, authorizationRequest)
|
435
|
+
: { payload: authorizationRequest }
|
419
436
|
|
420
437
|
const { uri, expiresAt } =
|
421
438
|
await this.requestManager.createAuthorizationRequest(
|
@@ -443,19 +460,21 @@ export class OAuthProvider extends OAuthVerifier {
|
|
443
460
|
}
|
444
461
|
}
|
445
462
|
|
446
|
-
private async
|
463
|
+
private async processAuthorizationRequest(
|
447
464
|
client: Client,
|
448
465
|
deviceId: DeviceId,
|
449
|
-
|
466
|
+
query: OAuthAuthorizationRequestQuery,
|
450
467
|
): Promise<RequestInfo> {
|
451
|
-
|
452
|
-
|
453
|
-
|
468
|
+
if ('request_uri' in query) {
|
469
|
+
const requestUri = await requestUriSchema
|
470
|
+
.parseAsync(query.request_uri, { path: ['query', 'request_uri'] })
|
471
|
+
.catch(throwInvalidRequest)
|
472
|
+
|
473
|
+
return this.requestManager.get(requestUri, client.id, deviceId)
|
454
474
|
}
|
455
475
|
|
456
|
-
|
457
|
-
|
458
|
-
const requestObject = await this.decodeJAR(client, input)
|
476
|
+
if ('request' in query) {
|
477
|
+
const requestObject = await this.decodeJAR(client, query)
|
459
478
|
|
460
479
|
if ('protectedHeader' in requestObject && requestObject.protectedHeader) {
|
461
480
|
// Allow using signed JAR during "/authorize" as client authentication.
|
@@ -488,7 +507,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
488
507
|
return this.requestManager.createAuthorizationRequest(
|
489
508
|
client,
|
490
509
|
{ method: 'none' },
|
491
|
-
|
510
|
+
query,
|
492
511
|
deviceId,
|
493
512
|
null,
|
494
513
|
)
|
@@ -496,7 +515,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
496
515
|
|
497
516
|
private async deleteRequest(
|
498
517
|
uri: RequestUri,
|
499
|
-
parameters:
|
518
|
+
parameters: OAuthAuthorizationRequestParameters,
|
500
519
|
) {
|
501
520
|
try {
|
502
521
|
await this.requestManager.delete(uri)
|
@@ -505,107 +524,113 @@ export class OAuthProvider extends OAuthVerifier {
|
|
505
524
|
}
|
506
525
|
}
|
507
526
|
|
527
|
+
/**
|
528
|
+
* @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
|
529
|
+
*/
|
508
530
|
protected async authorize(
|
509
531
|
deviceId: DeviceId,
|
510
|
-
|
532
|
+
credentials: OAuthClientCredentialsNone,
|
533
|
+
query: OAuthAuthorizationRequestQuery,
|
511
534
|
): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorize> {
|
512
535
|
const { issuer } = this
|
513
|
-
const client = await this.clientManager.getClient(input.client_id)
|
514
|
-
|
515
|
-
try {
|
516
|
-
const { uri, parameters, clientAuth } =
|
517
|
-
await this.loadAuthorizationRequest(client, deviceId, input)
|
518
536
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
if (parameters.prompt === 'none') {
|
528
|
-
const ssoSessions = sessions.filter((s) => s.matchesHint)
|
529
|
-
if (ssoSessions.length > 1) {
|
530
|
-
throw new AccountSelectionRequiredError(parameters)
|
531
|
-
}
|
532
|
-
if (ssoSessions.length < 1) {
|
533
|
-
throw new LoginRequiredError(parameters)
|
537
|
+
// If there is a chance to redirect the user to the client, let's do
|
538
|
+
// it by wrapping the error in an AccessDeniedError.
|
539
|
+
const accessDeniedCatcher =
|
540
|
+
'redirect_uri' in query
|
541
|
+
? (err: unknown): never => {
|
542
|
+
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1
|
543
|
+
throw AccessDeniedError.from(query, err, 'invalid_request')
|
534
544
|
}
|
545
|
+
: null
|
535
546
|
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
}
|
540
|
-
if (ssoSession.consentRequired) {
|
541
|
-
throw new ConsentRequiredError(parameters)
|
542
|
-
}
|
547
|
+
const client = await this.clientManager
|
548
|
+
.getClient(credentials.client_id)
|
549
|
+
.catch(accessDeniedCatcher)
|
543
550
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
551
|
+
const { clientAuth, parameters, uri } =
|
552
|
+
await this.processAuthorizationRequest(client, deviceId, query).catch(
|
553
|
+
accessDeniedCatcher,
|
554
|
+
)
|
555
|
+
|
556
|
+
try {
|
557
|
+
const sessions = await this.getSessions(
|
558
|
+
client,
|
559
|
+
clientAuth,
|
560
|
+
deviceId,
|
561
|
+
parameters,
|
562
|
+
)
|
550
563
|
|
551
|
-
|
564
|
+
if (parameters.prompt === 'none') {
|
565
|
+
const ssoSessions = sessions.filter((s) => s.matchesHint)
|
566
|
+
if (ssoSessions.length > 1) {
|
567
|
+
throw new AccountSelectionRequiredError(parameters)
|
568
|
+
}
|
569
|
+
if (ssoSessions.length < 1) {
|
570
|
+
throw new LoginRequiredError(parameters)
|
552
571
|
}
|
553
572
|
|
554
|
-
|
555
|
-
if (
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
const code = await this.requestManager.setAuthorized(
|
561
|
-
client,
|
562
|
-
uri,
|
563
|
-
deviceId,
|
564
|
-
ssoSession.account,
|
565
|
-
)
|
566
|
-
|
567
|
-
return { issuer, client, parameters, redirect: { code } }
|
568
|
-
}
|
569
|
-
}
|
573
|
+
const ssoSession = ssoSessions[0]!
|
574
|
+
if (ssoSession.loginRequired) {
|
575
|
+
throw new LoginRequiredError(parameters)
|
576
|
+
}
|
577
|
+
if (ssoSession.consentRequired) {
|
578
|
+
throw new ConsentRequiredError(parameters)
|
570
579
|
}
|
571
580
|
|
572
|
-
|
573
|
-
issuer,
|
581
|
+
const code = await this.requestManager.setAuthorized(
|
574
582
|
client,
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
scopeDetails: parameters.scope
|
580
|
-
?.split(/\s+/)
|
581
|
-
.filter(Boolean)
|
582
|
-
.sort((a, b) => a.localeCompare(b))
|
583
|
-
.map((scope) => ({
|
584
|
-
scope,
|
585
|
-
// @TODO Allow to customize the scope descriptions (e.g.
|
586
|
-
// using a hook)
|
587
|
-
description: undefined,
|
588
|
-
})),
|
589
|
-
},
|
590
|
-
}
|
591
|
-
} catch (err) {
|
592
|
-
await this.deleteRequest(uri, parameters)
|
583
|
+
uri,
|
584
|
+
deviceId,
|
585
|
+
ssoSession.account,
|
586
|
+
)
|
593
587
|
|
594
|
-
|
595
|
-
// to the client with the error details.
|
596
|
-
throw AccessDeniedError.from(parameters, err)
|
588
|
+
return { issuer, client, parameters, redirect: { code } }
|
597
589
|
}
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
590
|
+
|
591
|
+
// Automatic SSO when a did was provided
|
592
|
+
if (parameters.prompt == null && parameters.login_hint != null) {
|
593
|
+
const ssoSessions = sessions.filter((s) => s.matchesHint)
|
594
|
+
if (ssoSessions.length === 1) {
|
595
|
+
const ssoSession = ssoSessions[0]!
|
596
|
+
if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
|
597
|
+
const code = await this.requestManager.setAuthorized(
|
598
|
+
client,
|
599
|
+
uri,
|
600
|
+
deviceId,
|
601
|
+
ssoSession.account,
|
602
|
+
)
|
603
|
+
|
604
|
+
return { issuer, client, parameters, redirect: { code } }
|
605
|
+
}
|
605
606
|
}
|
606
607
|
}
|
607
608
|
|
608
|
-
|
609
|
+
return {
|
610
|
+
issuer,
|
611
|
+
client,
|
612
|
+
parameters,
|
613
|
+
authorize: {
|
614
|
+
uri,
|
615
|
+
sessions,
|
616
|
+
scopeDetails: parameters.scope
|
617
|
+
?.split(/\s+/)
|
618
|
+
.filter(Boolean)
|
619
|
+
.sort((a, b) => a.localeCompare(b))
|
620
|
+
.map((scope) => ({
|
621
|
+
scope,
|
622
|
+
// @TODO Allow to customize the scope descriptions (e.g.
|
623
|
+
// using a hook)
|
624
|
+
description: undefined,
|
625
|
+
})),
|
626
|
+
},
|
627
|
+
}
|
628
|
+
} catch (err) {
|
629
|
+
await this.deleteRequest(uri, parameters)
|
630
|
+
|
631
|
+
// Not using accessDeniedCatcher here because "parameters" will most
|
632
|
+
// likely contain the redirect_uri (using the client default).
|
633
|
+
throw AccessDeniedError.from(parameters, err)
|
609
634
|
}
|
610
635
|
}
|
611
636
|
|
@@ -613,7 +638,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
613
638
|
client: Client,
|
614
639
|
clientAuth: ClientAuth,
|
615
640
|
deviceId: DeviceId,
|
616
|
-
parameters:
|
641
|
+
parameters: OAuthAuthorizationRequestParameters,
|
617
642
|
): Promise<
|
618
643
|
{
|
619
644
|
account: Account
|
@@ -702,52 +727,42 @@ export class OAuthProvider extends OAuthVerifier {
|
|
702
727
|
const { issuer } = this
|
703
728
|
const client = await this.clientManager.getClient(clientId)
|
704
729
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
)
|
711
|
-
|
712
|
-
try {
|
713
|
-
const { account, info } = await this.accountManager.get(deviceId, sub)
|
730
|
+
const { parameters, clientAuth } = await this.requestManager.get(
|
731
|
+
uri,
|
732
|
+
clientId,
|
733
|
+
deviceId,
|
734
|
+
)
|
714
735
|
|
715
|
-
|
716
|
-
|
717
|
-
throw new LoginRequiredError(
|
718
|
-
parameters,
|
719
|
-
'Account authentication required.',
|
720
|
-
)
|
721
|
-
}
|
736
|
+
try {
|
737
|
+
const { account, info } = await this.accountManager.get(deviceId, sub)
|
722
738
|
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
739
|
+
// The user is trying to authorize without a fresh login
|
740
|
+
if (this.loginRequired(client, parameters, info)) {
|
741
|
+
throw new LoginRequiredError(
|
742
|
+
parameters,
|
743
|
+
'Account authentication required.',
|
728
744
|
)
|
745
|
+
}
|
729
746
|
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
747
|
+
const code = await this.requestManager.setAuthorized(
|
748
|
+
client,
|
749
|
+
uri,
|
750
|
+
deviceId,
|
751
|
+
account,
|
752
|
+
)
|
736
753
|
|
737
|
-
|
738
|
-
|
739
|
-
|
754
|
+
await this.accountManager.addAuthorizedClient(
|
755
|
+
deviceId,
|
756
|
+
account,
|
757
|
+
client,
|
758
|
+
clientAuth,
|
759
|
+
)
|
740
760
|
|
741
|
-
|
742
|
-
throw err
|
743
|
-
}
|
761
|
+
return { issuer, parameters, redirect: { code } }
|
744
762
|
} catch (err) {
|
745
|
-
|
746
|
-
const { parameters } = err
|
747
|
-
return { issuer, client, parameters, redirect: err.toJSON() }
|
748
|
-
}
|
763
|
+
await this.deleteRequest(uri, parameters)
|
749
764
|
|
750
|
-
throw err
|
765
|
+
throw AccessDeniedError.from(parameters, err)
|
751
766
|
}
|
752
767
|
}
|
753
768
|
|
@@ -756,69 +771,69 @@ export class OAuthProvider extends OAuthVerifier {
|
|
756
771
|
uri: RequestUri,
|
757
772
|
clientId: ClientId,
|
758
773
|
): Promise<AuthorizationResultRedirect> {
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
)
|
774
|
+
const { parameters } = await this.requestManager.get(
|
775
|
+
uri,
|
776
|
+
clientId,
|
777
|
+
deviceId,
|
778
|
+
)
|
765
779
|
|
766
|
-
|
780
|
+
await this.deleteRequest(uri, parameters)
|
767
781
|
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
parameters: err.parameters,
|
776
|
-
redirect: err.toJSON(),
|
777
|
-
}
|
778
|
-
}
|
779
|
-
|
780
|
-
throw err
|
782
|
+
return {
|
783
|
+
issuer: this.issuer,
|
784
|
+
parameters: parameters,
|
785
|
+
redirect: {
|
786
|
+
error: 'access_denied',
|
787
|
+
error_description: 'Access denied',
|
788
|
+
},
|
781
789
|
}
|
782
790
|
}
|
783
791
|
|
784
792
|
protected async token(
|
785
|
-
|
793
|
+
credentials: OAuthClientCredentials,
|
794
|
+
request: OAuthTokenRequest,
|
786
795
|
dpopJkt: null | string,
|
787
796
|
): Promise<OAuthTokenResponse> {
|
788
|
-
const client = await this.
|
789
|
-
|
797
|
+
const [client, clientAuth] = await this.authenticateClient(credentials)
|
798
|
+
|
799
|
+
if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {
|
800
|
+
throw new InvalidGrantError(
|
801
|
+
`Grant type "${request.grant_type}" is not supported by the server`,
|
802
|
+
)
|
803
|
+
}
|
790
804
|
|
791
|
-
if (!client.metadata.grant_types.includes(
|
805
|
+
if (!client.metadata.grant_types.includes(request.grant_type)) {
|
792
806
|
throw new InvalidGrantError(
|
793
|
-
`"${
|
807
|
+
`"${request.grant_type}" grant type is not allowed for this client`,
|
794
808
|
)
|
795
809
|
}
|
796
810
|
|
797
|
-
if (
|
798
|
-
return this.codeGrant(client, clientAuth,
|
811
|
+
if (request.grant_type === 'authorization_code') {
|
812
|
+
return this.codeGrant(client, clientAuth, request, dpopJkt)
|
799
813
|
}
|
800
814
|
|
801
|
-
if (
|
802
|
-
return this.refreshTokenGrant(client, clientAuth,
|
815
|
+
if (request.grant_type === 'refresh_token') {
|
816
|
+
return this.refreshTokenGrant(client, clientAuth, request, dpopJkt)
|
803
817
|
}
|
804
818
|
|
805
819
|
throw new InvalidGrantError(
|
806
|
-
|
807
|
-
`Grant type "${input.grant_type}" not supported`,
|
820
|
+
`Grant type "${request.grant_type}" not supported`,
|
808
821
|
)
|
809
822
|
}
|
810
823
|
|
811
824
|
protected async codeGrant(
|
812
825
|
client: Client,
|
813
826
|
clientAuth: ClientAuth,
|
814
|
-
input:
|
827
|
+
input: OAuthAuthorizationCodeGrantTokenRequest,
|
815
828
|
dpopJkt: null | string,
|
816
829
|
): Promise<OAuthTokenResponse> {
|
817
830
|
try {
|
831
|
+
const code = codeSchema.parse(input.code)
|
832
|
+
|
818
833
|
const { sub, deviceId, parameters } = await this.requestManager.findCode(
|
819
834
|
client,
|
820
835
|
clientAuth,
|
821
|
-
|
836
|
+
code,
|
822
837
|
)
|
823
838
|
|
824
839
|
// the following check prevents re-use of PKCE challenges, enforcing the
|
@@ -874,7 +889,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
874
889
|
async refreshTokenGrant(
|
875
890
|
client: Client,
|
876
891
|
clientAuth: ClientAuth,
|
877
|
-
input:
|
892
|
+
input: OAuthRefreshTokenGrantTokenRequest,
|
878
893
|
dpopJkt: null | string,
|
879
894
|
): Promise<OAuthTokenResponse> {
|
880
895
|
return this.tokenManager.refresh(client, clientAuth, input, dpopJkt)
|
@@ -883,20 +898,20 @@ export class OAuthProvider extends OAuthVerifier {
|
|
883
898
|
/**
|
884
899
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 rfc7009}
|
885
900
|
*/
|
886
|
-
protected async revoke(
|
901
|
+
protected async revoke({ token }: OAuthTokenIdentification) {
|
887
902
|
// @TODO this should also remove the account-device association (or, at
|
888
903
|
// least, mark it as expired)
|
889
|
-
await this.tokenManager.revoke(
|
904
|
+
await this.tokenManager.revoke(token)
|
890
905
|
}
|
891
906
|
|
892
907
|
/**
|
893
908
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 rfc7662}
|
894
909
|
*/
|
895
910
|
protected async introspect(
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
const clientAuth = await this.authenticateClient(
|
911
|
+
credentials: OAuthClientCredentials,
|
912
|
+
{ token }: OAuthTokenIdentification,
|
913
|
+
): Promise<OAuthIntrospectionResponse> {
|
914
|
+
const [client, clientAuth] = await this.authenticateClient(credentials)
|
900
915
|
|
901
916
|
// RFC7662 states the following:
|
902
917
|
//
|
@@ -915,7 +930,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
915
930
|
const tokenInfo = await this.tokenManager.clientTokenInfo(
|
916
931
|
client,
|
917
932
|
clientAuth,
|
918
|
-
|
933
|
+
token,
|
919
934
|
)
|
920
935
|
|
921
936
|
return {
|
@@ -946,7 +961,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
946
961
|
|
947
962
|
protected override async authenticateToken(
|
948
963
|
tokenType: OAuthTokenType,
|
949
|
-
token:
|
964
|
+
token: OAuthAccessToken,
|
950
965
|
dpopJkt: string | null,
|
951
966
|
verifyOptions?: VerifyTokenClaimsOptions,
|
952
967
|
) {
|
@@ -981,12 +996,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
981
996
|
T = void,
|
982
997
|
Req extends IncomingMessage = IncomingMessage,
|
983
998
|
Res extends ServerResponse = ServerResponse,
|
984
|
-
>({
|
985
|
-
onError = process.env['NODE_ENV'] === 'development'
|
986
|
-
? (req, res, err, msg): void =>
|
987
|
-
console.error(`OAuthProvider error (${msg}):`, err)
|
988
|
-
: undefined,
|
989
|
-
}: RouterOptions<Req, Res> = {}) {
|
999
|
+
>(options?: RouterOptions<Req, Res>) {
|
990
1000
|
const deviceManager = new DeviceManager(this.deviceStore)
|
991
1001
|
const outputManager = new OutputManager(this.customization)
|
992
1002
|
|
@@ -999,6 +1009,12 @@ export class OAuthProvider extends OAuthVerifier {
|
|
999
1009
|
// Utils
|
1000
1010
|
|
1001
1011
|
const csrfCookie = (uri: RequestUri) => `csrf-${uri}`
|
1012
|
+
const onError =
|
1013
|
+
options?.onError ??
|
1014
|
+
(process.env['NODE_ENV'] === 'development'
|
1015
|
+
? (req, res, err, msg): void =>
|
1016
|
+
console.error(`OAuthProvider error (${msg}):`, err)
|
1017
|
+
: undefined)
|
1002
1018
|
|
1003
1019
|
/**
|
1004
1020
|
* Creates a middleware that will serve static JSON content.
|
@@ -1059,7 +1075,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1059
1075
|
// OAuthError are used to build expected responses, so we don't log
|
1060
1076
|
// them as errors.
|
1061
1077
|
if (!(err instanceof OAuthError) || err.statusCode >= 500) {
|
1062
|
-
|
1078
|
+
onError?.(req, res, err, 'Unexpected error')
|
1063
1079
|
}
|
1064
1080
|
}
|
1065
1081
|
}
|
@@ -1086,7 +1102,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1086
1102
|
throw new Error('Navigation handler did not send a response')
|
1087
1103
|
}
|
1088
1104
|
} catch (err) {
|
1089
|
-
|
1105
|
+
onError?.(
|
1090
1106
|
req,
|
1091
1107
|
res,
|
1092
1108
|
err,
|
@@ -1099,6 +1115,30 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1099
1115
|
}
|
1100
1116
|
}
|
1101
1117
|
|
1118
|
+
/**
|
1119
|
+
* Provides a better UX when a request is denied by redirecting to the
|
1120
|
+
* client with the error details. This will also log any error that caused
|
1121
|
+
* the access to be denied (such as system errors).
|
1122
|
+
*/
|
1123
|
+
const accessDeniedToRedirectCatcher = (
|
1124
|
+
req: Req,
|
1125
|
+
res: Res,
|
1126
|
+
err: unknown,
|
1127
|
+
): AuthorizationResultRedirect => {
|
1128
|
+
if (err instanceof AccessDeniedError && err.parameters.redirect_uri) {
|
1129
|
+
const { cause } = err
|
1130
|
+
if (cause) onError?.(req, res, cause, 'Access denied')
|
1131
|
+
|
1132
|
+
return {
|
1133
|
+
issuer: server.issuer,
|
1134
|
+
parameters: err.parameters,
|
1135
|
+
redirect: err.toJSON(),
|
1136
|
+
}
|
1137
|
+
}
|
1138
|
+
|
1139
|
+
throw err
|
1140
|
+
}
|
1141
|
+
|
1102
1142
|
//- Public OAuth endpoints
|
1103
1143
|
|
1104
1144
|
router.get(
|
@@ -1139,10 +1179,15 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1139
1179
|
router.post(
|
1140
1180
|
'/oauth/par',
|
1141
1181
|
jsonHandler(async function (req, _res) {
|
1142
|
-
const
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1182
|
+
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
1183
|
+
|
1184
|
+
const credentials = await oauthClientCredentialsSchema
|
1185
|
+
.parseAsync(payload, { path: ['body'] })
|
1186
|
+
.catch(throwInvalidRequest)
|
1187
|
+
|
1188
|
+
const authorizationRequest = await oauthAuthorizationRequestParSchema
|
1189
|
+
.parseAsync(payload, { path: ['body'] })
|
1190
|
+
.catch(throwInvalidRequest)
|
1146
1191
|
|
1147
1192
|
const dpopJkt = await server.checkDpopProof(
|
1148
1193
|
req.headers['dpop'],
|
@@ -1150,7 +1195,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1150
1195
|
this.url,
|
1151
1196
|
)
|
1152
1197
|
|
1153
|
-
return server.pushedAuthorizationRequest(
|
1198
|
+
return server.pushedAuthorizationRequest(
|
1199
|
+
credentials,
|
1200
|
+
authorizationRequest,
|
1201
|
+
dpopJkt,
|
1202
|
+
)
|
1154
1203
|
}, 201),
|
1155
1204
|
)
|
1156
1205
|
|
@@ -1166,7 +1215,15 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1166
1215
|
router.post(
|
1167
1216
|
'/oauth/token',
|
1168
1217
|
jsonHandler(async function (req, _res) {
|
1169
|
-
const
|
1218
|
+
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
1219
|
+
|
1220
|
+
const credentials = await oauthClientCredentialsSchema
|
1221
|
+
.parseAsync(payload, { path: ['body'] })
|
1222
|
+
.catch(throwInvalidClient)
|
1223
|
+
|
1224
|
+
const tokenRequest = await oauthTokenRequestSchema
|
1225
|
+
.parseAsync(payload, { path: ['body'] })
|
1226
|
+
.catch(throwInvalidGrant)
|
1170
1227
|
|
1171
1228
|
const dpopJkt = await server.checkDpopProof(
|
1172
1229
|
req.headers['dpop'],
|
@@ -1174,7 +1231,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1174
1231
|
this.url,
|
1175
1232
|
)
|
1176
1233
|
|
1177
|
-
return server.token(
|
1234
|
+
return server.token(credentials, tokenRequest, dpopJkt)
|
1178
1235
|
}),
|
1179
1236
|
)
|
1180
1237
|
|
@@ -1182,10 +1239,14 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1182
1239
|
router.post(
|
1183
1240
|
'/oauth/revoke',
|
1184
1241
|
jsonHandler(async function (req, res) {
|
1185
|
-
const
|
1242
|
+
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
1243
|
+
|
1244
|
+
const tokenIdentification = await oauthTokenIdentificationSchema
|
1245
|
+
.parseAsync(payload, { path: ['body'] })
|
1246
|
+
.catch(throwInvalidRequest)
|
1186
1247
|
|
1187
1248
|
try {
|
1188
|
-
await server.revoke(
|
1249
|
+
await server.revoke(tokenIdentification)
|
1189
1250
|
} catch (err) {
|
1190
1251
|
onError?.(req, res, err, 'Failed to revoke token')
|
1191
1252
|
}
|
@@ -1197,10 +1258,13 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1197
1258
|
'/oauth/revoke',
|
1198
1259
|
navigationHandler(async function (req, res) {
|
1199
1260
|
const query = Object.fromEntries(this.url.searchParams)
|
1200
|
-
|
1261
|
+
|
1262
|
+
const tokenIdentification = await oauthTokenIdentificationSchema
|
1263
|
+
.parseAsync(query, { path: ['query'] })
|
1264
|
+
.catch(throwInvalidRequest)
|
1201
1265
|
|
1202
1266
|
try {
|
1203
|
-
await server.revoke(
|
1267
|
+
await server.revoke(tokenIdentification)
|
1204
1268
|
} catch (err) {
|
1205
1269
|
onError?.(req, res, err, 'Failed to revoke token')
|
1206
1270
|
}
|
@@ -1217,8 +1281,17 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1217
1281
|
router.post(
|
1218
1282
|
'/oauth/introspect',
|
1219
1283
|
jsonHandler(async function (req, _res) {
|
1220
|
-
const
|
1221
|
-
|
1284
|
+
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
1285
|
+
|
1286
|
+
const credentials = await oauthClientCredentialsSchema
|
1287
|
+
.parseAsync(payload, { path: ['body'] })
|
1288
|
+
.catch(throwInvalidRequest)
|
1289
|
+
|
1290
|
+
const tokenIdentification = await oauthTokenIdentificationSchema
|
1291
|
+
.parseAsync(payload, { path: ['body'] })
|
1292
|
+
.catch(throwInvalidRequest)
|
1293
|
+
|
1294
|
+
return server.introspect(credentials, tokenIdentification)
|
1222
1295
|
}),
|
1223
1296
|
)
|
1224
1297
|
|
@@ -1232,12 +1305,24 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1232
1305
|
validateFetchSite(req, res, ['cross-site', 'none'])
|
1233
1306
|
|
1234
1307
|
const query = Object.fromEntries(this.url.searchParams)
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1308
|
+
|
1309
|
+
const credentials = await oauthClientCredentialsSchema
|
1310
|
+
.parseAsync(query, { path: ['body'] })
|
1311
|
+
.catch(throwInvalidRequest)
|
1312
|
+
|
1313
|
+
if ('client_secret' in credentials) {
|
1314
|
+
throw new InvalidRequestError('Client secret must not be provided')
|
1315
|
+
}
|
1316
|
+
|
1317
|
+
const authorizationRequest = await oauthAuthorizationRequestQuerySchema
|
1318
|
+
.parseAsync(query, { path: ['query'] })
|
1319
|
+
.catch(throwInvalidRequest)
|
1238
1320
|
|
1239
1321
|
const { deviceId } = await deviceManager.load(req, res)
|
1240
|
-
|
1322
|
+
|
1323
|
+
const data = await server
|
1324
|
+
.authorize(deviceId, credentials, authorizationRequest)
|
1325
|
+
.catch((err) => accessDeniedToRedirectCatcher(req, res, err))
|
1241
1326
|
|
1242
1327
|
switch (true) {
|
1243
1328
|
case 'redirect' in data: {
|
@@ -1270,7 +1355,10 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1270
1355
|
validateFetchSite(req, res, ['same-origin'])
|
1271
1356
|
validateSameOrigin(req, res, issuerOrigin)
|
1272
1357
|
|
1273
|
-
const
|
1358
|
+
const payload = await parseHttpRequest(req, ['json'])
|
1359
|
+
const input = await signInPayloadSchema.parseAsync(payload, {
|
1360
|
+
path: ['body'],
|
1361
|
+
})
|
1274
1362
|
|
1275
1363
|
validateReferer(req, res, {
|
1276
1364
|
origin: issuerOrigin,
|
@@ -1338,12 +1426,14 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1338
1426
|
|
1339
1427
|
const { deviceId } = await deviceManager.load(req, res)
|
1340
1428
|
|
1341
|
-
const data = await server
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1429
|
+
const data = await server
|
1430
|
+
.acceptRequest(
|
1431
|
+
deviceId,
|
1432
|
+
input.request_uri,
|
1433
|
+
input.client_id,
|
1434
|
+
input.account_sub,
|
1435
|
+
)
|
1436
|
+
.catch((err) => accessDeniedToRedirectCatcher(req, res, err))
|
1347
1437
|
|
1348
1438
|
return await sendAuthorizeRedirect(res, data)
|
1349
1439
|
}),
|
@@ -1392,11 +1482,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1392
1482
|
|
1393
1483
|
const { deviceId } = await deviceManager.load(req, res)
|
1394
1484
|
|
1395
|
-
const data = await server
|
1396
|
-
deviceId,
|
1397
|
-
|
1398
|
-
input.client_id,
|
1399
|
-
)
|
1485
|
+
const data = await server
|
1486
|
+
.rejectRequest(deviceId, input.request_uri, input.client_id)
|
1487
|
+
.catch((err) => accessDeniedToRedirectCatcher(req, res, err))
|
1400
1488
|
|
1401
1489
|
return await sendAuthorizeRedirect(res, data)
|
1402
1490
|
}),
|
@@ -1406,25 +1494,36 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1406
1494
|
}
|
1407
1495
|
}
|
1408
1496
|
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
} catch (err) {
|
1416
|
-
if (err instanceof ZodError) {
|
1417
|
-
const issue = err.issues[0]
|
1418
|
-
if (issue?.path.length) {
|
1419
|
-
// "part" will typically be
|
1420
|
-
const [part, ...path] = issue.path
|
1421
|
-
throw new InvalidRequestError(
|
1422
|
-
`Validation of ${part}'s "${path.join('.')}" with error: ${issue.message}`,
|
1423
|
-
err,
|
1424
|
-
)
|
1425
|
-
}
|
1426
|
-
}
|
1497
|
+
function throwInvalidGrant(err: unknown): never {
|
1498
|
+
throw new InvalidGrantError(
|
1499
|
+
extractZodErrorMessage(err) || 'Invalid grant',
|
1500
|
+
err,
|
1501
|
+
)
|
1502
|
+
}
|
1427
1503
|
|
1428
|
-
|
1504
|
+
function throwInvalidClient(err: unknown): never {
|
1505
|
+
throw new InvalidClientError(
|
1506
|
+
extractZodErrorMessage(err) || 'Client authentication failed',
|
1507
|
+
err,
|
1508
|
+
)
|
1509
|
+
}
|
1510
|
+
|
1511
|
+
function throwInvalidRequest(err: unknown): never {
|
1512
|
+
throw new InvalidRequestError(
|
1513
|
+
extractZodErrorMessage(err) || 'Input validation error',
|
1514
|
+
err,
|
1515
|
+
)
|
1516
|
+
}
|
1517
|
+
|
1518
|
+
function extractZodErrorMessage(err: unknown): string | undefined {
|
1519
|
+
if (err instanceof ZodError) {
|
1520
|
+
const issue = err.issues[0]
|
1521
|
+
if (issue?.path.length) {
|
1522
|
+
// "part" will typically be "body" or "query"
|
1523
|
+
const [part, ...path] = issue.path
|
1524
|
+
return `Validation of "${path.join('.')}" ${part} parameter failed: ${issue.message}`
|
1525
|
+
}
|
1429
1526
|
}
|
1527
|
+
|
1528
|
+
return undefined
|
1430
1529
|
}
|