Skip to content

Commit dd09f79

Browse files
committed
[server] restrict allowed phone numbers
1 parent 689b7f8 commit dd09f79

File tree

8 files changed

+114
-4
lines changed

8 files changed

+114
-4
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { MigrationInterface, QueryRunner } from "typeorm";
8+
import { columnExists } from "./helper/helper";
9+
10+
const D_B_USER = "d_b_user";
11+
const COL_PHONE_NUMBER = "verificationPhoneNumber";
12+
13+
export class IndexPhoneNumber1663784254956 implements MigrationInterface {
14+
public async up(queryRunner: QueryRunner): Promise<void> {
15+
if (!(await columnExists(queryRunner, D_B_USER, COL_PHONE_NUMBER))) {
16+
await queryRunner.query(
17+
`ALTER TABLE ${D_B_USER} ADD INDEX (${COL_PHONE_NUMBER}), ALGORITHM=INPLACE, LOCK=NONE `,
18+
);
19+
}
20+
}
21+
22+
public async down(queryRunner: QueryRunner): Promise<void> {}
23+
}

components/gitpod-db/src/typeorm/user-db-impl.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,22 @@ export class TypeORMUserDBImpl implements UserDB {
570570
async getByRefreshToken(refreshTokenToken: string): Promise<OAuthToken> {
571571
throw new Error("Not implemented");
572572
}
573+
574+
async countUsagesOfPhoneNumber(phoneNumber: string): Promise<number> {
575+
return (await this.getUserRepo())
576+
.createQueryBuilder()
577+
.where("verificationPhoneNumber = :phoneNumber", { phoneNumber })
578+
.getCount();
579+
}
580+
581+
async isBlockedPhoneNumber(phoneNumber: string): Promise<boolean> {
582+
const blockedUsers = await (await this.getUserRepo())
583+
.createQueryBuilder()
584+
.where("verificationPhoneNumber = :phoneNumber", { phoneNumber })
585+
.andWhere("blocked = true")
586+
.getCount();
587+
return blockedUsers > 0;
588+
}
573589
}
574590

