@atproto/oauth-provider 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +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
|
|