@atproto/oauth-provider 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +46 -0
- package/dist/account/account.d.ts +6 -2
- package/dist/account/account.d.ts.map +1 -1
- 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 +2 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +7 -0
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/client/client-manager.d.ts +4 -3
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +91 -77
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client.d.ts +2 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +6 -12
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/device/device-manager.d.ts +1 -1
- package/dist/device/device-manager.d.ts.map +1 -1
- package/dist/device/device-manager.js +2 -2
- package/dist/device/device-manager.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts +0 -1
- package/dist/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/dpop/dpop-manager.js +1 -4
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.d.ts +4 -3
- package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js +4 -4
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/lib/http/parser.d.ts +13 -7
- package/dist/lib/http/parser.d.ts.map +1 -1
- package/dist/lib/http/parser.js +29 -9
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/request.d.ts +8 -5
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +24 -12
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +3 -2
- package/dist/lib/http/stream.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 +9 -49
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -10
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts +10 -15
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +176 -114
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +1 -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 +6 -0
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/build-authorize-data.js +1 -0
- package/dist/output/build-authorize-data.js.map +1 -1
- package/dist/replay/replay-manager.d.ts +1 -0
- package/dist/replay/replay-manager.d.ts.map +1 -1
- package/dist/replay/replay-manager.js +3 -0
- package/dist/replay/replay-manager.js.map +1 -1
- package/dist/replay/replay-store.d.ts +1 -1
- package/dist/request/request-info.d.ts +2 -0
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +3 -9
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +52 -77
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/types.d.ts +10 -10
- package/dist/signer/signed-token-payload.d.ts +88 -88
- package/dist/signer/signer.d.ts +24 -31
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js +0 -40
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +84 -84
- package/dist/token/token-manager.d.ts +1 -2
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +10 -37
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/types.d.ts +10 -10
- package/package.json +3 -3
- package/src/account/account.ts +11 -7
- package/src/assets/app/backend-data.ts +9 -2
- package/src/assets/app/components/accept-form.tsx +65 -51
- package/src/assets/app/components/client-name.tsx +24 -16
- package/src/assets/app/components/url-viewer.tsx +3 -3
- package/src/assets/app/views/accept-view.tsx +7 -4
- package/src/assets/app/views/authorize-view.tsx +2 -1
- package/src/assets/assets-middleware.ts +14 -2
- package/src/client/client-manager.ts +124 -120
- package/src/client/client.ts +5 -17
- package/src/constants.ts +3 -0
- package/src/device/device-manager.ts +7 -1
- package/src/dpop/dpop-manager.ts +1 -6
- package/src/errors/invalid-authorization-details-error.ts +9 -4
- package/src/lib/http/parser.ts +37 -13
- package/src/lib/http/request.ts +61 -15
- package/src/lib/http/stream.ts +5 -2
- package/src/metadata/build-metadata.ts +9 -56
- package/src/oauth-hooks.ts +3 -13
- package/src/oauth-provider.ts +187 -177
- package/src/oauth-verifier.ts +1 -2
- package/src/output/build-authorize-data.ts +8 -0
- package/src/replay/replay-manager.ts +9 -0
- package/src/replay/replay-store.ts +1 -1
- package/src/request/request-info.ts +2 -0
- package/src/request/request-manager.ts +81 -107
- package/src/signer/signer.ts +0 -63
- package/src/token/token-manager.ts +8 -41
- package/dist/oidc/claims.d.ts +0 -16
- package/dist/oidc/claims.d.ts.map +0 -1
- package/dist/oidc/claims.js +0 -29
- package/dist/oidc/claims.js.map +0 -1
- package/dist/oidc/userinfo.d.ts +0 -7
- package/dist/oidc/userinfo.d.ts.map +0 -1
- package/dist/oidc/userinfo.js +0 -3
- package/dist/oidc/userinfo.js.map +0 -1
- package/dist/parameters/claims-requested.d.ts +0 -3
- package/dist/parameters/claims-requested.d.ts.map +0 -1
- package/dist/parameters/claims-requested.js +0 -77
- package/dist/parameters/claims-requested.js.map +0 -1
- package/dist/parameters/oidc-payload.d.ts +0 -31
- package/dist/parameters/oidc-payload.d.ts.map +0 -1
- package/dist/parameters/oidc-payload.js +0 -25
- package/dist/parameters/oidc-payload.js.map +0 -1
- package/src/assets/app/components/client-identifier.tsx +0 -31
- package/src/oidc/claims.ts +0 -35
- package/src/oidc/userinfo.ts +0 -11
- package/src/parameters/claims-requested.ts +0 -106
- package/src/parameters/oidc-payload.ts +0 -28
package/src/oauth-provider.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { safeFetchWrap } from '@atproto-labs/fetch-node'
|
2
2
|
import { SimpleStore } from '@atproto-labs/simple-store'
|
3
3
|
import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
|
4
|
-
import { Jwks, Keyset
|
4
|
+
import { Jwks, Keyset } from '@atproto/jwk'
|
5
5
|
import {
|
6
6
|
AccessToken,
|
7
7
|
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
@@ -9,19 +9,17 @@ import {
|
|
9
9
|
OAuthAuthorizationServerMetadata,
|
10
10
|
OAuthClientIdentification,
|
11
11
|
OAuthClientMetadata,
|
12
|
-
OAuthEndpointName,
|
13
12
|
OAuthTokenResponse,
|
14
13
|
OAuthTokenType,
|
15
14
|
atprotoLoopbackClientMetadata,
|
16
15
|
oauthAuthenticationRequestParametersSchema,
|
17
16
|
} from '@atproto/oauth-types'
|
18
17
|
import { Redis, type RedisOptions } from 'ioredis'
|
19
|
-
import {
|
18
|
+
import z, { ZodError } from 'zod'
|
20
19
|
|
21
20
|
import { AccessTokenType } from './access-token/access-token-type.js'
|
22
21
|
import { AccountManager } from './account/account-manager.js'
|
23
22
|
import {
|
24
|
-
AccountInfo,
|
25
23
|
AccountStore,
|
26
24
|
DeviceAccountInfo,
|
27
25
|
SignInCredentials,
|
@@ -59,12 +57,13 @@ import {
|
|
59
57
|
Middleware,
|
60
58
|
Router,
|
61
59
|
ServerResponse,
|
62
|
-
acceptMiddleware,
|
63
60
|
combineMiddlewares,
|
64
61
|
setupCsrfToken,
|
65
62
|
staticJsonHandler,
|
66
63
|
validateCsrfToken,
|
64
|
+
validateFetchDest,
|
67
65
|
validateFetchMode,
|
66
|
+
validateFetchSite,
|
68
67
|
validateReferer,
|
69
68
|
validateRequestPayload,
|
70
69
|
validateSameOrigin,
|
@@ -75,7 +74,6 @@ import { Override } from './lib/util/type.js'
|
|
75
74
|
import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
|
76
75
|
import { OAuthHooks } from './oauth-hooks.js'
|
77
76
|
import { OAuthVerifier, OAuthVerifierOptions } from './oauth-verifier.js'
|
78
|
-
import { Userinfo } from './oidc/userinfo.js'
|
79
77
|
import { AuthorizationResultAuthorize } from './output/build-authorize-data.js'
|
80
78
|
import {
|
81
79
|
buildErrorPayload,
|
@@ -87,7 +85,6 @@ import {
|
|
87
85
|
AuthorizationResultRedirect,
|
88
86
|
sendAuthorizeRedirect,
|
89
87
|
} from './output/send-authorize-redirect.js'
|
90
|
-
import { oidcPayload } from './parameters/oidc-payload.js'
|
91
88
|
import { ReplayStore, ifReplayStore } from './replay/replay-store.js'
|
92
89
|
import { RequestInfo } from './request/request-info.js'
|
93
90
|
import { RequestManager } from './request/request-manager.js'
|
@@ -104,7 +101,7 @@ import {
|
|
104
101
|
} from './request/types.js'
|
105
102
|
import { isTokenId } from './token/token-id.js'
|
106
103
|
import { TokenManager } from './token/token-manager.js'
|
107
|
-
import {
|
104
|
+
import { TokenStore, asTokenStore } from './token/token-store.js'
|
108
105
|
import {
|
109
106
|
CodeGrantRequest,
|
110
107
|
Introspect,
|
@@ -147,9 +144,7 @@ export type OAuthProviderOptions = Override<
|
|
147
144
|
{
|
148
145
|
/**
|
149
146
|
* Maximum age a device/account session can be before requiring
|
150
|
-
* re-authentication.
|
151
|
-
* using the `max_age` parameter and on a client basis using the
|
152
|
-
* `default_max_age` client metadata.
|
147
|
+
* re-authentication.
|
153
148
|
*/
|
154
149
|
authenticationMaxAge?: number
|
155
150
|
|
@@ -287,6 +282,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
287
282
|
|
288
283
|
this.accountManager = new AccountManager(accountStore)
|
289
284
|
this.clientManager = new ClientManager(
|
285
|
+
this.metadata,
|
290
286
|
this.keyset,
|
291
287
|
rest,
|
292
288
|
clientStore || null,
|
@@ -327,26 +323,16 @@ export class OAuthProvider extends OAuthVerifier {
|
|
327
323
|
return true
|
328
324
|
}
|
329
325
|
|
330
|
-
|
331
|
-
const maxAge = parameters.max_age ?? client.metadata.default_max_age
|
332
|
-
|
333
|
-
if (maxAge != null && maxAge < this.authenticationMaxAge) {
|
334
|
-
return authAge >= maxAge
|
335
|
-
} else {
|
336
|
-
return authAge >= this.authenticationMaxAge
|
337
|
-
}
|
326
|
+
return authAge >= this.authenticationMaxAge
|
338
327
|
}
|
339
328
|
|
340
329
|
protected async authenticateClient(
|
341
330
|
client: Client,
|
342
|
-
endpoint: OAuthEndpointName,
|
343
331
|
credentials: OAuthClientIdentification,
|
344
332
|
): Promise<ClientAuth> {
|
345
|
-
const { clientAuth, nonce } = await client.verifyCredentials(
|
346
|
-
|
347
|
-
|
348
|
-
{ audience: this.issuer },
|
349
|
-
)
|
333
|
+
const { clientAuth, nonce } = await client.verifyCredentials(credentials, {
|
334
|
+
audience: this.issuer,
|
335
|
+
})
|
350
336
|
|
351
337
|
if (nonce != null) {
|
352
338
|
const unique = await this.replayManager.uniqueAuth(nonce, client.id)
|
@@ -424,11 +410,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
424
410
|
) {
|
425
411
|
try {
|
426
412
|
const client = await this.clientManager.getClient(input.client_id)
|
427
|
-
const clientAuth = await this.authenticateClient(
|
428
|
-
client,
|
429
|
-
'pushed_authorization_request',
|
430
|
-
input,
|
431
|
-
)
|
413
|
+
const clientAuth = await this.authenticateClient(client, input)
|
432
414
|
|
433
415
|
const { payload: parameters } =
|
434
416
|
'request' in input // Handle JAR
|
@@ -559,15 +541,14 @@ export class OAuthProvider extends OAuthVerifier {
|
|
559
541
|
throw new ConsentRequiredError(parameters)
|
560
542
|
}
|
561
543
|
|
562
|
-
const
|
544
|
+
const code = await this.requestManager.setAuthorized(
|
563
545
|
client,
|
564
546
|
uri,
|
565
547
|
deviceId,
|
566
548
|
ssoSession.account,
|
567
|
-
ssoSession.info,
|
568
549
|
)
|
569
550
|
|
570
|
-
return { issuer, client, parameters, redirect }
|
551
|
+
return { issuer, client, parameters, redirect: { code } }
|
571
552
|
}
|
572
553
|
|
573
554
|
// Automatic SSO when a did was provided
|
@@ -576,15 +557,14 @@ export class OAuthProvider extends OAuthVerifier {
|
|
576
557
|
if (ssoSessions.length === 1) {
|
577
558
|
const ssoSession = ssoSessions[0]!
|
578
559
|
if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
|
579
|
-
const
|
560
|
+
const code = await this.requestManager.setAuthorized(
|
580
561
|
client,
|
581
562
|
uri,
|
582
563
|
deviceId,
|
583
564
|
ssoSession.account,
|
584
|
-
ssoSession.info,
|
585
565
|
)
|
586
566
|
|
587
|
-
return { issuer, client, parameters, redirect }
|
567
|
+
return { issuer, client, parameters, redirect: { code } }
|
588
568
|
}
|
589
569
|
}
|
590
570
|
}
|
@@ -593,7 +573,20 @@ export class OAuthProvider extends OAuthVerifier {
|
|
593
573
|
issuer,
|
594
574
|
client,
|
595
575
|
parameters,
|
596
|
-
authorize: {
|
576
|
+
authorize: {
|
577
|
+
uri,
|
578
|
+
sessions,
|
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
|
+
},
|
597
590
|
}
|
598
591
|
} catch (err) {
|
599
592
|
await this.deleteRequest(uri, parameters)
|
@@ -660,6 +653,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
660
653
|
this.loginRequired(client, parameters, info),
|
661
654
|
consentRequired:
|
662
655
|
parameters.prompt === 'consent' ||
|
656
|
+
// @TODO the "authorizedClients" should also include the scopes that
|
657
|
+
// were already authorized for the client. Otherwise a client could
|
658
|
+
// use silent authentication to get additional scopes without consent.
|
663
659
|
!info.authorizedClients.includes(client.id),
|
664
660
|
|
665
661
|
matchesHint: hint == null || matchesHint(account),
|
@@ -668,9 +664,33 @@ export class OAuthProvider extends OAuthVerifier {
|
|
668
664
|
|
669
665
|
protected async signIn(
|
670
666
|
deviceId: DeviceId,
|
667
|
+
uri: RequestUri,
|
668
|
+
clientId: ClientId,
|
671
669
|
credentials: SignInCredentials,
|
672
|
-
): Promise<
|
673
|
-
|
670
|
+
): Promise<{
|
671
|
+
account: Account
|
672
|
+
consentRequired: boolean
|
673
|
+
}> {
|
674
|
+
const client = await this.clientManager.getClient(clientId)
|
675
|
+
|
676
|
+
// Ensure the request is still valid (and update the request expiration)
|
677
|
+
// @TODO use the returned scopes to determine if consent is required
|
678
|
+
await this.requestManager.get(uri, clientId, deviceId)
|
679
|
+
|
680
|
+
const { account, info } = await this.accountManager.signIn(
|
681
|
+
credentials,
|
682
|
+
deviceId,
|
683
|
+
)
|
684
|
+
|
685
|
+
return {
|
686
|
+
account,
|
687
|
+
consentRequired: client.info.isFirstParty
|
688
|
+
? false
|
689
|
+
: // @TODO: the "authorizedClients" should also include the scopes that
|
690
|
+
// were already authorized for the client. Otherwise a client could
|
691
|
+
// use silent authentication to get additional scopes without consent.
|
692
|
+
!info.authorizedClients.includes(client.id),
|
693
|
+
}
|
674
694
|
}
|
675
695
|
|
676
696
|
protected async acceptRequest(
|
@@ -700,12 +720,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
700
720
|
)
|
701
721
|
}
|
702
722
|
|
703
|
-
const
|
723
|
+
const code = await this.requestManager.setAuthorized(
|
704
724
|
client,
|
705
725
|
uri,
|
706
726
|
deviceId,
|
707
727
|
account,
|
708
|
-
info,
|
709
728
|
)
|
710
729
|
|
711
730
|
await this.accountManager.addAuthorizedClient(
|
@@ -715,7 +734,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
715
734
|
clientAuth,
|
716
735
|
)
|
717
736
|
|
718
|
-
return { issuer, client, parameters, redirect }
|
737
|
+
return { issuer, client, parameters, redirect: { code } }
|
719
738
|
} catch (err) {
|
720
739
|
await this.deleteRequest(uri, parameters)
|
721
740
|
|
@@ -767,7 +786,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
767
786
|
dpopJkt: null | string,
|
768
787
|
): Promise<OAuthTokenResponse> {
|
769
788
|
const client = await this.clientManager.getClient(input.client_id)
|
770
|
-
const clientAuth = await this.authenticateClient(client,
|
789
|
+
const clientAuth = await this.authenticateClient(client, input)
|
771
790
|
|
772
791
|
if (!client.metadata.grant_types.includes(input.grant_type)) {
|
773
792
|
throw new InvalidGrantError(
|
@@ -802,6 +821,32 @@ export class OAuthProvider extends OAuthVerifier {
|
|
802
821
|
input.code,
|
803
822
|
)
|
804
823
|
|
824
|
+
// the following check prevents re-use of PKCE challenges, enforcing the
|
825
|
+
// clients to generate a new challenge for each authorization request. The
|
826
|
+
// replay manager typically prevents replay over a certain time frame,
|
827
|
+
// which might not cover the entire lifetime of the token (depending on
|
828
|
+
// the implementation of the replay store). For this reason, we should
|
829
|
+
// ideally ensure that the code_challenge was not already used by any
|
830
|
+
// existing token or any other pending request.
|
831
|
+
//
|
832
|
+
// The current implementation will cause client devs not issuing a new
|
833
|
+
// code challenge for each authorization request to fail, which should be
|
834
|
+
// a good enough incentive to follow the best practices, until we have a
|
835
|
+
// better implementation.
|
836
|
+
//
|
837
|
+
// @TODO: Use tokenManager to ensure uniqueness of code_challenge
|
838
|
+
if (parameters.code_challenge) {
|
839
|
+
const unique = await this.replayManager.uniqueCodeChallenge(
|
840
|
+
parameters.code_challenge,
|
841
|
+
)
|
842
|
+
if (!unique) {
|
843
|
+
throw new InvalidGrantError(
|
844
|
+
'code_challenge',
|
845
|
+
'Code challenge already used',
|
846
|
+
)
|
847
|
+
}
|
848
|
+
}
|
849
|
+
|
805
850
|
const { account, info } = await this.accountManager.get(deviceId, sub)
|
806
851
|
|
807
852
|
return await this.tokenManager.create(
|
@@ -851,11 +896,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
851
896
|
input: Introspect,
|
852
897
|
): Promise<IntrospectionResponse> {
|
853
898
|
const client = await this.clientManager.getClient(input.client_id)
|
854
|
-
const clientAuth = await this.authenticateClient(
|
855
|
-
client,
|
856
|
-
'introspection',
|
857
|
-
input,
|
858
|
-
)
|
899
|
+
const clientAuth = await this.authenticateClient(client, input)
|
859
900
|
|
860
901
|
// RFC7662 states the following:
|
861
902
|
//
|
@@ -903,31 +944,6 @@ export class OAuthProvider extends OAuthVerifier {
|
|
903
944
|
}
|
904
945
|
}
|
905
946
|
|
906
|
-
/**
|
907
|
-
* @see {@link https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.3.2 Successful UserInfo Response}
|
908
|
-
*/
|
909
|
-
protected async userinfo({ data, account }: TokenInfo): Promise<Userinfo> {
|
910
|
-
return {
|
911
|
-
...oidcPayload(data.parameters, account),
|
912
|
-
|
913
|
-
sub: account.sub,
|
914
|
-
|
915
|
-
client_id: data.clientId,
|
916
|
-
username: account.preferred_username,
|
917
|
-
}
|
918
|
-
}
|
919
|
-
|
920
|
-
protected async signUserinfo(userinfo: Userinfo): Promise<SignedJwt> {
|
921
|
-
const client = await this.clientManager.getClient(userinfo.client_id)
|
922
|
-
return this.signer.sign(
|
923
|
-
{
|
924
|
-
alg: client.metadata.userinfo_signed_response_alg,
|
925
|
-
typ: 'JWT',
|
926
|
-
},
|
927
|
-
userinfo,
|
928
|
-
)
|
929
|
-
}
|
930
|
-
|
931
947
|
protected override async authenticateToken(
|
932
948
|
tokenType: OAuthTokenType,
|
933
949
|
token: AccessToken,
|
@@ -991,6 +1007,8 @@ export class OAuthProvider extends OAuthVerifier {
|
|
991
1007
|
combineMiddlewares([
|
992
1008
|
function (req, res, next) {
|
993
1009
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
1010
|
+
res.setHeader('Access-Control-Allow-Headers', '*')
|
1011
|
+
|
994
1012
|
res.setHeader('Cache-Control', 'max-age=300')
|
995
1013
|
next()
|
996
1014
|
},
|
@@ -1007,6 +1025,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1007
1025
|
): Handler<T, TReq, TRes> =>
|
1008
1026
|
async function (req, res) {
|
1009
1027
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
1028
|
+
res.setHeader('Access-Control-Allow-Headers', '*')
|
1010
1029
|
|
1011
1030
|
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
1012
1031
|
res.setHeader('Cache-Control', 'no-store')
|
@@ -1049,11 +1068,15 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1049
1068
|
handler: (this: T, req: TReq, res: TRes) => void | Promise<void>,
|
1050
1069
|
): Handler<T, TReq, TRes> =>
|
1051
1070
|
async function (req, res) {
|
1071
|
+
res.setHeader('Access-Control-Allow-Origin', '*')
|
1072
|
+
res.setHeader('Access-Control-Allow-Headers', '*')
|
1073
|
+
|
1052
1074
|
res.setHeader('Cache-Control', 'no-store')
|
1053
1075
|
res.setHeader('Pragma', 'no-cache')
|
1054
1076
|
|
1055
1077
|
try {
|
1056
1078
|
validateFetchMode(req, res, ['navigate'])
|
1079
|
+
validateFetchDest(req, res, ['document'])
|
1057
1080
|
validateSameOrigin(req, res, issuerOrigin)
|
1058
1081
|
|
1059
1082
|
await handler.call(this, req, res)
|
@@ -1078,49 +1101,45 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1078
1101
|
|
1079
1102
|
//- Public OAuth endpoints
|
1080
1103
|
|
1081
|
-
/*
|
1082
|
-
* Although OpenID compatibility is not required to implement the Atproto
|
1083
|
-
* OAuth2 specification, we do support OIDC discovery in this
|
1084
|
-
* implementation as we believe this may:
|
1085
|
-
* 1) Make the implementation of Atproto clients easier (since lots of
|
1086
|
-
* libraries support OIDC discovery)
|
1087
|
-
* 2) Allow self hosted PDS' to not implement authentication themselves
|
1088
|
-
* but rely on a trusted Atproto actor to act as their OIDC providers.
|
1089
|
-
* By supporting OIDC in the current implementation, Bluesky's
|
1090
|
-
* Authorization Server server can be used as an OIDC provider for
|
1091
|
-
* these users.
|
1092
|
-
*/
|
1093
|
-
router.get('/.well-known/openid-configuration', staticJson(server.metadata))
|
1094
|
-
|
1095
1104
|
router.get(
|
1096
1105
|
'/.well-known/oauth-authorization-server',
|
1097
1106
|
staticJson(server.metadata),
|
1098
1107
|
)
|
1099
1108
|
|
1100
1109
|
// CORS preflight
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
.
|
1115
|
-
|
1116
|
-
|
1110
|
+
const corsPreflight: Middleware = function (req, res, _next) {
|
1111
|
+
res
|
1112
|
+
.writeHead(204, {
|
1113
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
1114
|
+
//
|
1115
|
+
// > For requests without credentials, the literal value "*" can be
|
1116
|
+
// > specified as a wildcard; the value tells browsers to allow
|
1117
|
+
// > requesting code from any origin to access the resource.
|
1118
|
+
// > Attempting to use the wildcard with credentials results in an
|
1119
|
+
// > error.
|
1120
|
+
//
|
1121
|
+
// A "*" is safer to use than reflecting the request origin.
|
1122
|
+
'Access-Control-Allow-Origin': '*',
|
1123
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
1124
|
+
// > The value "*" only counts as a special wildcard value for
|
1125
|
+
// > requests without credentials (requests without HTTP cookies or
|
1126
|
+
// > HTTP authentication information). In requests with credentials,
|
1127
|
+
// > it is treated as the literal method name "*" without special
|
1128
|
+
// > semantics.
|
1129
|
+
'Access-Control-Allow-Methods': '*',
|
1130
|
+
'Access-Control-Allow-Headers': 'Content-Type,Authorization,DPoP',
|
1131
|
+
'Access-Control-Max-Age': '86400', // 1 day
|
1132
|
+
})
|
1133
|
+
.end()
|
1134
|
+
}
|
1117
1135
|
|
1118
1136
|
router.get('/oauth/jwks', staticJson(server.jwks))
|
1119
1137
|
|
1138
|
+
router.options('/oauth/par', corsPreflight)
|
1120
1139
|
router.post(
|
1121
1140
|
'/oauth/par',
|
1122
1141
|
jsonHandler(async function (req, _res) {
|
1123
|
-
const input = await
|
1142
|
+
const input = await validateRequest(
|
1124
1143
|
req,
|
1125
1144
|
pushedAuthorizationRequestSchema,
|
1126
1145
|
)
|
@@ -1136,14 +1155,18 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1136
1155
|
)
|
1137
1156
|
|
1138
1157
|
// https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
|
1139
|
-
|
1158
|
+
// > If the request did not use the POST method, the authorization server
|
1159
|
+
// > responds with an HTTP 405 (Method Not Allowed) status code.
|
1160
|
+
router.options('/oauth/par', corsPreflight)
|
1161
|
+
router.all('/oauth/par', (req, res) => {
|
1140
1162
|
res.writeHead(405).end()
|
1141
1163
|
})
|
1142
1164
|
|
1165
|
+
router.options('/oauth/token', corsPreflight)
|
1143
1166
|
router.post(
|
1144
1167
|
'/oauth/token',
|
1145
1168
|
jsonHandler(async function (req, _res) {
|
1146
|
-
const input = await
|
1169
|
+
const input = await validateRequest(req, tokenRequestSchema)
|
1147
1170
|
|
1148
1171
|
const dpopJkt = await server.checkDpopProof(
|
1149
1172
|
req.headers['dpop'],
|
@@ -1155,10 +1178,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1155
1178
|
}),
|
1156
1179
|
)
|
1157
1180
|
|
1181
|
+
router.options('/oauth/revoke', corsPreflight)
|
1158
1182
|
router.post(
|
1159
1183
|
'/oauth/revoke',
|
1160
1184
|
jsonHandler(async function (req, res) {
|
1161
|
-
const input = await
|
1185
|
+
const input = await validateRequest(req, revokeSchema)
|
1162
1186
|
|
1163
1187
|
try {
|
1164
1188
|
await server.revoke(input)
|
@@ -1168,6 +1192,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1168
1192
|
}),
|
1169
1193
|
)
|
1170
1194
|
|
1195
|
+
router.options('/oauth/revoke', corsPreflight)
|
1171
1196
|
router.get(
|
1172
1197
|
'/oauth/revoke',
|
1173
1198
|
navigationHandler(async function (req, res) {
|
@@ -1192,69 +1217,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1192
1217
|
router.post(
|
1193
1218
|
'/oauth/introspect',
|
1194
1219
|
jsonHandler(async function (req, _res) {
|
1195
|
-
const input = await
|
1220
|
+
const input = await validateRequest(req, introspectSchema)
|
1196
1221
|
return server.introspect(input)
|
1197
1222
|
}),
|
1198
1223
|
)
|
1199
1224
|
|
1200
|
-
const userinfoBodySchema = z.object({
|
1201
|
-
access_token: signedJwtSchema.optional(),
|
1202
|
-
})
|
1203
|
-
|
1204
|
-
router.addRoute(
|
1205
|
-
['GET', 'POST'],
|
1206
|
-
'/oauth/userinfo',
|
1207
|
-
acceptMiddleware(
|
1208
|
-
async function (req, _res) {
|
1209
|
-
const body =
|
1210
|
-
req.method === 'POST'
|
1211
|
-
? await validateRequestPayload(req, userinfoBodySchema)
|
1212
|
-
: null
|
1213
|
-
|
1214
|
-
if (body?.access_token && req.headers['authorization']) {
|
1215
|
-
throw new InvalidRequestError(
|
1216
|
-
'access token must be provided in either the authorization header or the request body',
|
1217
|
-
)
|
1218
|
-
}
|
1219
|
-
|
1220
|
-
const auth = await server.authenticateRequest(
|
1221
|
-
req.method!,
|
1222
|
-
this.url,
|
1223
|
-
body?.access_token // Allow credentials to be parsed from body.
|
1224
|
-
? {
|
1225
|
-
authorization: `Bearer ${body.access_token}`,
|
1226
|
-
dpop: undefined, // DPoP can only be used with headers
|
1227
|
-
}
|
1228
|
-
: req.headers,
|
1229
|
-
{
|
1230
|
-
scope: ['profile'],
|
1231
|
-
},
|
1232
|
-
)
|
1233
|
-
|
1234
|
-
const tokenInfo: TokenInfo =
|
1235
|
-
'tokenInfo' in auth
|
1236
|
-
? (auth.tokenInfo as TokenInfo)
|
1237
|
-
: await server.tokenManager.getTokenInfo(
|
1238
|
-
auth.tokenType,
|
1239
|
-
auth.tokenId,
|
1240
|
-
)
|
1241
|
-
|
1242
|
-
return server.userinfo(tokenInfo)
|
1243
|
-
},
|
1244
|
-
{
|
1245
|
-
'': 'application/json',
|
1246
|
-
'application/json': jsonHandler(async function (_req, _res) {
|
1247
|
-
return this.data
|
1248
|
-
}),
|
1249
|
-
'application/jwt': jsonHandler(async function (_req, res) {
|
1250
|
-
const jwt = await server.signUserinfo(this.data)
|
1251
|
-
res.writeHead(200, { 'Content-Type': 'application/jwt' }).end(jwt)
|
1252
|
-
return undefined
|
1253
|
-
}),
|
1254
|
-
},
|
1255
|
-
),
|
1256
|
-
)
|
1257
|
-
|
1258
1225
|
//- Private authorization endpoints
|
1259
1226
|
|
1260
1227
|
router.use(authorizeAssetsMiddleware())
|
@@ -1262,6 +1229,8 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1262
1229
|
router.get(
|
1263
1230
|
'/oauth/authorize',
|
1264
1231
|
navigationHandler(async function (req, res) {
|
1232
|
+
validateFetchSite(req, res, ['cross-site', 'none'])
|
1233
|
+
|
1265
1234
|
const query = Object.fromEntries(this.url.searchParams)
|
1266
1235
|
const input = await authorizationRequestQuerySchema.parseAsync(query, {
|
1267
1236
|
path: ['query'],
|
@@ -1293,13 +1262,15 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1293
1262
|
credentials: signInCredentialsSchema,
|
1294
1263
|
})
|
1295
1264
|
|
1265
|
+
router.options('/oauth/authorize/sign-in', corsPreflight)
|
1296
1266
|
router.post(
|
1297
1267
|
'/oauth/authorize/sign-in',
|
1298
1268
|
jsonHandler(async function (req, res) {
|
1299
1269
|
validateFetchMode(req, res, ['same-origin'])
|
1270
|
+
validateFetchSite(req, res, ['same-origin'])
|
1300
1271
|
validateSameOrigin(req, res, issuerOrigin)
|
1301
1272
|
|
1302
|
-
const input = await
|
1273
|
+
const input = await validateRequest(req, signInPayloadSchema)
|
1303
1274
|
|
1304
1275
|
validateReferer(req, res, {
|
1305
1276
|
origin: issuerOrigin,
|
@@ -1312,20 +1283,14 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1312
1283
|
csrfCookie(input.request_uri),
|
1313
1284
|
)
|
1314
1285
|
|
1315
|
-
const { deviceId } = await deviceManager.load(req, res)
|
1286
|
+
const { deviceId } = await deviceManager.load(req, res, true)
|
1316
1287
|
|
1317
|
-
|
1288
|
+
return server.signIn(
|
1318
1289
|
deviceId,
|
1290
|
+
input.request_uri,
|
1291
|
+
input.client_id,
|
1319
1292
|
input.credentials,
|
1320
1293
|
)
|
1321
|
-
|
1322
|
-
// Prevent fixation attacks
|
1323
|
-
await deviceManager.rotate(req, res, deviceId)
|
1324
|
-
|
1325
|
-
return {
|
1326
|
-
account,
|
1327
|
-
consentRequired: !info.authorizedClients.includes(input.client_id),
|
1328
|
-
}
|
1329
1294
|
}),
|
1330
1295
|
)
|
1331
1296
|
|
@@ -1336,9 +1301,20 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1336
1301
|
account_sub: z.string(),
|
1337
1302
|
})
|
1338
1303
|
|
1304
|
+
// Though this is a "no-cors" request, meaning that the browser will allow
|
1305
|
+
// any cross-origin request, with credentials, to be sent, the handler will
|
1306
|
+
// 1) validate the request origin,
|
1307
|
+
// 2) validate the CSRF token,
|
1308
|
+
// 3) validate the referer,
|
1309
|
+
// 4) validate the sec-fetch-site header,
|
1310
|
+
// 4) validate the sec-fetch-mode header,
|
1311
|
+
// 5) validate the sec-fetch-dest header (see navigationHandler).
|
1312
|
+
// And will error if any of these checks fail.
|
1339
1313
|
router.get(
|
1340
1314
|
'/oauth/authorize/accept',
|
1341
1315
|
navigationHandler(async function (req, res) {
|
1316
|
+
validateFetchSite(req, res, ['same-origin'])
|
1317
|
+
|
1342
1318
|
const query = Object.fromEntries(this.url.searchParams)
|
1343
1319
|
const input = await acceptQuerySchema.parseAsync(query, {
|
1344
1320
|
path: ['query'],
|
@@ -1379,9 +1355,20 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1379
1355
|
client_id: clientIdSchema,
|
1380
1356
|
})
|
1381
1357
|
|
1358
|
+
// Though this is a "no-cors" request, meaning that the browser will allow
|
1359
|
+
// any cross-origin request, with credentials, to be sent, the handler will
|
1360
|
+
// 1) validate the request origin,
|
1361
|
+
// 2) validate the CSRF token,
|
1362
|
+
// 3) validate the referer,
|
1363
|
+
// 4) validate the sec-fetch-site header,
|
1364
|
+
// 4) validate the sec-fetch-mode header,
|
1365
|
+
// 5) validate the sec-fetch-dest header (see navigationHandler).
|
1366
|
+
// And will error if any of these checks fail.
|
1382
1367
|
router.get(
|
1383
1368
|
'/oauth/authorize/reject',
|
1384
1369
|
navigationHandler(async function (req, res) {
|
1370
|
+
validateFetchSite(req, res, ['same-origin'])
|
1371
|
+
|
1385
1372
|
const query = Object.fromEntries(this.url.searchParams)
|
1386
1373
|
const input = await rejectQuerySchema.parseAsync(query, {
|
1387
1374
|
path: ['query'],
|
@@ -1418,3 +1405,26 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1418
1405
|
return router
|
1419
1406
|
}
|
1420
1407
|
}
|
1408
|
+
|
1409
|
+
async function validateRequest<S extends z.ZodTypeAny>(
|
1410
|
+
req: IncomingMessage,
|
1411
|
+
schema: S,
|
1412
|
+
): Promise<z.TypeOf<S>> {
|
1413
|
+
try {
|
1414
|
+
return await validateRequestPayload(req, schema)
|
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
|
+
}
|
1427
|
+
|
1428
|
+
throw new InvalidRequestError('Input validation error', err)
|
1429
|
+
}
|
1430
|
+
}
|
package/src/oauth-verifier.ts
CHANGED
@@ -36,8 +36,7 @@ export type OAuthVerifierOptions = Override<
|
|
36
36
|
issuer: URL | string
|
37
37
|
|
38
38
|
/**
|
39
|
-
* The keyset used to sign tokens.
|
40
|
-
* RS256 key is present in the keyset. ATPROTO requires ES256.
|
39
|
+
* The keyset used to sign access tokens.
|
41
40
|
*/
|
42
41
|
keyset: Keyset | Iterable<Key | undefined | null | false>
|
43
42
|
|