From e78faa5e57534ae672cb46e0e59db52d4d03e244 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 7 Feb 2024 21:09:24 -0600 Subject: [PATCH] Entryway: multi verifier (#2156) * add multi verifier * add second try flag --- packages/pds/src/config/config.ts | 29 ++++++- packages/pds/src/context.ts | 15 ++++ packages/pds/src/phone-verification/multi.ts | 85 ++++++++++++++++++++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 packages/pds/src/phone-verification/multi.ts diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index c288795f1f1..c318cddd0c5 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -131,7 +131,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { } if (env.phoneVerificationRequired) { const provider = env.phoneVerificationProvider - let providerCfg: TwilioConfig | PlivoConfig + let providerCfg: TwilioConfig | PlivoConfig | MultiVerifierConfig if (provider === 'twilio') { assert(env.twilioAccountSid) assert(env.twilioServiceSid) @@ -148,6 +148,25 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { authId: env.plivoAuthId, appId: env.plivoAppId, } + } else if (provider === 'multi') { + assert(env.twilioAccountSid) + assert(env.twilioServiceSid) + assert(env.plivoAuthId) + assert(env.plivoAppId) + + providerCfg = { + provider, + twilio: { + provider: 'twilio', + accountSid: env.twilioAccountSid, + serviceSid: env.twilioServiceSid, + }, + plivo: { + provider: 'plivo', + authId: env.plivoAuthId, + appId: env.plivoAppId, + }, + } } else { throw new Error(`invalid phone verification provider: ${provider}`) } @@ -350,7 +369,7 @@ export type InvitesConfig = export type PhoneVerificationConfig = | { required: true - provider: TwilioConfig | PlivoConfig + provider: TwilioConfig | PlivoConfig | MultiVerifierConfig accountsPerPhoneNumber: number bypassPhoneNumber?: string } @@ -370,6 +389,12 @@ export type PlivoConfig = { appId: string } +export type MultiVerifierConfig = { + provider: 'multi' + twilio: TwilioConfig + plivo: PlivoConfig +} + export type EmailConfig = { smtpUrl: string fromAddress: string diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 1d38b5f1e99..383e4158836 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -30,6 +30,7 @@ import { DAY } from '@atproto/common' import { PhoneVerifier } from './phone-verification/util' import { TwilioClient } from './phone-verification/twilio' import { PlivoClient } from './phone-verification/plivo' +import { MultiVerifier } from './phone-verification/multi' export type AppContextOptions = { db: Database @@ -241,6 +242,20 @@ export class AppContext { appId: cfg.phoneVerification.provider.appId, authToken: secrets.plivoAuthToken, }) + } else if (cfg.phoneVerification.provider.provider === 'multi') { + assert(secrets.twilioAuthToken, 'expected twilio auth token') + assert(secrets.plivoAuthToken, 'expected plivo auth token') + const twilio = new TwilioClient({ + accountSid: cfg.phoneVerification.provider.twilio.accountSid, + serviceSid: cfg.phoneVerification.provider.twilio.serviceSid, + authToken: secrets.twilioAuthToken, + }) + const plivo = new PlivoClient(db, { + authId: cfg.phoneVerification.provider.plivo.authId, + appId: cfg.phoneVerification.provider.plivo.appId, + authToken: secrets.plivoAuthToken, + }) + phoneVerifier = new MultiVerifier(db, twilio, plivo) } } diff --git a/packages/pds/src/phone-verification/multi.ts b/packages/pds/src/phone-verification/multi.ts new file mode 100644 index 00000000000..e7c2324c333 --- /dev/null +++ b/packages/pds/src/phone-verification/multi.ts @@ -0,0 +1,85 @@ +import Database from '../db' +import { PlivoClient } from './plivo' +import { TwilioClient } from './twilio' +import { SECOND } from '@atproto/common' +import { randomIntFromSeed } from '@atproto/crypto' +import { PhoneVerifier } from './util' + +const PLIVO_RATIO_FLAG = 'phone-verification:plivoRatio' +const SECOND_TRY_FLAG = 'phone-verification:attemptSecondTry' + +export class MultiVerifier implements PhoneVerifier { + plivoRatio = 0 + attemptSecondTry = false + lastRefreshed = 0 + + constructor( + public db: Database, + public twilio: TwilioClient, + public plivo: PlivoClient, + ) {} + + async checkRefreshRatio() { + if (Date.now() - this.lastRefreshed > 30 * SECOND) { + await this.refreshRatio() + } + } + + async refreshRatio() { + const res = await this.db.db + .selectFrom('runtime_flag') + .where('name', '=', PLIVO_RATIO_FLAG) + .orWhere('name', '=', SECOND_TRY_FLAG) + .selectAll() + .execute() + + this.plivoRatio = parseMaybeInt( + res.find((val) => val.name === PLIVO_RATIO_FLAG)?.value, + ) + this.attemptSecondTry = + res.find((val) => val.name === SECOND_TRY_FLAG)?.value === 'true' + + this.lastRefreshed = Date.now() + } + + async sendCode(phoneNumber: string) { + await this.checkRefreshRatio() + const id = await randomIntFromSeed(phoneNumber, 10, 0) + if (id < this.plivoRatio) { + await this.plivo.sendCode(phoneNumber) + } else { + await this.twilio.sendCode(phoneNumber) + } + } + + async verifyCode(phoneNumber: string, code: string) { + await this.checkRefreshRatio() + const id = await randomIntFromSeed(phoneNumber, 10, 0) + const firstTry = + id < this.plivoRatio + ? () => this.plivo.verifyCode(phoneNumber, code) + : () => this.twilio.verifyCode(phoneNumber, code) + const secondTry = + id < this.plivoRatio + ? () => this.plivo.verifyCode(phoneNumber, code) + : () => this.twilio.verifyCode(phoneNumber, code) + try { + return await firstTry() + } catch (err) { + if (this.attemptSecondTry) { + return await secondTry() + } else { + throw err + } + } + } +} + +const parseMaybeInt = (str?: string): number => { + if (!str) return 0 + const parsed = parseInt(str) + if (isNaN(parsed) || parsed < 0) { + return 0 + } + return parsed +}