From 5f8f09422ee3facf59c9d09f6ce9f85a6b914ff5 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 31 Jan 2024 19:56:01 -0600 Subject: [PATCH 1/2] Entryway: non-transactional createAccount (#2109) make create account non transactional --- .../api/com/atproto/server/createAccount.ts | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 1fda28e0266..77af9a180bb 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -20,6 +20,7 @@ import Database from '../../../../db' import { didDocForSession } from './util' import { getPdsEndpoint } from '../../../../pds-agents' import { isThisPds } from '../../../proxy' +import { dbLogger as log } from '../../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createAccount({ @@ -96,19 +97,6 @@ export default function (server: Server, ctx: AppContext) { throw err } - // Generate a real did with PLC - if (plcOp && !entrywayAssignedPds) { - try { - await ctx.plcClient.sendOperation(did, plcOp) - } catch (err) { - req.log.error( - { didKey: ctx.plcRotationKey.did(), handle }, - 'failed to create did:plc', - ) - throw err - } - } - // insert invite code use if (ctx.cfg.invites.required && inviteCode) { await ensureCodeIsAvailable(dbTxn, inviteCode, true) @@ -132,6 +120,10 @@ export default function (server: Server, ctx: AppContext) { .execute() } + if (!entrywayAssignedPds) { + await repoTxn.createRepo(did, [], now) + } + const { access, refresh } = await ctx.services .auth(dbTxn) .createSession({ @@ -140,6 +132,15 @@ export default function (server: Server, ctx: AppContext) { appPasswordName: null, }) + return { + did, + pdsDid: entrywayAssignedPds?.did ?? null, + accessJwt: access, + refreshJwt: refresh, + } + }) + + try { if (entrywayAssignedPds) { const agent = ctx.pdsAgents.get(entrywayAssignedPds.host) await agent.com.atproto.server.createAccount({ @@ -149,17 +150,21 @@ export default function (server: Server, ctx: AppContext) { recoveryKey: input.body.recoveryKey, }) } else { - // Setup repo root - await repoTxn.createRepo(did, [], now) - } - - return { - did, - pdsDid: entrywayAssignedPds?.did ?? null, - accessJwt: access, - refreshJwt: refresh, + assert(plcOp) + try { + await ctx.plcClient.sendOperation(did, plcOp) + } catch (err) { + req.log.error( + { didKey: ctx.plcRotationKey.did(), handle }, + 'failed to create did:plc', + ) + throw err + } } - }) + } catch (err) { + await cleanupUncreatedAccount(ctx, did) + throw err + } const didDoc = await didDocForSession(ctx, result) @@ -496,3 +501,21 @@ const randomIndexByWeight = (weights) => { const rand = Math.random() * sum return cumulative.findIndex((item) => item >= rand) } + +const cleanupUncreatedAccount = async ( + ctx: AppContext, + did: string, + tries = 0, +) => { + if (tries > 3) return + try { + await Promise.all([ + ctx.services.account(ctx.db).deleteAccount(did), + ctx.services.record(ctx.db).deleteForActor(did), + ctx.services.repo(ctx.db).deleteRepo(did), + ]) + } catch (err) { + log.error({ err, did, tries }, 'failed to clean up partially created user') + return cleanupUncreatedAccount(ctx, did, tries + 1) + } +} From 86655ea70b0ce2ff763b60721cf55b99d5292b34 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 31 Jan 2024 19:56:13 -0600 Subject: [PATCH 2/2] Phone verification bypass number (#2119) * add bypass phone number * trim earlier --- .../api/com/atproto/server/createAccount.ts | 68 +++++++++++-------- packages/pds/src/config/config.ts | 2 + packages/pds/src/config/env.ts | 2 + 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 77af9a180bb..1f487c2b770 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -44,33 +44,11 @@ export default function (server: Server, ctx: AppContext) { const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) - let verificationPhone: string | undefined = undefined - if (ctx.cfg.phoneVerification.required && ctx.twilio) { - if (!input.body.verificationPhone) { - throw new InvalidRequestError( - `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, - 'InvalidPhoneVerification', - ) - } else if (!input.body.verificationCode) { - throw new InvalidRequestError( - `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, - 'InvalidPhoneVerification', - ) - } - verificationPhone = ctx.twilio.normalizePhoneNumber( - input.body.verificationPhone, - ) - const verified = await ctx.twilio.verifyCode( - verificationPhone, - input.body.verificationCode.trim(), - ) - if (!verified) { - throw new InvalidRequestError( - 'Could not verify phone number. Please try again.', - 'InvalidPhoneVerification', - ) - } - } + const verificationPhone = await ensurePhoneVerification( + ctx, + input.body.verificationPhone, + input.body.verificationCode?.trim(), + ) const result = await ctx.db.transaction(async (dbTxn) => { const actorTxn = ctx.services.account(dbTxn) @@ -491,6 +469,42 @@ const ensureUnusedHandleAndEmail = async ( } } +const ensurePhoneVerification = async ( + ctx: AppContext, + phone?: string, + code?: string, +): Promise => { + if (!ctx.cfg.phoneVerification.required || !ctx.twilio) { + return + } + + if (!phone) { + throw new InvalidRequestError( + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, + 'InvalidPhoneVerification', + ) + } + if (ctx.cfg.phoneVerification.bypassPhoneNumber === phone) { + return undefined + } + + if (!code) { + throw new InvalidRequestError( + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, + 'InvalidPhoneVerification', + ) + } + const normalizedPhone = ctx.twilio.normalizePhoneNumber(phone) + const verified = await ctx.twilio.verifyCode(normalizedPhone, code) + if (!verified) { + throw new InvalidRequestError( + 'Could not verify phone number. Please try again.', + 'InvalidPhoneVerification', + ) + } + return normalizedPhone +} + const randomIndexByWeight = (weights) => { let sum = 0 const cumulative = weights.map((weight) => { diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 6652a2a29eb..a96462e05ab 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -137,6 +137,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { twilioAccountSid: env.twilioAccountSid, twilioServiceSid: env.twilioServiceSid, accountsPerPhoneNumber: env.accountsPerPhoneNumber ?? 3, + bypassPhoneNumber: env.bypassPhoneNumber, } } @@ -322,6 +323,7 @@ export type PhoneVerificationConfig = twilioAccountSid: string twilioServiceSid: string accountsPerPhoneNumber: number + bypassPhoneNumber?: string } | { required: false diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 1d332f9b8ff..324d74889db 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -52,6 +52,7 @@ export const readEnv = (): ServerEnvironment => { // phone verification phoneVerificationRequired: envBool('PDS_PHONE_VERIFICATION_REQUIRED'), accountsPerPhoneNumber: envInt('PDS_ACCOUNTS_PER_PHONE_NUMBER'), + bypassPhoneNumber: envStr('PDS_BYPASS_PHONE_NUMBER'), twilioAccountSid: envStr('PDS_TWILIO_ACCOUNT_SID'), twilioAuthToken: envStr('PDS_TWILIO_AUTH_TOKEN'), twilioServiceSid: envStr('PDS_TWILIO_SERVICE_SID'), @@ -166,6 +167,7 @@ export type ServerEnvironment = { // phone verification phoneVerificationRequired?: boolean accountsPerPhoneNumber?: number + bypassPhoneNumber?: string twilioAccountSid?: string twilioAuthToken?: string twilioServiceSid?: string