diff --git a/components/gitpod-db/src/typeorm/migration/1663784254956-IndexPhoneNumber.ts b/components/gitpod-db/src/typeorm/migration/1663784254956-IndexPhoneNumber.ts new file mode 100644 index 00000000000000..0b63d0f22a8e6e --- /dev/null +++ b/components/gitpod-db/src/typeorm/migration/1663784254956-IndexPhoneNumber.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2022 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { MigrationInterface, QueryRunner } from "typeorm"; +import { columnExists } from "./helper/helper"; + +const D_B_USER = "d_b_user"; +const COL_PHONE_NUMBER = "verificationPhoneNumber"; + +export class IndexPhoneNumber1663784254956 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + if (!(await columnExists(queryRunner, D_B_USER, COL_PHONE_NUMBER))) { + await queryRunner.query( + `ALTER TABLE ${D_B_USER} ADD INDEX (${COL_PHONE_NUMBER}), ALGORITHM=INPLACE, LOCK=NONE `, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/components/gitpod-db/src/typeorm/user-db-impl.ts b/components/gitpod-db/src/typeorm/user-db-impl.ts index 94cbd8c9de9ff9..c6e82aa7233720 100644 --- a/components/gitpod-db/src/typeorm/user-db-impl.ts +++ b/components/gitpod-db/src/typeorm/user-db-impl.ts @@ -570,6 +570,22 @@ export class TypeORMUserDBImpl implements UserDB { async getByRefreshToken(refreshTokenToken: string): Promise { throw new Error("Not implemented"); } + + async countUsagesOfPhoneNumber(phoneNumber: string): Promise { + return (await this.getUserRepo()) + .createQueryBuilder() + .where("verificationPhoneNumber = :phoneNumber", { phoneNumber }) + .getCount(); + } + + async isBlockedPhoneNumber(phoneNumber: string): Promise { + const blockedUsers = await (await this.getUserRepo()) + .createQueryBuilder() + .where("verificationPhoneNumber = :phoneNumber", { phoneNumber }) + .andWhere("blocked = true") + .getCount(); + return blockedUsers > 0; + } } export class TransactionalUserDBImpl extends TypeORMUserDBImpl { diff --git a/components/gitpod-db/src/user-db.ts b/components/gitpod-db/src/user-db.ts index 88e798d2e08325..a022b08eb70049 100644 --- a/components/gitpod-db/src/user-db.ts +++ b/components/gitpod-db/src/user-db.ts @@ -146,6 +146,8 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository { storeGitpodToken(token: GitpodToken & { user: DBUser }): Promise; deleteGitpodToken(tokenHash: string): Promise; deleteGitpodTokensNamedLike(userId: string, namePattern: string): Promise; + countUsagesOfPhoneNumber(phoneNumber: string): Promise; + isBlockedPhoneNumber(phoneNumber: string): Promise; } export type PartialUserUpdate = Partial> & Pick; diff --git a/components/server/src/auth/verification-service.ts b/components/server/src/auth/verification-service.ts index 8db34ca2111d5c..1993b48818155b 100644 --- a/components/server/src/auth/verification-service.ts +++ b/components/server/src/auth/verification-service.ts @@ -10,13 +10,14 @@ import { inject, injectable, postConstruct } from "inversify"; import { Config } from "../config"; import { Twilio } from "twilio"; import { ServiceContext } from "twilio/lib/rest/verify/v2/service"; -import { WorkspaceDB } from "@gitpod/gitpod-db/lib"; +import { UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib"; import { ConfigCatClientFactory } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; @injectable() export class VerificationService { @inject(Config) protected config: Config; @inject(WorkspaceDB) protected workspaceDB: WorkspaceDB; + @inject(UserDB) protected userDB: UserDB; @inject(ConfigCatClientFactory) protected readonly configCatClientFactory: ConfigCatClientFactory; protected verifyService: ServiceContext; @@ -59,6 +60,14 @@ export class VerificationService { if (!this.verifyService) { throw new Error("No verification service configured."); } + const isBlockedNumber = this.userDB.countUsagesOfPhoneNumber(phoneNumber); + const usages = await this.userDB.countUsagesOfPhoneNumber(phoneNumber); + if (usages > 3) { + throw new Error("The given phone number has been used more than three times."); + } + if (await isBlockedNumber) { + throw new Error("The given phone number is blocked due to abuse."); + } const verification = await this.verifyService.verifications.create({ to: phoneNumber, channel: "sms" }); log.info("Verification code sent", { phoneNumber, status: verification.status }); }