@atproto/pds 0.4.164 → 0.4.166
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 +19 -0
- package/dist/account-manager/account-manager.js +2 -2
- package/dist/account-manager/account-manager.js.map +1 -1
- package/dist/account-manager/helpers/account-device.d.ts +4 -4
- package/dist/account-manager/helpers/account.d.ts +1 -1
- package/dist/account-manager/helpers/auth.d.ts +1 -1
- package/dist/account-manager/helpers/auth.d.ts.map +1 -1
- package/dist/account-manager/helpers/auth.js +8 -8
- package/dist/account-manager/helpers/auth.js.map +1 -1
- package/dist/account-manager/helpers/authorization-request.d.ts +1 -1
- package/dist/account-manager/helpers/authorization-request.d.ts.map +1 -1
- package/dist/account-manager/helpers/authorization-request.js +16 -8
- package/dist/account-manager/helpers/authorization-request.js.map +1 -1
- package/dist/account-manager/helpers/token.d.ts +65 -65
- package/dist/actor-store/preference/reader.d.ts +2 -2
- package/dist/actor-store/preference/reader.d.ts.map +1 -1
- package/dist/actor-store/preference/reader.js +2 -2
- package/dist/actor-store/preference/reader.js.map +1 -1
- package/dist/actor-store/preference/transactor.d.ts +2 -2
- package/dist/actor-store/preference/transactor.d.ts.map +1 -1
- package/dist/actor-store/preference/transactor.js +5 -5
- package/dist/actor-store/preference/transactor.js.map +1 -1
- package/dist/actor-store/preference/util.d.ts +4 -2
- package/dist/actor-store/preference/util.d.ts.map +1 -1
- package/dist/actor-store/preference/util.js +9 -8
- package/dist/actor-store/preference/util.js.map +1 -1
- package/dist/actor-store/record/reader.d.ts +2 -2
- package/dist/api/app/bsky/actor/getPreferences.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/getPreferences.js +29 -7
- package/dist/api/app/bsky/actor/getPreferences.js.map +1 -1
- package/dist/api/app/bsky/actor/getProfile.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/getProfile.js +9 -1
- package/dist/api/app/bsky/actor/getProfile.js.map +1 -1
- package/dist/api/app/bsky/actor/getProfiles.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/getProfiles.js +9 -1
- package/dist/api/app/bsky/actor/getProfiles.js.map +1 -1
- package/dist/api/app/bsky/actor/putPreferences.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/putPreferences.js +30 -8
- package/dist/api/app/bsky/actor/putPreferences.js.map +1 -1
- package/dist/api/app/bsky/feed/getActorLikes.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getActorLikes.js +9 -1
- package/dist/api/app/bsky/feed/getActorLikes.js.map +1 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.js +9 -1
- package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
- package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getFeed.js +8 -1
- package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
- package/dist/api/app/bsky/feed/getPostThread.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getPostThread.js +8 -1
- package/dist/api/app/bsky/feed/getPostThread.js.map +1 -1
- package/dist/api/app/bsky/feed/getTimeline.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getTimeline.js +9 -1
- package/dist/api/app/bsky/feed/getTimeline.js.map +1 -1
- package/dist/api/app/bsky/notification/registerPush.d.ts.map +1 -1
- package/dist/api/app/bsky/notification/registerPush.js +16 -4
- package/dist/api/app/bsky/notification/registerPush.js.map +1 -1
- package/dist/api/com/atproto/identity/getRecommendedDidCredentials.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/getRecommendedDidCredentials.js +5 -1
- package/dist/api/com/atproto/identity/getRecommendedDidCredentials.js.map +1 -1
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.js +9 -2
- package/dist/api/com/atproto/identity/requestPlcOperationSignature.js.map +1 -1
- package/dist/api/com/atproto/identity/signPlcOperation.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/signPlcOperation.js +9 -1
- package/dist/api/com/atproto/identity/signPlcOperation.js.map +1 -1
- package/dist/api/com/atproto/identity/submitPlcOperation.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/submitPlcOperation.js +5 -1
- package/dist/api/com/atproto/identity/submitPlcOperation.js.map +1 -1
- package/dist/api/com/atproto/identity/updateHandle.d.ts.map +1 -1
- package/dist/api/com/atproto/identity/updateHandle.js +6 -1
- package/dist/api/com/atproto/identity/updateHandle.js.map +1 -1
- package/dist/api/com/atproto/moderation/createReport.d.ts.map +1 -1
- package/dist/api/com/atproto/moderation/createReport.js +8 -3
- package/dist/api/com/atproto/moderation/createReport.js.map +1 -1
- package/dist/api/com/atproto/repo/applyWrites.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/applyWrites.js +25 -19
- package/dist/api/com/atproto/repo/applyWrites.js.map +1 -1
- package/dist/api/com/atproto/repo/createRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/createRecord.js +10 -1
- package/dist/api/com/atproto/repo/createRecord.js.map +1 -1
- package/dist/api/com/atproto/repo/deleteRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/deleteRecord.js +12 -1
- package/dist/api/com/atproto/repo/deleteRecord.js.map +1 -1
- package/dist/api/com/atproto/repo/importRepo.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/importRepo.js +7 -2
- package/dist/api/com/atproto/repo/importRepo.js.map +1 -1
- package/dist/api/com/atproto/repo/listMissingBlobs.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/listMissingBlobs.js +6 -2
- package/dist/api/com/atproto/repo/listMissingBlobs.js.map +1 -1
- package/dist/api/com/atproto/repo/putRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/putRecord.js +17 -11
- package/dist/api/com/atproto/repo/putRecord.js.map +1 -1
- package/dist/api/com/atproto/repo/uploadBlob.d.ts.map +1 -1
- package/dist/api/com/atproto/repo/uploadBlob.js +5 -1
- package/dist/api/com/atproto/repo/uploadBlob.js.map +1 -1
- package/dist/api/com/atproto/server/activateAccount.d.ts.map +1 -1
- package/dist/api/com/atproto/server/activateAccount.js +7 -1
- package/dist/api/com/atproto/server/activateAccount.js.map +1 -1
- package/dist/api/com/atproto/server/checkAccountStatus.d.ts.map +1 -1
- package/dist/api/com/atproto/server/checkAccountStatus.js +5 -1
- package/dist/api/com/atproto/server/checkAccountStatus.js.map +1 -1
- package/dist/api/com/atproto/server/confirmEmail.d.ts.map +1 -1
- package/dist/api/com/atproto/server/confirmEmail.js +6 -1
- package/dist/api/com/atproto/server/confirmEmail.js.map +1 -1
- package/dist/api/com/atproto/server/createAppPassword.d.ts.map +1 -1
- package/dist/api/com/atproto/server/createAppPassword.js +7 -1
- package/dist/api/com/atproto/server/createAppPassword.js.map +1 -1
- package/dist/api/com/atproto/server/deactivateAccount.d.ts.map +1 -1
- package/dist/api/com/atproto/server/deactivateAccount.js +9 -2
- package/dist/api/com/atproto/server/deactivateAccount.js.map +1 -1
- package/dist/api/com/atproto/server/deleteSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/deleteSession.js +3 -1
- package/dist/api/com/atproto/server/deleteSession.js.map +1 -1
- package/dist/api/com/atproto/server/getAccountInviteCodes.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getAccountInviteCodes.js +8 -1
- package/dist/api/com/atproto/server/getAccountInviteCodes.js.map +1 -1
- package/dist/api/com/atproto/server/getServiceAuth.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getServiceAuth.js +24 -13
- package/dist/api/com/atproto/server/getServiceAuth.js.map +1 -1
- package/dist/api/com/atproto/server/getSession.d.ts.map +1 -1
- package/dist/api/com/atproto/server/getSession.js +12 -19
- package/dist/api/com/atproto/server/getSession.js.map +1 -1
- package/dist/api/com/atproto/server/listAppPasswords.d.ts.map +1 -1
- package/dist/api/com/atproto/server/listAppPasswords.js +6 -1
- package/dist/api/com/atproto/server/listAppPasswords.js.map +1 -1
- package/dist/api/com/atproto/server/refreshSession.js +1 -1
- package/dist/api/com/atproto/server/refreshSession.js.map +1 -1
- package/dist/api/com/atproto/server/requestAccountDelete.d.ts.map +1 -1
- package/dist/api/com/atproto/server/requestAccountDelete.js +8 -1
- package/dist/api/com/atproto/server/requestAccountDelete.js.map +1 -1
- package/dist/api/com/atproto/server/requestEmailConfirmation.d.ts.map +1 -1
- package/dist/api/com/atproto/server/requestEmailConfirmation.js +6 -1
- package/dist/api/com/atproto/server/requestEmailConfirmation.js.map +1 -1
- package/dist/api/com/atproto/server/requestEmailUpdate.d.ts.map +1 -1
- package/dist/api/com/atproto/server/requestEmailUpdate.js +6 -1
- package/dist/api/com/atproto/server/requestEmailUpdate.js.map +1 -1
- package/dist/api/com/atproto/server/revokeAppPassword.d.ts.map +1 -1
- package/dist/api/com/atproto/server/revokeAppPassword.js +6 -1
- package/dist/api/com/atproto/server/revokeAppPassword.js.map +1 -1
- package/dist/api/com/atproto/server/updateEmail.d.ts.map +1 -1
- package/dist/api/com/atproto/server/updateEmail.js +8 -1
- package/dist/api/com/atproto/server/updateEmail.js.map +1 -1
- package/dist/api/com/atproto/sync/deprecated/getCheckout.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/deprecated/getCheckout.js +7 -2
- package/dist/api/com/atproto/sync/deprecated/getCheckout.js.map +1 -1
- package/dist/api/com/atproto/sync/deprecated/getHead.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/deprecated/getHead.js +7 -2
- package/dist/api/com/atproto/sync/deprecated/getHead.js.map +1 -1
- package/dist/api/com/atproto/sync/getBlob.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getBlob.js +7 -3
- package/dist/api/com/atproto/sync/getBlob.js.map +1 -1
- package/dist/api/com/atproto/sync/getBlocks.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getBlocks.js +7 -2
- package/dist/api/com/atproto/sync/getBlocks.js.map +1 -1
- package/dist/api/com/atproto/sync/getLatestCommit.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getLatestCommit.js +7 -2
- package/dist/api/com/atproto/sync/getLatestCommit.js.map +1 -1
- package/dist/api/com/atproto/sync/getRecord.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getRecord.js +7 -2
- package/dist/api/com/atproto/sync/getRecord.js.map +1 -1
- package/dist/api/com/atproto/sync/getRepo.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/getRepo.js +7 -3
- package/dist/api/com/atproto/sync/getRepo.js.map +1 -1
- package/dist/api/com/atproto/sync/listBlobs.d.ts.map +1 -1
- package/dist/api/com/atproto/sync/listBlobs.js +7 -3
- package/dist/api/com/atproto/sync/listBlobs.js.map +1 -1
- package/dist/api/com/atproto/temp/checkSignupQueue.d.ts.map +1 -1
- package/dist/api/com/atproto/temp/checkSignupQueue.js +7 -3
- package/dist/api/com/atproto/temp/checkSignupQueue.js.map +1 -1
- package/dist/auth-output.d.ts +45 -0
- package/dist/auth-output.d.ts.map +1 -0
- package/dist/auth-output.js +3 -0
- package/dist/auth-output.js.map +1 -0
- package/dist/auth-scope.d.ts +16 -0
- package/dist/auth-scope.d.ts.map +1 -0
- package/dist/auth-scope.js +40 -0
- package/dist/auth-scope.js.map +1 -0
- package/dist/auth-verifier.d.ts +50 -115
- package/dist/auth-verifier.d.ts.map +1 -1
- package/dist/auth-verifier.js +275 -366
- package/dist/auth-verifier.js.map +1 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +2 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +3 -1
- package/dist/config/env.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -5
- package/dist/context.js.map +1 -1
- package/dist/lexicon/index.d.ts +234 -230
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +682 -674
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +17994 -17706
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +9126 -8980
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/getLists.d.ts +2 -0
- package/dist/lexicon/types/app/bsky/graph/getLists.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/getListsWithMembership.d.ts +40 -0
- package/dist/lexicon/types/app/bsky/graph/getListsWithMembership.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/graph/getListsWithMembership.js +16 -0
- package/dist/lexicon/types/app/bsky/graph/getListsWithMembership.js.map +1 -0
- package/dist/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.d.ts +38 -0
- package/dist/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.js +16 -0
- package/dist/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.js.map +1 -0
- package/dist/pipethrough.d.ts +5 -3
- package/dist/pipethrough.d.ts.map +1 -1
- package/dist/pipethrough.js +42 -15
- package/dist/pipethrough.js.map +1 -1
- package/dist/sequencer/events.d.ts +13 -13
- package/dist/util/http.d.ts +7 -0
- package/dist/util/http.d.ts.map +1 -0
- package/dist/util/http.js +31 -0
- package/dist/util/http.js.map +1 -0
- package/dist/util/types.d.ts +5 -0
- package/dist/util/types.d.ts.map +1 -0
- package/dist/util/types.js +3 -0
- package/dist/util/types.js.map +1 -0
- package/package.json +7 -6
- package/src/account-manager/account-manager.ts +1 -1
- package/src/account-manager/helpers/auth.ts +1 -1
- package/src/account-manager/helpers/authorization-request.ts +8 -4
- package/src/actor-store/preference/reader.ts +3 -4
- package/src/actor-store/preference/transactor.ts +6 -7
- package/src/actor-store/preference/util.ts +15 -5
- package/src/api/app/bsky/actor/getPreferences.ts +33 -8
- package/src/api/app/bsky/actor/getProfile.ts +9 -1
- package/src/api/app/bsky/actor/getProfiles.ts +9 -1
- package/src/api/app/bsky/actor/putPreferences.ts +35 -12
- package/src/api/app/bsky/feed/getActorLikes.ts +9 -1
- package/src/api/app/bsky/feed/getAuthorFeed.ts +9 -1
- package/src/api/app/bsky/feed/getFeed.ts +9 -2
- package/src/api/app/bsky/feed/getPostThread.ts +8 -1
- package/src/api/app/bsky/feed/getTimeline.ts +9 -1
- package/src/api/app/bsky/notification/registerPush.ts +16 -5
- package/src/api/com/atproto/identity/getRecommendedDidCredentials.ts +5 -1
- package/src/api/com/atproto/identity/requestPlcOperationSignature.ts +9 -2
- package/src/api/com/atproto/identity/signPlcOperation.ts +9 -1
- package/src/api/com/atproto/identity/submitPlcOperation.ts +5 -1
- package/src/api/com/atproto/identity/updateHandle.ts +6 -1
- package/src/api/com/atproto/moderation/createReport.ts +8 -3
- package/src/api/com/atproto/repo/applyWrites.ts +28 -20
- package/src/api/com/atproto/repo/createRecord.ts +12 -1
- package/src/api/com/atproto/repo/deleteRecord.ts +14 -1
- package/src/api/com/atproto/repo/importRepo.ts +9 -2
- package/src/api/com/atproto/repo/listMissingBlobs.ts +7 -2
- package/src/api/com/atproto/repo/putRecord.ts +18 -10
- package/src/api/com/atproto/repo/uploadBlob.ts +6 -2
- package/src/api/com/atproto/server/activateAccount.ts +10 -2
- package/src/api/com/atproto/server/checkAccountStatus.ts +5 -1
- package/src/api/com/atproto/server/confirmEmail.ts +6 -1
- package/src/api/com/atproto/server/createAppPassword.ts +9 -1
- package/src/api/com/atproto/server/deactivateAccount.ts +11 -2
- package/src/api/com/atproto/server/deleteSession.ts +3 -1
- package/src/api/com/atproto/server/getAccountInviteCodes.ts +11 -2
- package/src/api/com/atproto/server/getServiceAuth.ts +37 -18
- package/src/api/com/atproto/server/getSession.ts +20 -27
- package/src/api/com/atproto/server/listAppPasswords.ts +8 -1
- package/src/api/com/atproto/server/refreshSession.ts +1 -1
- package/src/api/com/atproto/server/requestAccountDelete.ts +11 -2
- package/src/api/com/atproto/server/requestEmailConfirmation.ts +6 -1
- package/src/api/com/atproto/server/requestEmailUpdate.ts +6 -1
- package/src/api/com/atproto/server/revokeAppPassword.ts +8 -1
- package/src/api/com/atproto/server/updateEmail.ts +11 -2
- package/src/api/com/atproto/sync/deprecated/getCheckout.ts +7 -6
- package/src/api/com/atproto/sync/deprecated/getHead.ts +7 -6
- package/src/api/com/atproto/sync/getBlob.ts +7 -7
- package/src/api/com/atproto/sync/getBlocks.ts +7 -6
- package/src/api/com/atproto/sync/getLatestCommit.ts +7 -6
- package/src/api/com/atproto/sync/getRecord.ts +7 -6
- package/src/api/com/atproto/sync/getRepo.ts +7 -7
- package/src/api/com/atproto/sync/listBlobs.ts +7 -7
- package/src/api/com/atproto/temp/checkSignupQueue.ts +8 -2
- package/src/auth-output.ts +51 -0
- package/src/auth-scope.ts +40 -0
- package/src/auth-verifier.ts +404 -520
- package/src/config/config.ts +7 -7
- package/src/config/env.ts +5 -1
- package/src/context.ts +6 -5
- package/src/lexicon/index.ts +1247 -1221
- package/src/lexicon/lexicons.ts +9494 -9341
- package/src/lexicon/types/app/bsky/graph/getLists.ts +2 -0
- package/src/lexicon/types/app/bsky/graph/getListsWithMembership.ts +63 -0
- package/src/lexicon/types/app/bsky/graph/getStarterPacksWithMembership.ts +65 -0
- package/src/pipethrough.ts +61 -18
- package/src/util/http.ts +31 -0
- package/src/util/types.ts +7 -0
- package/tests/oauth.test.ts +11 -37
- package/tests/preferences.test.ts +7 -3
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/auth-verifier.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import { KeyObject, createPublicKey, createSecretKey } from 'node:crypto'
|
2
2
|
import { IncomingMessage, ServerResponse } from 'node:http'
|
3
|
-
import { Request } from 'express'
|
4
3
|
import * as jose from 'jose'
|
5
4
|
import KeyEncoder from 'key-encoder'
|
6
5
|
import { getVerificationMaterial } from '@atproto/common'
|
@@ -8,108 +7,56 @@ import { IdResolver, getDidKeyFromMultibase } from '@atproto/identity'
|
|
8
7
|
import {
|
9
8
|
OAuthError,
|
10
9
|
OAuthVerifier,
|
10
|
+
VerifyTokenClaimsOptions,
|
11
11
|
WWWAuthenticateError,
|
12
12
|
} from '@atproto/oauth-provider'
|
13
|
+
import { PermissionSet, PermissionSetTransition } from '@atproto/oauth-scopes'
|
13
14
|
import {
|
14
15
|
AuthRequiredError,
|
16
|
+
Awaitable,
|
15
17
|
ForbiddenError,
|
16
18
|
InvalidRequestError,
|
19
|
+
MethodAuthContext,
|
20
|
+
MethodAuthVerifier,
|
21
|
+
Params,
|
17
22
|
XRPCError,
|
18
23
|
parseReqNsid,
|
19
24
|
verifyJwt as verifyServiceJwt,
|
20
25
|
} from '@atproto/xrpc-server'
|
21
26
|
import { AccountManager } from './account-manager/account-manager'
|
27
|
+
import {
|
28
|
+
AccessOutput,
|
29
|
+
AdminTokenOutput,
|
30
|
+
ModServiceOutput,
|
31
|
+
OAuthOutput,
|
32
|
+
RefreshOutput,
|
33
|
+
UnauthenticatedOutput,
|
34
|
+
UserServiceAuthOutput,
|
35
|
+
} from './auth-output'
|
36
|
+
import { ACCESS_STANDARD, AuthScope, isAuthScope } from './auth-scope'
|
22
37
|
import { softDeleted } from './db'
|
23
38
|
import { oauthLogger } from './logger'
|
39
|
+
import { appendVary } from './util/http'
|
40
|
+
import { WithRequired } from './util/types'
|
24
41
|
|
25
|
-
type
|
26
|
-
|
27
|
-
|
28
|
-
export enum AuthScope {
|
29
|
-
Access = 'com.atproto.access',
|
30
|
-
Refresh = 'com.atproto.refresh',
|
31
|
-
AppPass = 'com.atproto.appPass',
|
32
|
-
AppPassPrivileged = 'com.atproto.appPassPrivileged',
|
33
|
-
SignupQueued = 'com.atproto.signupQueued',
|
34
|
-
Takendown = 'com.atproto.takendown',
|
35
|
-
}
|
36
|
-
|
37
|
-
export type AccessOpts = {
|
38
|
-
additional: AuthScope[]
|
39
|
-
checkTakedown: boolean
|
40
|
-
checkDeactivated: boolean
|
41
|
-
}
|
42
|
-
|
43
|
-
export enum RoleStatus {
|
44
|
-
Valid,
|
45
|
-
Invalid,
|
46
|
-
Missing,
|
42
|
+
export type VerifiedOptions = {
|
43
|
+
checkTakedown?: boolean
|
44
|
+
checkDeactivated?: boolean
|
47
45
|
}
|
48
46
|
|
49
|
-
export type
|
50
|
-
|
47
|
+
export type ScopedOptions<S extends AuthScope = AuthScope> = {
|
48
|
+
scopes?: readonly S[]
|
51
49
|
}
|
52
50
|
|
53
|
-
export type
|
54
|
-
|
55
|
-
type: 'admin_token'
|
56
|
-
}
|
51
|
+
export type ExtraScopedOptions<S extends AuthScope = AuthScope> = {
|
52
|
+
additional?: readonly S[]
|
57
53
|
}
|
58
54
|
|
59
|
-
export type
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
}
|
65
|
-
}
|
66
|
-
|
67
|
-
export type AccessOutput = {
|
68
|
-
credentials: {
|
69
|
-
type: 'access'
|
70
|
-
did: string
|
71
|
-
scope: AuthScope
|
72
|
-
isPrivileged: boolean
|
73
|
-
}
|
74
|
-
}
|
75
|
-
|
76
|
-
export type OAuthOutput = {
|
77
|
-
credentials: {
|
78
|
-
type: 'oauth'
|
79
|
-
did: string
|
80
|
-
scope: AuthScope
|
81
|
-
isPrivileged: boolean
|
82
|
-
oauthScopes: Set<string>
|
83
|
-
}
|
84
|
-
}
|
85
|
-
|
86
|
-
export type RefreshOutput = {
|
87
|
-
credentials: {
|
88
|
-
type: 'refresh'
|
89
|
-
did: string
|
90
|
-
scope: AuthScope
|
91
|
-
tokenId: string
|
92
|
-
}
|
93
|
-
}
|
94
|
-
|
95
|
-
export type UserServiceAuthOutput = {
|
96
|
-
credentials: {
|
97
|
-
type: 'user_service_auth'
|
98
|
-
aud: string
|
99
|
-
did: string
|
100
|
-
}
|
101
|
-
}
|
102
|
-
|
103
|
-
type ValidatedBearer = {
|
104
|
-
did: string
|
105
|
-
scope: AuthScope
|
106
|
-
token: string
|
107
|
-
payload: jose.JWTPayload
|
108
|
-
audience: string | undefined
|
109
|
-
}
|
110
|
-
|
111
|
-
type ValidatedRefreshBearer = ValidatedBearer & {
|
112
|
-
tokenId: string
|
55
|
+
export type AuthorizedOptions<P extends Params = Params> = {
|
56
|
+
authorize: (
|
57
|
+
permissions: PermissionSet,
|
58
|
+
ctx: MethodAuthContext<P>,
|
59
|
+
) => Awaitable<void>
|
113
60
|
}
|
114
61
|
|
115
62
|
export type AuthVerifierOpts = {
|
@@ -123,6 +70,21 @@ export type AuthVerifierOpts = {
|
|
123
70
|
}
|
124
71
|
}
|
125
72
|
|
73
|
+
export type VerifyBearerJwtOptions<S extends AuthScope = AuthScope> =
|
74
|
+
WithRequired<
|
75
|
+
Omit<jose.JWTVerifyOptions, 'scopes'> & {
|
76
|
+
scopes: readonly S[]
|
77
|
+
},
|
78
|
+
'audience' | 'typ'
|
79
|
+
>
|
80
|
+
|
81
|
+
export type VerifyBearerJwtResult<S extends AuthScope = AuthScope> = {
|
82
|
+
sub: string
|
83
|
+
aud: string
|
84
|
+
jti: string | undefined
|
85
|
+
scope: S
|
86
|
+
}
|
87
|
+
|
126
88
|
export class AuthVerifier {
|
127
89
|
private _publicUrl: string
|
128
90
|
private _jwtKey: KeyObject
|
@@ -143,360 +105,279 @@ export class AuthVerifier {
|
|
143
105
|
|
144
106
|
// verifiers (arrow fns to preserve scope)
|
145
107
|
|
146
|
-
|
147
|
-
(
|
148
|
-
async (ctx: ReqCtx): Promise<AccessOutput | OAuthOutput> => {
|
149
|
-
return this.validateAccessToken(
|
150
|
-
ctx,
|
151
|
-
[
|
152
|
-
AuthScope.Access,
|
153
|
-
AuthScope.AppPassPrivileged,
|
154
|
-
AuthScope.AppPass,
|
155
|
-
...(opts.additional ?? []),
|
156
|
-
],
|
157
|
-
opts,
|
158
|
-
)
|
159
|
-
}
|
160
|
-
|
161
|
-
accessFull =
|
162
|
-
(opts: Partial<AccessOpts> = {}) =>
|
163
|
-
(ctx: ReqCtx): Promise<AccessOutput | OAuthOutput> => {
|
164
|
-
return this.validateAccessToken(
|
165
|
-
ctx,
|
166
|
-
[AuthScope.Access, ...(opts.additional ?? [])],
|
167
|
-
opts,
|
168
|
-
)
|
169
|
-
}
|
170
|
-
|
171
|
-
accessPrivileged =
|
172
|
-
(opts: Partial<AccessOpts> = {}) =>
|
173
|
-
(ctx: ReqCtx): Promise<AccessOutput | OAuthOutput> => {
|
174
|
-
return this.validateAccessToken(ctx, [
|
175
|
-
AuthScope.Access,
|
176
|
-
AuthScope.AppPassPrivileged,
|
177
|
-
...(opts.additional ?? []),
|
178
|
-
])
|
179
|
-
}
|
108
|
+
public unauthenticated: MethodAuthVerifier<UnauthenticatedOutput> = (ctx) => {
|
109
|
+
setAuthHeaders(ctx.res)
|
180
110
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
did,
|
188
|
-
scope,
|
189
|
-
tokenId,
|
190
|
-
},
|
111
|
+
// @NOTE this auth method is typically used as fallback when no other auth
|
112
|
+
// method is applicable. This means that the presence of an "authorization"
|
113
|
+
// header means that that header is invalid (as it did not match any of the
|
114
|
+
// other auth methods).
|
115
|
+
if (ctx.req.headers['authorization']) {
|
116
|
+
throw new AuthRequiredError('Invalid authorization header')
|
191
117
|
}
|
192
|
-
}
|
193
|
-
|
194
|
-
refreshExpired = async (ctx: ReqCtx): Promise<RefreshOutput> => {
|
195
|
-
const { did, scope, tokenId } = await this.validateRefreshToken(ctx, {
|
196
|
-
clockTolerance: Infinity,
|
197
|
-
})
|
198
118
|
|
199
119
|
return {
|
200
|
-
credentials:
|
201
|
-
type: 'refresh',
|
202
|
-
did,
|
203
|
-
scope,
|
204
|
-
tokenId,
|
205
|
-
},
|
120
|
+
credentials: null,
|
206
121
|
}
|
207
122
|
}
|
208
123
|
|
209
|
-
adminToken = async (ctx
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
optionalAccessOrAdminToken =
|
215
|
-
(opts: Partial<AccessOpts> = {}) =>
|
216
|
-
async (
|
217
|
-
ctx: ReqCtx,
|
218
|
-
): Promise<AccessOutput | OAuthOutput | AdminTokenOutput | NullOutput> => {
|
219
|
-
if (isAccessToken(ctx.req)) {
|
220
|
-
return await this.accessStandard(opts)(ctx)
|
221
|
-
} else if (isBasicToken(ctx.req)) {
|
222
|
-
return await this.adminToken(ctx)
|
223
|
-
} else {
|
224
|
-
return this.null(ctx)
|
225
|
-
}
|
226
|
-
}
|
227
|
-
|
228
|
-
userServiceAuth = async (ctx: ReqCtx): Promise<UserServiceAuthOutput> => {
|
229
|
-
const payload = await this.verifyServiceJwt(ctx, {
|
230
|
-
aud: null,
|
231
|
-
iss: null,
|
232
|
-
})
|
233
|
-
if (
|
234
|
-
payload.aud !== this.dids.pds &&
|
235
|
-
(!this.dids.entryway || payload.aud !== this.dids.entryway)
|
236
|
-
) {
|
237
|
-
throw new AuthRequiredError(
|
238
|
-
'jwt audience does not match service did',
|
239
|
-
'BadJwtAudience',
|
240
|
-
)
|
124
|
+
public adminToken: MethodAuthVerifier<AdminTokenOutput> = async (ctx) => {
|
125
|
+
setAuthHeaders(ctx.res)
|
126
|
+
const parsed = parseBasicAuth(ctx.req)
|
127
|
+
if (!parsed) {
|
128
|
+
throw new AuthRequiredError()
|
241
129
|
}
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
aud: payload.aud,
|
246
|
-
did: payload.iss,
|
247
|
-
},
|
130
|
+
const { username, password } = parsed
|
131
|
+
if (username !== 'admin' || password !== this._adminPass) {
|
132
|
+
throw new AuthRequiredError()
|
248
133
|
}
|
249
|
-
}
|
250
134
|
|
251
|
-
|
252
|
-
ctx: ReqCtx,
|
253
|
-
): Promise<UserServiceAuthOutput | NullOutput> => {
|
254
|
-
if (isBearerToken(ctx.req)) {
|
255
|
-
return await this.userServiceAuth(ctx)
|
256
|
-
} else {
|
257
|
-
return this.null(ctx)
|
258
|
-
}
|
135
|
+
return { credentials: { type: 'admin_token' } }
|
259
136
|
}
|
260
137
|
|
261
|
-
|
262
|
-
(
|
263
|
-
async (
|
264
|
-
ctx: ReqCtx,
|
265
|
-
): Promise<UserServiceAuthOutput | AccessOutput | OAuthOutput> => {
|
266
|
-
const token = bearerTokenFromReq(ctx.req)
|
267
|
-
if (token) {
|
268
|
-
const payload = jose.decodeJwt(token)
|
269
|
-
if (payload['lxm']) {
|
270
|
-
return this.userServiceAuth(ctx)
|
271
|
-
}
|
272
|
-
}
|
273
|
-
return this.accessStandard(opts)(ctx)
|
274
|
-
}
|
275
|
-
|
276
|
-
modService = async (ctx: ReqCtx): Promise<ModServiceOutput> => {
|
138
|
+
public modService: MethodAuthVerifier<ModServiceOutput> = async (ctx) => {
|
139
|
+
setAuthHeaders(ctx.res)
|
277
140
|
if (!this.dids.modService) {
|
278
141
|
throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
|
279
142
|
}
|
280
|
-
const payload = await this.verifyServiceJwt(ctx, {
|
281
|
-
aud: null,
|
143
|
+
const payload = await this.verifyServiceJwt(ctx.req, {
|
282
144
|
iss: [this.dids.modService, `${this.dids.modService}#atproto_labeler`],
|
283
145
|
})
|
284
|
-
if (
|
285
|
-
payload.aud !== this.dids.pds &&
|
286
|
-
(!this.dids.entryway || payload.aud !== this.dids.entryway)
|
287
|
-
) {
|
288
|
-
throw new AuthRequiredError(
|
289
|
-
'jwt audience does not match service did',
|
290
|
-
'BadJwtAudience',
|
291
|
-
)
|
292
|
-
}
|
293
146
|
return {
|
294
147
|
credentials: {
|
295
148
|
type: 'mod_service',
|
296
|
-
|
297
|
-
iss: payload.iss,
|
149
|
+
did: payload.iss,
|
298
150
|
},
|
299
151
|
}
|
300
152
|
}
|
301
153
|
|
302
|
-
moderator
|
303
|
-
ctx
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
154
|
+
public moderator: MethodAuthVerifier<AdminTokenOutput | ModServiceOutput> =
|
155
|
+
async (ctx) => {
|
156
|
+
const type = extractAuthType(ctx.req)
|
157
|
+
if (type === AuthType.BEARER) {
|
158
|
+
return this.modService(ctx)
|
159
|
+
} else {
|
160
|
+
return this.adminToken(ctx)
|
161
|
+
}
|
309
162
|
}
|
310
|
-
}
|
311
163
|
|
312
|
-
protected
|
313
|
-
|
314
|
-
|
315
|
-
const
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
164
|
+
protected access<S extends AuthScope>(
|
165
|
+
options: VerifiedOptions & Required<ScopedOptions<S>>,
|
166
|
+
): MethodAuthVerifier<AccessOutput<S>> {
|
167
|
+
const { scopes, ...statusOptions } = options
|
168
|
+
|
169
|
+
const verifyJwtOptions: VerifyBearerJwtOptions<S> = {
|
170
|
+
audience: this.dids.pds,
|
171
|
+
typ: 'at+jwt',
|
172
|
+
scopes:
|
173
|
+
// @NOTE We can reject taken down credentials based on the scope if
|
174
|
+
// "checkTakedown" is set.
|
175
|
+
statusOptions.checkTakedown && scopes.includes(AuthScope.Takendown as S)
|
176
|
+
? scopes.filter((s) => s !== AuthScope.Takendown)
|
177
|
+
: scopes,
|
322
178
|
}
|
323
179
|
|
324
|
-
return
|
180
|
+
return async (ctx) => {
|
181
|
+
setAuthHeaders(ctx.res)
|
182
|
+
|
183
|
+
const { sub: did, scope } = await this.verifyBearerJwt(
|
184
|
+
ctx.req,
|
185
|
+
verifyJwtOptions,
|
186
|
+
)
|
187
|
+
|
188
|
+
await this.verifyStatus(did, statusOptions)
|
189
|
+
|
190
|
+
return {
|
191
|
+
credentials: { type: 'access', did, scope },
|
192
|
+
}
|
193
|
+
}
|
325
194
|
}
|
326
195
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
...verifyOptions,
|
196
|
+
public refresh(options?: {
|
197
|
+
allowExpired?: boolean
|
198
|
+
}): MethodAuthVerifier<RefreshOutput> {
|
199
|
+
const verifyOptions: VerifyBearerJwtOptions<AuthScope.Refresh> = {
|
200
|
+
clockTolerance: options?.allowExpired ? Infinity : undefined,
|
333
201
|
typ: 'refresh+jwt',
|
334
202
|
// when using entryway, proxying refresh credentials
|
335
203
|
audience: this.dids.entryway ? this.dids.entryway : this.dids.pds,
|
336
|
-
|
337
|
-
const tokenId = result.payload.jti
|
338
|
-
if (!tokenId) {
|
339
|
-
throw new AuthRequiredError(
|
340
|
-
'Unexpected missing refresh token id',
|
341
|
-
'MissingTokenId',
|
342
|
-
)
|
204
|
+
scopes: [AuthScope.Refresh],
|
343
205
|
}
|
344
|
-
return { ...result, tokenId }
|
345
|
-
}
|
346
|
-
|
347
|
-
protected async validateBearerToken(
|
348
|
-
ctx: ReqCtx,
|
349
|
-
scopes: AuthScope[],
|
350
|
-
verifyOptions: jose.JWTVerifyOptions &
|
351
|
-
Required<Pick<jose.JWTVerifyOptions, 'audience' | 'typ'>>,
|
352
|
-
): Promise<ValidatedBearer> {
|
353
|
-
this.setAuthHeaders(ctx)
|
354
206
|
|
355
|
-
|
356
|
-
|
357
|
-
throw new AuthRequiredError(undefined, 'AuthMissing')
|
358
|
-
}
|
207
|
+
return async (ctx) => {
|
208
|
+
setAuthHeaders(ctx.res)
|
359
209
|
|
360
|
-
|
361
|
-
token,
|
362
|
-
// @TODO: Once all access & refresh tokens have a "typ" claim (i.e. 90
|
363
|
-
// days after this code was deployed), replace the following line with
|
364
|
-
// "verifyOptions," (to re-enable the verification of the "typ" property
|
365
|
-
// from verifyJwt()). Once the change is made, the "if" block below that
|
366
|
-
// checks for "typ" can be removed.
|
367
|
-
{
|
368
|
-
...verifyOptions,
|
369
|
-
typ: undefined,
|
370
|
-
},
|
371
|
-
)
|
210
|
+
const result = await this.verifyBearerJwt(ctx.req, verifyOptions)
|
372
211
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
// src/account-manager/helpers/auth.ts
|
381
|
-
throw new InvalidRequestError('Invalid token type', 'InvalidToken')
|
382
|
-
}
|
212
|
+
const tokenId = result.jti
|
213
|
+
if (!tokenId) {
|
214
|
+
throw new AuthRequiredError(
|
215
|
+
'Unexpected missing refresh token id',
|
216
|
+
'MissingTokenId',
|
217
|
+
)
|
218
|
+
}
|
383
219
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
393
|
-
}
|
394
|
-
if (payload['cnf'] !== undefined) {
|
395
|
-
// Proof-of-Possession (PoP) tokens are not allowed here
|
396
|
-
// https://www.rfc-editor.org/rfc/rfc7800.html
|
397
|
-
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
398
|
-
}
|
399
|
-
if (!isAuthScope(scope) || (scopes.length > 0 && !scopes.includes(scope))) {
|
400
|
-
throw new InvalidRequestError('Bad token scope', 'InvalidToken')
|
401
|
-
}
|
402
|
-
return {
|
403
|
-
did: sub,
|
404
|
-
scope,
|
405
|
-
audience: aud,
|
406
|
-
token,
|
407
|
-
payload,
|
220
|
+
return {
|
221
|
+
credentials: {
|
222
|
+
type: 'refresh',
|
223
|
+
did: result.sub,
|
224
|
+
scope: result.scope,
|
225
|
+
tokenId,
|
226
|
+
},
|
227
|
+
}
|
408
228
|
}
|
409
229
|
}
|
410
230
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
this.
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
const
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
231
|
+
public authorization<P extends Params>({
|
232
|
+
scopes = ACCESS_STANDARD,
|
233
|
+
additional = [],
|
234
|
+
...options
|
235
|
+
}: VerifiedOptions &
|
236
|
+
ScopedOptions &
|
237
|
+
ExtraScopedOptions &
|
238
|
+
AuthorizedOptions<P>): MethodAuthVerifier<AccessOutput | OAuthOutput, P> {
|
239
|
+
const access = this.access({
|
240
|
+
...options,
|
241
|
+
scopes: [...scopes, ...additional],
|
242
|
+
})
|
243
|
+
const oauth = this.oauth(options)
|
244
|
+
|
245
|
+
return async (ctx) => {
|
246
|
+
const type = extractAuthType(ctx.req)
|
247
|
+
|
248
|
+
if (type === AuthType.BEARER) {
|
249
|
+
return access(ctx)
|
428
250
|
}
|
429
|
-
|
430
|
-
|
431
|
-
|
251
|
+
|
252
|
+
if (type === AuthType.DPOP) {
|
253
|
+
return oauth(ctx)
|
432
254
|
}
|
433
|
-
|
434
|
-
|
435
|
-
|
255
|
+
|
256
|
+
// Auth headers are set through the access and oauth methods so we only
|
257
|
+
// need to set them here if we reach this point
|
258
|
+
setAuthHeaders(ctx.res)
|
259
|
+
|
260
|
+
if (type !== null) {
|
436
261
|
throw new InvalidRequestError(
|
437
262
|
'Unexpected authorization type',
|
438
263
|
'InvalidToken',
|
439
264
|
)
|
265
|
+
}
|
266
|
+
|
267
|
+
throw new AuthRequiredError(undefined, 'AuthMissing')
|
440
268
|
}
|
269
|
+
}
|
441
270
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
}
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
'AccountTakedown',
|
458
|
-
)
|
459
|
-
}
|
460
|
-
if (checkDeactivated && found.deactivatedAt) {
|
461
|
-
throw new AuthRequiredError(
|
462
|
-
'Account is deactivated',
|
463
|
-
'AccountDeactivated',
|
464
|
-
)
|
271
|
+
public authorizationOrAdminTokenOptional<P extends Params>(
|
272
|
+
opts: VerifiedOptions & ExtraScopedOptions & AuthorizedOptions<P>,
|
273
|
+
): MethodAuthVerifier<
|
274
|
+
OAuthOutput | AccessOutput | AdminTokenOutput | UnauthenticatedOutput,
|
275
|
+
P
|
276
|
+
> {
|
277
|
+
const authorization = this.authorization(opts)
|
278
|
+
return async (ctx) => {
|
279
|
+
const type = extractAuthType(ctx.req)
|
280
|
+
if (type === AuthType.BEARER || type === AuthType.DPOP) {
|
281
|
+
return authorization(ctx)
|
282
|
+
} else if (type === AuthType.BASIC) {
|
283
|
+
return this.adminToken(ctx)
|
284
|
+
} else {
|
285
|
+
return this.unauthenticated(ctx)
|
465
286
|
}
|
466
287
|
}
|
288
|
+
}
|
289
|
+
|
290
|
+
public userServiceAuth: MethodAuthVerifier<UserServiceAuthOutput> = async (
|
291
|
+
ctx,
|
292
|
+
) => {
|
293
|
+
setAuthHeaders(ctx.res)
|
294
|
+
const payload = await this.verifyServiceJwt(ctx.req)
|
295
|
+
return {
|
296
|
+
credentials: {
|
297
|
+
type: 'user_service_auth',
|
298
|
+
did: payload.iss,
|
299
|
+
},
|
300
|
+
}
|
301
|
+
}
|
467
302
|
|
468
|
-
|
303
|
+
public userServiceAuthOptional: MethodAuthVerifier<
|
304
|
+
UserServiceAuthOutput | UnauthenticatedOutput
|
305
|
+
> = async (ctx) => {
|
306
|
+
const type = extractAuthType(ctx.req)
|
307
|
+
if (type === AuthType.BEARER) {
|
308
|
+
return await this.userServiceAuth(ctx)
|
309
|
+
} else {
|
310
|
+
return this.unauthenticated(ctx)
|
311
|
+
}
|
312
|
+
}
|
313
|
+
|
314
|
+
public authorizationOrUserServiceAuth<P extends Params>(
|
315
|
+
options: VerifiedOptions &
|
316
|
+
ScopedOptions &
|
317
|
+
ExtraScopedOptions &
|
318
|
+
AuthorizedOptions<P>,
|
319
|
+
): MethodAuthVerifier<UserServiceAuthOutput | OAuthOutput | AccessOutput, P> {
|
320
|
+
const authorizationVerifier = this.authorization(options)
|
321
|
+
return async (ctx) => {
|
322
|
+
if (isDefinitelyServiceAuth(ctx.req)) {
|
323
|
+
return this.userServiceAuth(ctx)
|
324
|
+
} else {
|
325
|
+
return authorizationVerifier(ctx)
|
326
|
+
}
|
327
|
+
}
|
469
328
|
}
|
470
329
|
|
471
|
-
protected
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
330
|
+
protected oauth<P extends Params>({
|
331
|
+
authorize,
|
332
|
+
...verifyStatusOptions
|
333
|
+
}: VerifiedOptions & AuthorizedOptions<P>): MethodAuthVerifier<
|
334
|
+
OAuthOutput,
|
335
|
+
P
|
336
|
+
> {
|
337
|
+
const verifyTokenOptions: VerifyTokenClaimsOptions = {
|
338
|
+
audience: [this.dids.pds],
|
339
|
+
scope: ['atproto'],
|
340
|
+
}
|
476
341
|
|
477
|
-
|
478
|
-
|
342
|
+
return async (ctx) => {
|
343
|
+
setAuthHeaders(ctx.res)
|
479
344
|
|
480
|
-
|
481
|
-
|
345
|
+
const { req, res } = ctx
|
346
|
+
|
347
|
+
// https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
|
482
348
|
const dpopNonce = this.oauthVerifier.nextDpopNonce()
|
483
349
|
if (dpopNonce) {
|
484
350
|
res.setHeader('DPoP-Nonce', dpopNonce)
|
485
351
|
res.appendHeader('Access-Control-Expose-Headers', 'DPoP-Nonce')
|
486
352
|
}
|
487
|
-
}
|
488
353
|
|
489
|
-
|
490
|
-
const originalUrl =
|
491
|
-
('originalUrl' in req && (req as Request).originalUrl) || req.url || '/'
|
354
|
+
const originalUrl = req.originalUrl || req.url || '/'
|
492
355
|
const url = new URL(originalUrl, this._publicUrl)
|
493
|
-
|
494
|
-
|
356
|
+
|
357
|
+
const { tokenClaims, dpopProof } = await this.oauthVerifier
|
358
|
+
.authenticateRequest(
|
495
359
|
req.method || 'GET',
|
496
360
|
url,
|
497
361
|
req.headers,
|
498
|
-
|
362
|
+
verifyTokenOptions,
|
499
363
|
)
|
364
|
+
.catch((err) => {
|
365
|
+
// Make sure to include any WWW-Authenticate header in the response
|
366
|
+
// (particularly useful for DPoP's "use_dpop_nonce" error)
|
367
|
+
if (err instanceof WWWAuthenticateError) {
|
368
|
+
res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader)
|
369
|
+
res.appendHeader(
|
370
|
+
'Access-Control-Expose-Headers',
|
371
|
+
'WWW-Authenticate',
|
372
|
+
)
|
373
|
+
}
|
374
|
+
|
375
|
+
if (err instanceof OAuthError) {
|
376
|
+
throw new XRPCError(err.status, err.error_description, err.error)
|
377
|
+
}
|
378
|
+
|
379
|
+
throw err
|
380
|
+
})
|
500
381
|
|
501
382
|
// @TODO drop this once oauth provider no longer accepts DPoP proof with
|
502
383
|
// query or fragment in "htu" claim.
|
@@ -510,99 +391,142 @@ export class AuthVerifier {
|
|
510
391
|
)
|
511
392
|
}
|
512
393
|
|
513
|
-
const { sub } = tokenClaims
|
514
|
-
if (typeof
|
394
|
+
const { sub: did } = tokenClaims
|
395
|
+
if (typeof did !== 'string' || !did.startsWith('did:')) {
|
515
396
|
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
516
397
|
}
|
517
398
|
|
518
|
-
|
399
|
+
await this.verifyStatus(did, verifyStatusOptions)
|
519
400
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
'InvalidToken',
|
524
|
-
)
|
525
|
-
}
|
526
|
-
|
527
|
-
const scopeEquivalent: AuthScope = oauthScopes.has('transition:chat.bsky')
|
528
|
-
? AuthScope.AppPassPrivileged
|
529
|
-
: AuthScope.AppPass
|
530
|
-
|
531
|
-
if (!scopes.includes(scopeEquivalent)) {
|
532
|
-
// AppPassPrivileged is sufficient but was not provided "transition:chat.bsky"
|
533
|
-
if (scopes.includes(AuthScope.AppPassPrivileged)) {
|
534
|
-
throw new InvalidRequestError(
|
535
|
-
'Missing required scope: transition:chat.bsky',
|
536
|
-
'InvalidToken',
|
537
|
-
)
|
538
|
-
}
|
401
|
+
const permissions = new PermissionSetTransition(
|
402
|
+
tokenClaims.scope?.split(' '),
|
403
|
+
)
|
539
404
|
|
540
|
-
|
541
|
-
|
405
|
+
// Should never happen
|
406
|
+
if (!permissions.scopes.has('atproto')) {
|
542
407
|
throw new InvalidRequestError(
|
543
|
-
'
|
408
|
+
'OAuth token does not have "atproto" scope',
|
544
409
|
'InvalidToken',
|
545
410
|
)
|
546
411
|
}
|
547
412
|
|
413
|
+
await authorize(permissions, ctx)
|
414
|
+
|
548
415
|
return {
|
549
416
|
credentials: {
|
550
417
|
type: 'oauth',
|
551
|
-
did
|
552
|
-
|
553
|
-
oauthScopes,
|
554
|
-
isPrivileged: scopeEquivalent === AuthScope.AppPassPrivileged,
|
418
|
+
did,
|
419
|
+
permissions,
|
555
420
|
},
|
556
421
|
}
|
557
|
-
}
|
558
|
-
|
559
|
-
// (particularly useful for DPoP's "use_dpop_nonce" error)
|
560
|
-
if (res && err instanceof WWWAuthenticateError) {
|
561
|
-
res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader)
|
562
|
-
res.appendHeader('Access-Control-Expose-Headers', 'WWW-Authenticate')
|
563
|
-
}
|
422
|
+
}
|
423
|
+
}
|
564
424
|
|
565
|
-
|
566
|
-
|
425
|
+
protected async verifyStatus(
|
426
|
+
did: string,
|
427
|
+
{ checkTakedown = false, checkDeactivated = false }: VerifiedOptions,
|
428
|
+
): Promise<void> {
|
429
|
+
if (checkTakedown || checkDeactivated) {
|
430
|
+
const found = await this.accountManager.getAccount(did, {
|
431
|
+
includeDeactivated: true,
|
432
|
+
includeTakenDown: true,
|
433
|
+
})
|
434
|
+
if (!found) {
|
435
|
+
// will be turned into ExpiredToken for the client if proxied by entryway
|
436
|
+
throw new ForbiddenError('Account not found', 'AccountNotFound')
|
437
|
+
}
|
438
|
+
if (checkTakedown && softDeleted(found)) {
|
439
|
+
throw new AuthRequiredError(
|
440
|
+
'Account has been taken down',
|
441
|
+
'AccountTakedown',
|
442
|
+
)
|
443
|
+
}
|
444
|
+
if (checkDeactivated && found.deactivatedAt) {
|
445
|
+
throw new AuthRequiredError(
|
446
|
+
'Account is deactivated',
|
447
|
+
'AccountDeactivated',
|
448
|
+
)
|
567
449
|
}
|
568
|
-
|
569
|
-
throw err
|
570
450
|
}
|
571
451
|
}
|
572
452
|
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
453
|
+
/**
|
454
|
+
* Wraps {@link jose.jwtVerify} into a function that also validates the token
|
455
|
+
* payload's type and wraps errors into {@link InvalidRequestError}.
|
456
|
+
*/
|
457
|
+
protected async verifyBearerJwt<S extends AuthScope = AuthScope>(
|
458
|
+
req: IncomingMessage,
|
459
|
+
{ scopes, ...options }: VerifyBearerJwtOptions<S>,
|
460
|
+
): Promise<VerifyBearerJwtResult<S>> {
|
461
|
+
const token = bearerTokenFromReq(req)
|
462
|
+
if (!token) {
|
463
|
+
throw new AuthRequiredError(undefined, 'AuthMissing')
|
464
|
+
}
|
581
465
|
|
582
|
-
const
|
583
|
-
|
466
|
+
const { payload, protectedHeader } = await jose
|
467
|
+
.jwtVerify(token, this._jwtKey, { ...options, typ: undefined })
|
468
|
+
.catch((cause) => {
|
469
|
+
if (cause instanceof jose.errors.JWTExpired) {
|
470
|
+
throw new InvalidRequestError('Token has expired', 'ExpiredToken', {
|
471
|
+
cause,
|
472
|
+
})
|
473
|
+
} else {
|
474
|
+
throw new InvalidRequestError(
|
475
|
+
'Token could not be verified',
|
476
|
+
'InvalidToken',
|
477
|
+
{ cause },
|
478
|
+
)
|
479
|
+
}
|
480
|
+
})
|
584
481
|
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
},
|
482
|
+
// @NOTE: the "typ" is now set in production environments, so we should be
|
483
|
+
// able to safely check it through jose.jwtVerify(). However, tests depend
|
484
|
+
// on @atproto/pds-entryway which does not set "typ" in the access tokens.
|
485
|
+
// For that reason, we still allow it to be missing.
|
486
|
+
if (protectedHeader.typ && options.typ !== protectedHeader.typ) {
|
487
|
+
throw new InvalidRequestError('Invalid token type', 'InvalidToken')
|
592
488
|
}
|
489
|
+
|
490
|
+
const { sub, aud, scope, lxm, cnf, jti } = payload
|
491
|
+
|
492
|
+
if (typeof lxm !== 'undefined') {
|
493
|
+
// Service auth tokens should never make it to here. But since service
|
494
|
+
// auth tokens do not have a "typ" header, the "typ" check above will not
|
495
|
+
// catch them. This check here is mainly to protect against the
|
496
|
+
// hypothetical case in which a PDS would issue service auth tokens using
|
497
|
+
// its private key.
|
498
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
499
|
+
}
|
500
|
+
if (typeof cnf !== 'undefined') {
|
501
|
+
// Proof-of-Possession (PoP) tokens are not allowed here
|
502
|
+
// https://www.rfc-editor.org/rfc/rfc7800.html
|
503
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
504
|
+
}
|
505
|
+
if (typeof sub !== 'string' || !sub.startsWith('did:')) {
|
506
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
507
|
+
}
|
508
|
+
if (typeof aud !== 'string' || !aud.startsWith('did:')) {
|
509
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
510
|
+
}
|
511
|
+
if (typeof jti !== 'string' && typeof jti !== 'undefined') {
|
512
|
+
throw new InvalidRequestError('Malformed token', 'InvalidToken')
|
513
|
+
}
|
514
|
+
if (!isAuthScope(scope) || !scopes.includes(scope as any)) {
|
515
|
+
throw new InvalidRequestError('Bad token scope', 'InvalidToken')
|
516
|
+
}
|
517
|
+
|
518
|
+
return { sub, aud, jti, scope: scope as S }
|
593
519
|
}
|
594
520
|
|
595
521
|
protected async verifyServiceJwt(
|
596
|
-
|
597
|
-
opts
|
522
|
+
req: IncomingMessage,
|
523
|
+
opts?: { iss?: string[] },
|
598
524
|
) {
|
599
|
-
this.setAuthHeaders(ctx)
|
600
|
-
|
601
525
|
const getSigningKey = async (
|
602
526
|
iss: string,
|
603
527
|
forceRefresh: boolean,
|
604
528
|
): Promise<string> => {
|
605
|
-
if (opts
|
529
|
+
if (opts?.iss && !opts.iss.includes(iss)) {
|
606
530
|
throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss')
|
607
531
|
}
|
608
532
|
const [did, serviceId] = iss.split('#')
|
@@ -623,78 +547,51 @@ export class AuthVerifier {
|
|
623
547
|
return didKey
|
624
548
|
}
|
625
549
|
|
626
|
-
const jwtStr = bearerTokenFromReq(
|
550
|
+
const jwtStr = bearerTokenFromReq(req)
|
627
551
|
if (!jwtStr) {
|
628
552
|
throw new AuthRequiredError('missing jwt', 'MissingJwt')
|
629
553
|
}
|
630
|
-
const nsid = parseReqNsid(
|
631
|
-
const payload = await verifyServiceJwt(
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
protected null(ctx: ReqCtx): NullOutput {
|
641
|
-
this.setAuthHeaders(ctx)
|
642
|
-
return {
|
643
|
-
credentials: null,
|
644
|
-
}
|
645
|
-
}
|
646
|
-
|
647
|
-
isUserOrAdmin(
|
648
|
-
auth: AccessOutput | OAuthOutput | AdminTokenOutput | NullOutput,
|
649
|
-
did: string,
|
650
|
-
): boolean {
|
651
|
-
if (!auth.credentials) {
|
652
|
-
return false
|
653
|
-
} else if (auth.credentials.type === 'admin_token') {
|
654
|
-
return true
|
655
|
-
} else {
|
656
|
-
return auth.credentials.did === did
|
657
|
-
}
|
658
|
-
}
|
659
|
-
|
660
|
-
protected async jwtVerify(
|
661
|
-
token: string,
|
662
|
-
verifyOptions?: jose.JWTVerifyOptions,
|
663
|
-
) {
|
664
|
-
try {
|
665
|
-
return await jose.jwtVerify(token, this._jwtKey, verifyOptions)
|
666
|
-
} catch (err) {
|
667
|
-
if (err?.['code'] === 'ERR_JWT_EXPIRED') {
|
668
|
-
throw new InvalidRequestError('Token has expired', 'ExpiredToken')
|
669
|
-
}
|
670
|
-
throw new InvalidRequestError(
|
671
|
-
'Token could not be verified',
|
672
|
-
'InvalidToken',
|
554
|
+
const nsid = parseReqNsid(req)
|
555
|
+
const payload = await verifyServiceJwt(jwtStr, null, nsid, getSigningKey)
|
556
|
+
if (
|
557
|
+
payload.aud !== this.dids.pds &&
|
558
|
+
(!this.dids.entryway || payload.aud !== this.dids.entryway)
|
559
|
+
) {
|
560
|
+
throw new AuthRequiredError(
|
561
|
+
'jwt audience does not match service did',
|
562
|
+
'BadJwtAudience',
|
673
563
|
)
|
674
564
|
}
|
675
|
-
|
676
|
-
|
677
|
-
protected setAuthHeaders(ctx: ReqCtx) {
|
678
|
-
const res = 'res' in ctx ? ctx.res : null
|
679
|
-
if (res) {
|
680
|
-
res.setHeader('Cache-Control', 'private')
|
681
|
-
vary(res, 'Authorization')
|
682
|
-
}
|
565
|
+
return payload
|
683
566
|
}
|
684
567
|
}
|
685
568
|
|
686
569
|
// HELPERS
|
687
570
|
// ---------
|
688
571
|
|
572
|
+
export function isUserOrAdmin(
|
573
|
+
auth: AccessOutput | OAuthOutput | AdminTokenOutput | UnauthenticatedOutput,
|
574
|
+
did: string,
|
575
|
+
): boolean {
|
576
|
+
if (!auth.credentials) {
|
577
|
+
return false
|
578
|
+
} else if (auth.credentials.type === 'admin_token') {
|
579
|
+
return true
|
580
|
+
} else {
|
581
|
+
return auth.credentials.did === did
|
582
|
+
}
|
583
|
+
}
|
584
|
+
|
689
585
|
enum AuthType {
|
690
586
|
BASIC = 'Basic',
|
691
587
|
BEARER = 'Bearer',
|
692
588
|
DPOP = 'DPoP',
|
693
589
|
}
|
694
590
|
|
695
|
-
|
696
|
-
|
591
|
+
const parseAuthorizationHeader = (
|
592
|
+
req: IncomingMessage,
|
697
593
|
): [type: null] | [type: AuthType, token: string] => {
|
594
|
+
const authorization = req.headers['authorization']
|
698
595
|
if (!authorization) return [null]
|
699
596
|
|
700
597
|
const result = authorization.split(' ')
|
@@ -717,31 +614,33 @@ export const parseAuthorizationHeader = (
|
|
717
614
|
)
|
718
615
|
}
|
719
616
|
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
const
|
726
|
-
const
|
727
|
-
|
617
|
+
/**
|
618
|
+
* @note Not all service auth tokens are guaranteed to have "lxm" claim, so this
|
619
|
+
* function should not be used to verify service auth tokens. It is only used to
|
620
|
+
* check if a token is definitely a service auth token.
|
621
|
+
*/
|
622
|
+
const isDefinitelyServiceAuth = (req: IncomingMessage): boolean => {
|
623
|
+
const token = bearerTokenFromReq(req)
|
624
|
+
if (!token) return false
|
625
|
+
const payload = jose.decodeJwt(token)
|
626
|
+
return payload['lxm'] != null
|
728
627
|
}
|
729
628
|
|
730
|
-
const
|
731
|
-
const [type] = parseAuthorizationHeader(req
|
732
|
-
return type
|
629
|
+
const extractAuthType = (req: IncomingMessage): AuthType | null => {
|
630
|
+
const [type] = parseAuthorizationHeader(req)
|
631
|
+
return type
|
733
632
|
}
|
734
633
|
|
735
634
|
const bearerTokenFromReq = (req: IncomingMessage) => {
|
736
|
-
const [type, token] = parseAuthorizationHeader(req
|
635
|
+
const [type, token] = parseAuthorizationHeader(req)
|
737
636
|
return type === AuthType.BEARER ? token : null
|
738
637
|
}
|
739
638
|
|
740
|
-
|
741
|
-
|
639
|
+
const parseBasicAuth = (
|
640
|
+
req: IncomingMessage,
|
742
641
|
): { username: string; password: string } | null => {
|
743
642
|
try {
|
744
|
-
const [type, b64] = parseAuthorizationHeader(
|
643
|
+
const [type, b64] = parseAuthorizationHeader(req)
|
745
644
|
if (type !== AuthType.BASIC) return null
|
746
645
|
const decoded = Buffer.from(b64, 'base64').toString('utf8')
|
747
646
|
// We must not use split(':') because the password can contain colons
|
@@ -755,32 +654,17 @@ export const parseBasicAuth = (
|
|
755
654
|
}
|
756
655
|
}
|
757
656
|
|
758
|
-
const authScopes = new Set(Object.values(AuthScope))
|
759
|
-
const isAuthScope = (val: unknown): val is AuthScope => {
|
760
|
-
return authScopes.has(val as any)
|
761
|
-
}
|
762
|
-
|
763
657
|
export const createSecretKeyObject = (secret: string): KeyObject => {
|
764
658
|
return createSecretKey(Buffer.from(secret))
|
765
659
|
}
|
766
660
|
|
661
|
+
const keyEncoder = new KeyEncoder('secp256k1')
|
767
662
|
export const createPublicKeyObject = (publicKeyHex: string): KeyObject => {
|
768
663
|
const key = keyEncoder.encodePublic(publicKeyHex, 'raw', 'pem')
|
769
664
|
return createPublicKey({ format: 'pem', key })
|
770
665
|
}
|
771
666
|
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
const current = res.getHeader('Vary')
|
776
|
-
if (current == null || typeof current === 'number') {
|
777
|
-
res.setHeader('Vary', value)
|
778
|
-
} else {
|
779
|
-
const alreadyIncluded = Array.isArray(current)
|
780
|
-
? current.some((value) => value.includes(value))
|
781
|
-
: current.includes(value)
|
782
|
-
if (!alreadyIncluded) {
|
783
|
-
res.appendHeader('Vary', value)
|
784
|
-
}
|
785
|
-
}
|
667
|
+
function setAuthHeaders(res: ServerResponse) {
|
668
|
+
res.setHeader('Cache-Control', 'private')
|
669
|
+
appendVary(res, 'Authorization')
|
786
670
|
}
|