575591
export class TransactionalUserDBImpl extends TypeORMUserDBImpl {

components/gitpod-db/src/user-db.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ export interface UserDB extends OAuthUserRepository, OAuthTokenRepository {
146146
storeGitpodToken(token: GitpodToken & { user: DBUser }): Promise<void>;
147147
deleteGitpodToken(tokenHash: string): Promise<void>;
148148
deleteGitpodTokensNamedLike(userId: string, namePattern: string): Promise<void>;
149+
countUsagesOfPhoneNumber(phoneNumber: string): Promise<number>;
150+
isBlockedPhoneNumber(phoneNumber: string): Promise<boolean>;
149151
}
150152
export type PartialUserUpdate = Partial<Omit<User, "identities">> & Pick<User, "id">;
151153

components/gitpod-protocol/src/messaging/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,7 @@ export namespace ErrorCodes {
9494

9595
// 640 Headless logs are not available (yet)
9696
export const HEADLESS_LOG_NOT_YET_AVAILABLE = 640;
97+
98+
// 650 Invalid Value
99+
export const INVALID_VALUE = 650;
97100
}

components/server/src/auth/verification-service.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import { inject, injectable, postConstruct } from "inversify";
1010
import { Config } from "../config";
1111
import { Twilio } from "twilio";
1212
import { ServiceContext } from "twilio/lib/rest/verify/v2/service";
13-
import { WorkspaceDB } from "@gitpod/gitpod-db/lib";
13+
import { UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib";
1414
import { ConfigCatClientFactory } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
15+
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
16+
import { ResponseError } from "vscode-ws-jsonrpc";
1517

1618
@injectable()
1719
export class VerificationService {
1820
@inject(Config) protected config: Config;
1921
@inject(WorkspaceDB) protected workspaceDB: WorkspaceDB;
22+
@inject(UserDB) protected userDB: UserDB;
2023
@inject(ConfigCatClientFactory) protected readonly configCatClientFactory: ConfigCatClientFactory;
2124

2225
protected verifyService: ServiceContext;
@@ -59,6 +62,17 @@ export class VerificationService {
5962
if (!this.verifyService) {
6063
throw new Error("No verification service configured.");
6164
}
65+
const isBlockedNumber = this.userDB.isBlockedPhoneNumber(phoneNumber);
66+
const usages = await this.userDB.countUsagesOfPhoneNumber(phoneNumber);
67+
if (usages > 3) {
68+
throw new ResponseError(
69+
ErrorCodes.INVALID_VALUE,
70+
"The given phone number has been used more than three times.",
71+
);
72+
}
73+
if (await isBlockedNumber) {
74+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "The given phone number is blocked due to abuse.");
75+
}
6276
const verification = await this.verifyService.verifications.create({ to: phoneNumber, channel: "sms" });
6377
log.info("Verification code sent", { phoneNumber, status: verification.status });
6478
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as chai from "chai";
8+
import { suite, test } from "mocha-typescript";
9+
import { formatPhoneNumber } from "./phone-numbers";
10+
const expect = chai.expect;
11+
12+
@suite
13+
export class PhoneNumberSpec {
14+
@test public testFormatPhoneNumber() {
15+
const tests = [
16+
["00123234254", "+123234254"],
17+
["+1 232 34 254", "+123234254"],
18+
["001 23234-254", "+123234254"],
19+
["0012swedfkwejfew32sdf3sdvsf sdv fsdv4254", "+123234254"],
20+
];
21+
22+
for (const test of tests) {
23+
expect(formatPhoneNumber(test[0]), "Values : " + JSON.stringify(test)).to.eq(test[1]);
24+
}
25+
}
26+
}
27+
module.exports = new PhoneNumberSpec();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
/**
8+
* a simply cleaning method, that removes all non numbers and repaces a leading '00' with a leading '+' sign.
9+
*
10+
* @param phoneNumber
11+
* @returns formatted phone number
12+
*/
13+
export function formatPhoneNumber(phoneNumber: string): string {
14+
var cleanPhoneNumber = phoneNumber.trim();
15+
if (cleanPhoneNumber.startsWith("+")) {
16+
cleanPhoneNumber = "00" + cleanPhoneNumber.substring(1);
17+
}
18+
cleanPhoneNumber = cleanPhoneNumber.replace(/\D/g, "");
19+
if (cleanPhoneNumber.startsWith("00")) {
20+
cleanPhoneNumber = "+" + cleanPhoneNumber.substring(2);
21+
}
22+
return cleanPhoneNumber;
23+
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ import { VerificationService } from "../auth/verification-service";
178178
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
179179
import { EntitlementService } from "../billing/entitlement-service";
180180
import { WorkspaceClasses } from "./workspace-classes";
181+
import { formatPhoneNumber } from "../user/phone-numbers";
181182

182183
// shortcut
183184
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -469,16 +470,17 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
469470
return user;
470471
}
471472

472-
public async sendPhoneNumberVerificationToken(ctx: TraceContext, phoneNumber: string): Promise<void> {
473+
public async sendPhoneNumberVerificationToken(ctx: TraceContext, rawPhoneNumber: string): Promise<void> {
473474
this.checkUser("sendPhoneNumberVerificationToken");
474-
return this.verificationService.sendVerificationToken(phoneNumber);
475+
return this.verificationService.sendVerificationToken(formatPhoneNumber(rawPhoneNumber));
475476
}
476477

477478
public async verifyPhoneNumberVerificationToken(
478479
ctx: TraceContext,
479-
phoneNumber: string,
480+
rawPhoneNumber: string,
480481
token: string,
481482
): Promise<boolean> {
483+
const phoneNumber = formatPhoneNumber(rawPhoneNumber);
482484
const user = this.checkUser("verifyPhoneNumberVerificationToken");
483485
const checked = await this.verificationService.verifyVerificationToken(phoneNumber, token);
484486
if (!checked) {

0 commit comments

Comments
 (0)