Skip to content

Commit 6d50167

Browse files
committed
Add phone verification
1 parent 969cb86 commit 6d50167

File tree

12 files changed

+209
-3
lines changed

12 files changed

+209
-3
lines changed

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
8282
getLoggedInUser(): Promise<User>;
8383
getTerms(): Promise<Terms>;
8484
updateLoggedInUser(user: Partial<User>): Promise<User>;
85+
needsVerification(): Promise<boolean>;
86+
sendPhoneNumberVerificationToken(phoneNumber: string): Promise<void>;
87+
verifyPhoneNumberVerificationToken(phoneNumber: string, token: string): Promise<boolean>;
8588
getAuthProviders(): Promise<AuthProviderInfo[]>;
8689
getOwnAuthProviders(): Promise<AuthProviderEntry[]>;
8790
updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;
@@ -378,7 +381,7 @@ export const createServerMock = function <C extends GitpodClient, S extends Gitp
378381
get: (target: S, property: keyof S) => {
379382
const result = target[property];
380383
if (!result) {
381-
throw new Error(`Method ${property} not implemented`);
384+
throw new Error(`Method ${property.toString()} not implemented`);
382385
}
383386
return result;
384387
},

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export namespace ErrorCodes {
2626
// 410 No User
2727
export const SETUP_REQUIRED = 410;
2828

29+
// 411 No User
30+
export const NEEDS_VERIFICATION = 411;
31+
2932
// 429 Too Many Requests
3033
export const TOO_MANY_REQUESTS = 429;
3134

components/gitpod-protocol/src/protocol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export interface ProfileDetails {
215215
companyName?: string;
216216
// the user's email
217217
emailAddress?: string;
218+
// verified
219+
verifiedPhoneNumber?: string;
218220
}
219221

220222
export interface EmailNotificationSettings {

components/server/ee/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import { SnapshotService } from "./workspace/snapshot-service";
6161
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
6262
import { UserCounter } from "./user/user-counter";
6363
import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
64+
import { PhoneVerificationService } from "../../src/auth/phone-verification-service";
6465

6566
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
6667
rebind(Server).to(ServerEE).inSingletonScope();
@@ -81,6 +82,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
8182
bind(BitbucketAppSupport).toSelf().inSingletonScope();
8283
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
8384
bind(BitbucketServerApp).toSelf().inSingletonScope();
85+
bind(PhoneVerificationService).toSelf().inSingletonScope();
8486

8587
bind(UserCounter).toSelf().inSingletonScope();
8688

components/server/ee/src/user/eligibility-service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import { AccountStatementProvider, CachedAccountStatement } from "./account-stat
2323
import { EMailDomainService } from "../auth/email-domain-service";
2424
import fetch from "node-fetch";
2525
import { Config } from "../../../src/config";
26+
import { PhoneVerificationService } from "../../../src/auth/phone-verification-service";
2627

2728
export interface MayStartWorkspaceResult {
2829
hitParallelWorkspaceLimit?: HitParallelWorkspaceLimit;
2930
enoughCredits: boolean;
31+
needsPhoneNumberVerification?: boolean;
3032
}
3133

3234
export interface HitParallelWorkspaceLimit {
@@ -57,6 +59,7 @@ export class EligibilityService {
5759
@inject(AccountStatementProvider) protected readonly accountStatementProvider: AccountStatementProvider;
5860
@inject(TeamSubscriptionDB) protected readonly teamSubscriptionDb: TeamSubscriptionDB;
5961
@inject(TeamSubscription2DB) protected readonly teamSubscription2Db: TeamSubscription2DB;
62+
@inject(PhoneVerificationService) protected readonly phoneVerificationService: PhoneVerificationService;
6063

6164
/**
6265
* Whether the given user is recognized as a student within Gitpod
@@ -160,14 +163,16 @@ export class EligibilityService {
160163
return undefined;
161164
}
162165
};
163-
const [enoughCredits, hitParallelWorkspaceLimit] = await Promise.all([
166+
const [enoughCredits, hitParallelWorkspaceLimit, needsPhoneNumberVerification] = await Promise.all([
164167
this.checkEnoughCreditForWorkspaceStart(user.id, date, runningInstances),
165168
hasHitParallelWorkspaceLimit(),
169+
this.phoneVerificationService.needsVerification(user),
166170
]);
167171

168172
return {
169173
enoughCredits: !!enoughCredits,
170174
hitParallelWorkspaceLimit,
175+
needsPhoneNumberVerification,
171176
};
172177
}
173178

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
241241
await super.mayStartWorkspace(ctx, user, runningInstances);
242242

243243
const result = await this.eligibilityService.mayStartWorkspace(user, new Date(), runningInstances);
244+
if (result.needsPhoneNumberVerification) {
245+
throw new ResponseError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`);
246+
}
244247
if (!result.enoughCredits) {
245248
throw new ResponseError(
246249
ErrorCodes.NOT_ENOUGH_CREDIT,

components/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"reflect-metadata": "^0.1.10",
7979
"stripe": "^9.0.0",
8080
"swot-js": "^1.0.3",
81+
"twilio": "^3.78.0",
8182
"uuid": "^8.3.2",
8283
"vscode-ws-jsonrpc": "^0.2.0",
8384
"ws": "^7.4.6"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 { User } from "@gitpod/gitpod-protocol";
8+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
9+
import { inject, injectable, postConstruct } from "inversify";
10+
import { Config } from "../config";
11+
import { Twilio } from "twilio";
12+
import { ServiceContext } from "twilio/lib/rest/verify/v2/service";
13+
import { WorkspaceDB } from "@gitpod/gitpod-db/lib";
14+
15+
@injectable()
16+
export class PhoneVerificationService {
17+
@inject(Config) protected config: Config;
18+
@inject(WorkspaceDB) protected workspaceDB: WorkspaceDB;
19+
20+
protected verifyService: ServiceContext;
21+
22+
@postConstruct()
23+
protected initialize(): void {
24+
if (this.config.phoneVerification) {
25+
const client = new Twilio(
26+
this.config.phoneVerification.accountSID,
27+
this.config.phoneVerification.authToken,
28+
);
29+
this.verifyService = client.verify.v2.services(this.config.phoneVerification.serviceName);
30+
}
31+
}
32+
33+
public async needsVerification(user: User): Promise<boolean> {
34+
if (!this.config.phoneVerification) {
35+
return false;
36+
}
37+
if (!!user.additionalData?.profile?.verifiedPhoneNumber) {
38+
return false;
39+
}
40+
return true;
41+
}
42+
43+
public async sendVerificationToken(phoneNumber: string): Promise<void> {
44+
if (!this.verifyService) {
45+
throw new Error("No verification service configured.");
46+
}
47+
const verification = await this.verifyService.verifications.create({ to: phoneNumber, channel: "sms" });
48+
log.info("Verification code sent", { phoneNumber, status: verification.status });
49+
}
50+
51+
public async verifyVerificationToken(phoneNumber: string, oneTimePassword: string): Promise<boolean> {
52+
if (!this.verifyService) {
53+
throw new Error("No verification service configured.");
54+
}
55+
const verification_check = await this.verifyService.verificationChecks.create({
56+
to: phoneNumber,
57+
code: oneTimePassword,
58+
});
59+
return verification_check.status === "approved";
60+
}
61+
}

components/server/src/auth/rate-limiter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
5151
getLoggedInUser: { group: "default", points: 1 },
5252
getTerms: { group: "default", points: 1 },
5353
updateLoggedInUser: { group: "default", points: 1 },
54+
needsVerification: { group: "default", points: 1 },
55+
sendPhoneNumberVerificationToken: { group: "default", points: 1 },
56+
verifyPhoneNumberVerificationToken: { group: "default", points: 1 },
5457
getAuthProviders: { group: "default", points: 1 },
5558
getOwnAuthProviders: { group: "default", points: 1 },
5659
updateOwnAuthProvider: { group: "default", points: 1 },

components/server/src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ export interface ConfigSerialized {
178178
* considered inactive.
179179
*/
180180
inactivityPeriodForRepos?: number;
181+
182+
phoneVerification?: {
183+
serviceName: string;
184+
accountSID: string;
185+
authToken: string;
186+
};
181187
}
182188

183189
export namespace ConfigFile {

0 commit comments

Comments
 (0)