diff --git a/.changeset/thick-shrimps-warn.md b/.changeset/thick-shrimps-warn.md new file mode 100644 index 00000000000..b8d37a82738 --- /dev/null +++ b/.changeset/thick-shrimps-warn.md @@ -0,0 +1,5 @@ +--- +"@atproto/pds": patch +--- + +Allow takendown accounts to perform account migration diff --git a/packages/pds/src/account-manager/index.ts b/packages/pds/src/account-manager/index.ts index 820ae641be7..b646e69bf7c 100644 --- a/packages/pds/src/account-manager/index.ts +++ b/packages/pds/src/account-manager/index.ts @@ -318,6 +318,7 @@ export class AccountManager if (!user) { throw new AuthRequiredError('Invalid identifier or password') } + const isSoftDeleted = softDeleted(user) let appPassword: password.AppPassDescript | null = null const validAccountPass = await this.verifyAccountPassword( @@ -325,13 +326,17 @@ export class AccountManager password, ) if (!validAccountPass) { + // takendown/suspended accounts cannot login with app password + if (isSoftDeleted) { + throw new AuthRequiredError('Invalid identifier or password') + } appPassword = await this.verifyAppPassword(user.did, password) if (appPassword === null) { throw new AuthRequiredError('Invalid identifier or password') } } - return { user, appPassword, isSoftDeleted: softDeleted(user) } + return { user, appPassword, isSoftDeleted } } finally { // Mitigate timing attacks await wait(350 - (Date.now() - start)) diff --git a/packages/pds/src/api/app/bsky/actor/getPreferences.ts b/packages/pds/src/api/app/bsky/actor/getPreferences.ts index 310495e3282..761873df473 100644 --- a/packages/pds/src/api/app/bsky/actor/getPreferences.ts +++ b/packages/pds/src/api/app/bsky/actor/getPreferences.ts @@ -1,10 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { if (!ctx.cfg.bskyAppView) return server.app.bsky.actor.getPreferences({ - auth: ctx.authVerifier.accessStandard(), + auth: ctx.authVerifier.accessStandard({ + additional: [AuthScope.Takendown], + }), handler: async ({ auth }) => { const requester = auth.credentials.did const preferences = await ctx.actorStore.read(requester, (store) => diff --git a/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts b/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts index d611eddc5a0..93be620f7c1 100644 --- a/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts +++ b/packages/pds/src/api/com/atproto/identity/requestPlcOperationSignature.ts @@ -5,10 +5,11 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { ids } from '../../../../lexicon/lexicons' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.requestPlcOperationSignature({ - auth: ctx.authVerifier.accessFull(), + auth: ctx.authVerifier.accessFull({ additional: [AuthScope.Takendown] }), handler: async ({ auth }) => { if (ctx.entrywayAgent) { assert(ctx.cfg.entryway) diff --git a/packages/pds/src/api/com/atproto/server/deactivateAccount.ts b/packages/pds/src/api/com/atproto/server/deactivateAccount.ts index 7076b17a1a7..42b39b7ce84 100644 --- a/packages/pds/src/api/com/atproto/server/deactivateAccount.ts +++ b/packages/pds/src/api/com/atproto/server/deactivateAccount.ts @@ -1,10 +1,11 @@ +import { AuthScope } from '../../../../auth-verifier' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { authPassthru } from '../../../proxy' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.deactivateAccount({ - auth: ctx.authVerifier.accessFull(), + auth: ctx.authVerifier.accessFull({ additional: [AuthScope.Takendown] }), handler: async ({ req, auth, input }) => { // in the case of entryway, the full flow is deactivateAccount (PDS) -> deactivateAccount (Entryway) -> updateSubjectStatus(PDS) if (ctx.entrywayAgent) { diff --git a/packages/pds/src/api/com/atproto/server/getServiceAuth.ts b/packages/pds/src/api/com/atproto/server/getServiceAuth.ts index 7a9a2c7a617..5f85ba57cb3 100644 --- a/packages/pds/src/api/com/atproto/server/getServiceAuth.ts +++ b/packages/pds/src/api/com/atproto/server/getServiceAuth.ts @@ -3,14 +3,27 @@ import { HOUR, MINUTE } from '@atproto/common' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { PRIVILEGED_METHODS, PROTECTED_METHODS } from '../../../../pipethrough' +import { AuthScope } from '../../../../auth-verifier' +import { ids } from '../../../../lexicon/lexicons' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.getServiceAuth({ - auth: ctx.authVerifier.accessStandard(), + auth: ctx.authVerifier.accessStandard({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, auth }) => { const did = auth.credentials.did const { aud, lxm = null } = params const exp = params.exp ? params.exp * 1000 : undefined + + // Takendown accounts should not be able to generate service auth tokens except for methods necessary for account migration + if ( + auth.credentials.scope === AuthScope.Takendown && + lxm !== ids.ComAtprotoServerCreateAccount + ) { + throw new InvalidRequestError('Bad token scope', 'InvalidToken') + } + if (exp) { const diff = exp - Date.now() if (diff < 0) { diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts index 85f15f84779..0abc358534c 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts @@ -5,7 +5,7 @@ import { assertRepoAvailability } from '../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCheckout({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts index 205652ee9ca..9c74e33315d 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts @@ -5,7 +5,7 @@ import { assertRepoAvailability } from '../util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getHead({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getBlob.ts b/packages/pds/src/api/com/atproto/sync/getBlob.ts index 7c3290c83a6..8125656072e 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlob.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlob.ts @@ -4,10 +4,13 @@ import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' import { BlobNotFoundError } from '@atproto/repo' import { assertRepoAvailability } from './util' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlob({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, res, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getBlocks.ts b/packages/pds/src/api/com/atproto/sync/getBlocks.ts index 21d330357ef..dfd10b61dc5 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlocks.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlocks.ts @@ -8,7 +8,7 @@ import { assertRepoAvailability } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlocks({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts index a8809f77e39..65043612c1e 100644 --- a/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts +++ b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts @@ -5,7 +5,7 @@ import { assertRepoAvailability } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getLatestCommit({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getRecord.ts b/packages/pds/src/api/com/atproto/sync/getRecord.ts index 4ea429e3a43..9cc4adfecd5 100644 --- a/packages/pds/src/api/com/atproto/sync/getRecord.ts +++ b/packages/pds/src/api/com/atproto/sync/getRecord.ts @@ -10,7 +10,7 @@ import { assertRepoAvailability } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRecord({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken(), handler: async ({ params, auth }) => { const { did, collection, rkey } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index f4a93d483b0..3282220fda0 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -7,10 +7,13 @@ import { SqlRepoReader, } from '../../../../actor-store/repo/sql-repo-reader' import { assertRepoAvailability } from './util' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRepo({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, auth }) => { const { did, since } = params await assertRepoAvailability( diff --git a/packages/pds/src/api/com/atproto/sync/listBlobs.ts b/packages/pds/src/api/com/atproto/sync/listBlobs.ts index 0466386731e..97d04644d11 100644 --- a/packages/pds/src/api/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/api/com/atproto/sync/listBlobs.ts @@ -1,10 +1,13 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { assertRepoAvailability } from './util' +import { AuthScope } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.listBlobs({ - auth: ctx.authVerifier.optionalAccessOrAdminToken, + auth: ctx.authVerifier.optionalAccessOrAdminToken({ + additional: [AuthScope.Takendown], + }), handler: async ({ params, auth }) => { const { did, since, limit, cursor } = params await assertRepoAvailability( diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 22bfc7d7bd6..53c93c8abd8 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -210,17 +210,19 @@ export class AuthVerifier { return this.validateAdminToken(ctx) } - optionalAccessOrAdminToken = async ( - ctx: ReqCtx, - ): Promise => { - if (isAccessToken(ctx.req)) { - return await this.accessStandard()(ctx) - } else if (isBasicToken(ctx.req)) { - return await this.adminToken(ctx) - } else { - return this.null(ctx) + optionalAccessOrAdminToken = + (opts: Partial = {}) => + async ( + ctx: ReqCtx, + ): Promise => { + if (isAccessToken(ctx.req)) { + return await this.accessStandard(opts)(ctx) + } else if (isBasicToken(ctx.req)) { + return await this.adminToken(ctx) + } else { + return this.null(ctx) + } } - } userServiceAuth = async (ctx: ReqCtx): Promise => { const payload = await this.verifyServiceJwt(ctx, {