@atproto/oauth-provider 0.2.0 → 0.2.2
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 +42 -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 +7 -6
- package/src/assets/app/components/sign-in-form.tsx +31 -2
- package/src/assets/app/components/url-viewer.tsx +3 -3
- 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
|
}
|