Skip to content

Commit

Permalink
Add phone verification
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge committed Jul 13, 2022
1 parent 969cb86 commit 6d50167
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 3 deletions.
5 changes: 4 additions & 1 deletion components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getLoggedInUser(): Promise<User>;
getTerms(): Promise<Terms>;
updateLoggedInUser(user: Partial<User>): Promise<User>;
needsVerification(): Promise<boolean>;
sendPhoneNumberVerificationToken(phoneNumber: string): Promise<void>;
verifyPhoneNumberVerificationToken(phoneNumber: string, token: string): Promise<boolean>;
getAuthProviders(): Promise<AuthProviderInfo[]>;
getOwnAuthProviders(): Promise<AuthProviderEntry[]>;
updateOwnAuthProvider(params: GitpodServer.UpdateOwnAuthProviderParams): Promise<AuthProviderEntry>;
Expand Down Expand Up @@ -378,7 +381,7 @@ export const createServerMock = function <C extends GitpodClient, S extends Gitp
get: (target: S, property: keyof S) => {
const result = target[property];
if (!result) {
throw new Error(`Method ${property} not implemented`);
throw new Error(`Method ${property.toString()} not implemented`);
}
return result;
},
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export namespace ErrorCodes {
// 410 No User
export const SETUP_REQUIRED = 410;

// 411 No User
export const NEEDS_VERIFICATION = 411;

// 429 Too Many Requests
export const TOO_MANY_REQUESTS = 429;

Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ export interface ProfileDetails {
companyName?: string;
// the user's email
emailAddress?: string;
// verified
verifiedPhoneNumber?: string;
}

export interface EmailNotificationSettings {
Expand Down
2 changes: 2 additions & 0 deletions components/server/ee/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { SnapshotService } from "./workspace/snapshot-service";
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
import { UserCounter } from "./user/user-counter";
import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
import { PhoneVerificationService } from "../../src/auth/phone-verification-service";

export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(Server).to(ServerEE).inSingletonScope();
Expand All @@ -81,6 +82,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
bind(BitbucketAppSupport).toSelf().inSingletonScope();
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
bind(BitbucketServerApp).toSelf().inSingletonScope();
bind(PhoneVerificationService).toSelf().inSingletonScope();

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

Expand Down
7 changes: 6 additions & 1 deletion components/server/ee/src/user/eligibility-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import { AccountStatementProvider, CachedAccountStatement } from "./account-stat
import { EMailDomainService } from "../auth/email-domain-service";
import fetch from "node-fetch";
import { Config } from "../../../src/config";
import { PhoneVerificationService } from "../../../src/auth/phone-verification-service";

export interface MayStartWorkspaceResult {
hitParallelWorkspaceLimit?: HitParallelWorkspaceLimit;
enoughCredits: boolean;
needsPhoneNumberVerification?: boolean;
}

export interface HitParallelWorkspaceLimit {
Expand Down Expand Up @@ -57,6 +59,7 @@ export class EligibilityService {
@inject(AccountStatementProvider) protected readonly accountStatementProvider: AccountStatementProvider;
@inject(TeamSubscriptionDB) protected readonly teamSubscriptionDb: TeamSubscriptionDB;
@inject(TeamSubscription2DB) protected readonly teamSubscription2Db: TeamSubscription2DB;
@inject(PhoneVerificationService) protected readonly phoneVerificationService: PhoneVerificationService;

/**
* Whether the given user is recognized as a student within Gitpod
Expand Down Expand Up @@ -160,14 +163,16 @@ export class EligibilityService {
return undefined;
}
};
const [enoughCredits, hitParallelWorkspaceLimit] = await Promise.all([
const [enoughCredits, hitParallelWorkspaceLimit, needsPhoneNumberVerification] = await Promise.all([
this.checkEnoughCreditForWorkspaceStart(user.id, date, runningInstances),
hasHitParallelWorkspaceLimit(),
this.phoneVerificationService.needsVerification(user),
]);

return {
enoughCredits: !!enoughCredits,
hitParallelWorkspaceLimit,
needsPhoneNumberVerification,
};
}

Expand Down
3 changes: 3 additions & 0 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
await super.mayStartWorkspace(ctx, user, runningInstances);

const result = await this.eligibilityService.mayStartWorkspace(user, new Date(), runningInstances);
if (result.needsPhoneNumberVerification) {
throw new ResponseError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`);
}
if (!result.enoughCredits) {
throw new ResponseError(
ErrorCodes.NOT_ENOUGH_CREDIT,
Expand Down
1 change: 1 addition & 0 deletions components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"reflect-metadata": "^0.1.10",
"stripe": "^9.0.0",
"swot-js": "^1.0.3",
"twilio": "^3.78.0",
"uuid": "^8.3.2",
"vscode-ws-jsonrpc": "^0.2.0",
"ws": "^7.4.6"
Expand Down
61 changes: 61 additions & 0 deletions components/server/src/auth/phone-verification-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 { User } from "@gitpod/gitpod-protocol";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
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";

@injectable()
export class PhoneVerificationService {
@inject(Config) protected config: Config;
@inject(WorkspaceDB) protected workspaceDB: WorkspaceDB;

protected verifyService: ServiceContext;

@postConstruct()
protected initialize(): void {
if (this.config.phoneVerification) {
const client = new Twilio(
this.config.phoneVerification.accountSID,
this.config.phoneVerification.authToken,
);
this.verifyService = client.verify.v2.services(this.config.phoneVerification.serviceName);
}
}

public async needsVerification(user: User): Promise<boolean> {
if (!this.config.phoneVerification) {
return false;
}
if (!!user.additionalData?.profile?.verifiedPhoneNumber) {
return false;
}
return true;
}

public async sendVerificationToken(phoneNumber: string): Promise<void> {
if (!this.verifyService) {
throw new Error("No verification service configured.");
}
const verification = await this.verifyService.verifications.create({ to: phoneNumber, channel: "sms" });
log.info("Verification code sent", { phoneNumber, status: verification.status });
}

public async verifyVerificationToken(phoneNumber: string, oneTimePassword: string): Promise<boolean> {
if (!this.verifyService) {
throw new Error("No verification service configured.");
}
const verification_check = await this.verifyService.verificationChecks.create({
to: phoneNumber,
code: oneTimePassword,
});
return verification_check.status === "approved";
}
}
3 changes: 3 additions & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
getLoggedInUser: { group: "default", points: 1 },
getTerms: { group: "default", points: 1 },
updateLoggedInUser: { group: "default", points: 1 },
needsVerification: { group: "default", points: 1 },
sendPhoneNumberVerificationToken: { group: "default", points: 1 },
verifyPhoneNumberVerificationToken: { group: "default", points: 1 },
getAuthProviders: { group: "default", points: 1 },
getOwnAuthProviders: { group: "default", points: 1 },
updateOwnAuthProvider: { group: "default", points: 1 },
Expand Down
6 changes: 6 additions & 0 deletions components/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ export interface ConfigSerialized {
* considered inactive.
*/
inactivityPeriodForRepos?: number;

phoneVerification?: {
serviceName: string;
accountSID: string;
authToken: string;
};
}

export namespace ConfigFile {
Expand Down
34 changes: 34 additions & 0 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ import { Currency } from "@gitpod/gitpod-protocol/lib/plans";
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage";
import { WorkspaceClusterImagebuilderClientProvider } from "./workspace-cluster-imagebuilder-client-provider";
import { PhoneVerificationService } from "../auth/phone-verification-service";

// shortcut
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
Expand Down Expand Up @@ -237,6 +238,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

@inject(IDEConfigService) protected readonly ideConfigService: IDEConfigService;

@inject(PhoneVerificationService) protected readonly phoneVerificationService: PhoneVerificationService;

/** Id the uniquely identifies this server instance */
public readonly uuid: string = uuidv4();
public readonly clientMetadata: ClientMetadata;
Expand Down Expand Up @@ -457,6 +460,37 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
return user;
}

public async needsVerification(ctx: TraceContext): Promise<boolean> {
const user = this.checkUser("needsVerification");
return this.phoneVerificationService.needsVerification(user);
}

public async sendPhoneNumberVerificationToken(ctx: TraceContext, phoneNumber: string): Promise<void> {
this.checkUser("sendPhoneNumberVerificationToken");
return this.phoneVerificationService.sendVerificationToken(phoneNumber);
}

public async verifyPhoneNumberVerificationToken(
ctx: TraceContext,
phoneNumber: string,
token: string,
): Promise<boolean> {
const user = this.checkUser("verifyPhoneNumberVerificationToken");
const checked = await this.phoneVerificationService.verifyVerificationToken(phoneNumber, token);
if (!checked) {
return false;
}
if (!user.additionalData) {
user.additionalData = {};
}
if (!user.additionalData?.profile) {
user.additionalData.profile = {};
}
user.additionalData.profile.verifiedPhoneNumber = phoneNumber;
await this.userDB.updateUserPartial(user);
return true;
}

public async getClientRegion(ctx: TraceContext): Promise<string | undefined> {
this.checkUser("getClientRegion");
return this.clientHeaderFields?.clientRegion;
Expand Down
Loading

0 comments on commit 6d50167

Please sign in to comment.