Skip to content

Commit 175d079

Browse files
committed
Implement user account verification with LinkedIn during onboarding
1 parent 0095dce commit 175d079

File tree

20 files changed

+182
-2
lines changed

20 files changed

+182
-2
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"react-dom": "^17.0.1",
2929
"react-error-boundary": "^3.1.4",
3030
"react-intl-tel-input": "^8.2.0",
31+
"react-linkedin-login-oauth2": "^2.0.1",
3132
"react-popper": "^2.3.0",
3233
"react-portal": "^4.2.2",
3334
"react-router-dom": "^5.2.0",
Lines changed: 3 additions & 0 deletions
Loading

components/dashboard/src/onboarding/StepUserInfo.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@
66

77
import { User } from "@gitpod/gitpod-protocol";
88
import { FC, useCallback, useState } from "react";
9+
import { useLinkedIn } from "react-linkedin-login-oauth2";
910
import { TextInputField } from "../components/forms/TextInputField";
1011
import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation";
1112
import { useOnBlurError } from "../hooks/use-onblur-error";
1213
import { OnboardingStep } from "./OnboardingStep";
14+
import SignInWithLinkedIn from "../images/sign-in-with-linkedin.svg";
1315

1416
type Props = {
1517
user: User;
18+
linkedInClientId: string;
1619
onComplete(user: User): void;
1720
};
18-
export const StepUserInfo: FC<Props> = ({ user, onComplete }) => {
21+
export const StepUserInfo: FC<Props> = ({ user, linkedInClientId, onComplete }) => {
1922
const updateUser = useUpdateCurrentUserMutation();
2023
// attempt to split provided name for default input values
2124
const { first, last } = getInitialNameParts(user);
@@ -25,6 +28,17 @@ export const StepUserInfo: FC<Props> = ({ user, onComplete }) => {
2528
// Email purposefully not pre-filled
2629
const [emailAddress, setEmailAddress] = useState("");
2730

31+
const { linkedInLogin } = useLinkedIn({
32+
clientId: linkedInClientId,
33+
redirectUri: `${window.location.origin}/linkedin`,
34+
onSuccess: (code) => {
35+
console.log(code);
36+
},
37+
onError: (error) => {
38+
console.log(error);
39+
},
40+
});
41+
2842
const handleSubmit = useCallback(async () => {
2943
const additionalData = user.additionalData || {};
3044
const profile = additionalData.profile || {};
@@ -102,6 +116,13 @@ export const StepUserInfo: FC<Props> = ({ user, onComplete }) => {
102116
onChange={setEmailAddress}
103117
required
104118
/>
119+
120+
<img
121+
onClick={linkedInLogin}
122+
src={SignInWithLinkedIn}
123+
alt="Sign in with Linked In"
124+
style={{ maxWidth: "180px", cursor: "pointer" }}
125+
/>
105126
</OnboardingStep>
106127
);
107128
};

components/dashboard/src/onboarding/UserOnboarding.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { User } from "@gitpod/gitpod-protocol";
8-
import { FunctionComponent, useCallback, useContext, useState } from "react";
8+
import { FunctionComponent, useCallback, useContext, useEffect, useState } from "react";
99
import gitpodIcon from "../icons/gitpod.svg";
1010
import { Separator } from "../components/Separator";
1111
import { useHistory, useLocation } from "react-router";
@@ -40,6 +40,14 @@ const UserOnboarding: FunctionComponent<Props> = ({ user }) => {
4040

4141
const [step, setStep] = useState(STEPS.ONE);
4242
const [completingError, setCompletingError] = useState("");
43+
const [linkedInClientId, setLinkedInClientId] = useState("");
44+
45+
useEffect(() => {
46+
(async () => {
47+
const clientId = await getGitpodService().server.getLinkedInClientId();
48+
setLinkedInClientId(clientId);
49+
})();
50+
}, []);
4351

4452
// We track this state here so we can persist it at the end of the flow instead of when it's selected
4553
// This is because setting the ide is how we indicate a user has onboarded, and want to defer that until the end
@@ -134,6 +142,7 @@ const UserOnboarding: FunctionComponent<Props> = ({ user }) => {
134142
{step === STEPS.ONE && (
135143
<StepUserInfo
136144
user={user}
145+
linkedInClientId={linkedInClientId}
137146
onComplete={(updatedUser) => {
138147
setUser(updatedUser);
139148
setStep(STEPS.TWO);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Copyright (c) 2023 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+
export const LinkedInTokenDB = Symbol("LinkedInTokenDB");
8+
export interface LinkedInTokenDB {}

components/gitpod-db/src/tables.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,11 @@ export class GitpodTableDescriptionProvider implements TableDescriptionProvider
340340
timeColumn: "_lastModified",
341341
deletionColumn: "deleted",
342342
},
343+
{
344+
name: "d_b_linked_in_token",
345+
primaryKeys: ["id"],
346+
timeColumn: "_lastModified",
347+
},
343348
];
344349

345350
public getSortedTables(): TableDescription[] {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Copyright (c) 2023 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 { Entity, PrimaryColumn } from "typeorm";
8+
import { TypeORM } from "../typeorm";
9+
10+
@Entity()
11+
// on DB but not Typeorm: @Index("ind_lastModified", ["_lastModified"]) // DBSync
12+
export class DBLinkedInToken {
13+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
14+
id: string;
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2023 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 { inject, injectable } from "inversify";
8+
import { Repository } from "typeorm";
9+
import { LinkedInTokenDB } from "../linked-in-token-db";
10+
import { DBLinkedInToken } from "./entity/db-linked-in-token";
11+
import { TypeORM } from "./typeorm";
12+
13+
@injectable()
14+
export class LinkedInTokenDBImpl implements LinkedInTokenDB {
15+
@inject(TypeORM) typeORM: TypeORM;
16+
17+
protected async getEntityManager() {
18+
return (await this.typeORM.getConnection()).manager;
19+
}
20+
21+
protected async getRepo(): Promise<Repository<DBLinkedInToken>> {
22+
return (await this.getEntityManager()).getRepository<DBLinkedInToken>(DBLinkedInToken);
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2023 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 { tableExists } from "./helper/helper";
9+
10+
const TABLE_NAME = "d_b_linked_in_token";
11+
12+
export class LinkedInToken1680096507296 implements MigrationInterface {
13+
public async up(queryRunner: QueryRunner): Promise<void> {
14+
await queryRunner.query(
15+
`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (id char(36) NOT NULL, _lastModified timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`,
16+
);
17+
}
18+
19+
public async down(queryRunner: QueryRunner): Promise<void> {
20+
if (await tableExists(queryRunner, TABLE_NAME)) {
21+
await queryRunner.query(`DROP TABLE ${TABLE_NAME}`);
22+
}
23+
}
24+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
308308
getBillingModeForUser(): Promise<BillingMode>;
309309
getBillingModeForTeam(teamId: string): Promise<BillingMode>;
310310

311+
getLinkedInClientId(): Promise<string>;
312+
311313
/**
312314
* Analytics
313315
*/

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2675,6 +2675,19 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
26752675
return this.billingModes.getBillingModeForTeam(team, new Date());
26762676
}
26772677

2678+
async getLinkedInClientId(ctx: TraceContextWithSpan): Promise<string> {
2679+
traceAPIParams(ctx, {});
2680+
this.checkAndBlockUser("getLinkedInClientID");
2681+
const clientId = this.config.linkedInSecrets?.clientId || "86lcfbj2vwm2ba"; // FIXME(janx)
2682+
if (!clientId) {
2683+
throw new ResponseError(
2684+
ErrorCodes.INTERNAL_SERVER_ERROR,
2685+
"LinkedIn is not properly configured (no Client ID)",
2686+
);
2687+
}
2688+
return clientId;
2689+
}
2690+
26782691
// (SaaS) – admin
26792692
async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise<AccountStatement> {
26802693
traceAPIParams(ctx, { userId });

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ const defaultFunctions: FunctionsConfig = {
224224
listUsage: { group: "default", points: 1 },
225225
getBillingModeForTeam: { group: "default", points: 1 },
226226
getBillingModeForUser: { group: "default", points: 1 },
227+
getLinkedInClientId: { group: "default", points: 1 },
227228
tsAddMembersToOrg: { group: "default", points: 1 },
228229

229230
trackEvent: { group: "default", points: 1 },

components/server/src/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ export type Config = Omit<
2626
| "chargebeeProviderOptionsFile"
2727
| "stripeSecretsFile"
2828
| "stripeConfigFile"
29+
| "linkedInSecretsFile"
2930
| "licenseFile"
3031
| "patSigningKeyFile"
3132
> & {
3233
hostUrl: GitpodHostUrl;
3334
workspaceDefaults: WorkspaceDefaults;
3435
chargebeeProviderOptions?: ChargebeeProviderOptions;
3536
stripeSecrets?: { publishableKey: string; secretKey: string };
37+
linkedInSecrets?: { clientId: string; clientSecret: string };
3638
builtinAuthProvidersConfigured: boolean;
3739
inactivityPeriodForReposInDays?: number;
3840

@@ -206,6 +208,11 @@ export interface ConfigSerialized {
206208
stripeConfigFile?: string;
207209
enablePayment?: boolean;
208210

211+
/**
212+
* LinkedIn OAuth2 configuration
213+
*/
214+
linkedInSecretsFile?: string;
215+
209216
/**
210217
* Number of prebuilds that can be started in a given time period.
211218
* Key '*' specifies the default rate limit for a cloneURL, unless overriden by a specific cloneURL.
@@ -291,6 +298,16 @@ export namespace ConfigFile {
291298
log.error("Could not load Stripe secrets", error);
292299
}
293300
}
301+
let linkedInSecrets: { clientId: string; clientSecret: string } | undefined;
302+
if (config.linkedInSecretsFile) {
303+
try {
304+
linkedInSecrets = JSON.parse(
305+
fs.readFileSync(filePathTelepresenceAware(config.linkedInSecretsFile), "utf-8"),
306+
);
307+
} catch (error) {
308+
log.error("Could not load LinkedIn secrets", error);
309+
}
310+
}
294311
let license = config.license;
295312
const licenseFile = config.licenseFile;
296313
if (licenseFile) {
@@ -336,6 +353,7 @@ export namespace ConfigFile {
336353
builtinAuthProvidersConfigured,
337354
chargebeeProviderOptions,
338355
stripeSecrets,
356+
linkedInSecrets,
339357
twilioConfig,
340358
license,
341359
workspaceGarbageCollection: {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3576,6 +3576,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
35763576
return BillingMode.NONE;
35773577
}
35783578

3579+
async getLinkedInClientId(ctx: TraceContextWithSpan): Promise<string> {
3580+
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
3581+
}
3582+
35793583
//
35803584
//#endregion
35813585

install/installer/pkg/components/server/configmap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
282282
EnablePayment: chargebeeSecret != "" || stripeSecret != "" || stripeConfig != "",
283283
ChargebeeProviderOptionsFile: fmt.Sprintf("%s/providerOptions", chargebeeMountPath),
284284
StripeSecretsFile: fmt.Sprintf("%s/apikeys", stripeSecretMountPath),
285+
LinkedInSecretsFile: fmt.Sprintf("%s/linkedin", linkedInSecretMountPath),
285286
InsecureNoDomain: false,
286287
PrebuildLimiter: PrebuildRateLimiters{
287288
// default limit for all cloneURLs

install/installer/pkg/components/server/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
licenseFilePath = "/gitpod/license"
1717
chargebeeMountPath = "/chargebee"
1818
stripeSecretMountPath = "/stripe-secret"
19+
linkedInSecretMountPath = "/linkedin-secret"
1920
githubAppCertSecret = "github-app-cert-secret"
2021
IAMSessionPort = common.ServerIAMSessionPort
2122
IAMSessionPortName = "session"

install/installer/pkg/components/server/deployment.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,29 @@ func deployment(ctx *common.RenderContext) ([]runtime.Object, error) {
205205
return nil
206206
})
207207

208+
_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
209+
if cfg.WebApp != nil && cfg.WebApp.Server != nil && cfg.WebApp.Server.LinkedInSecret != "" {
210+
linkedInSecret := cfg.WebApp.Server.LinkedInSecret
211+
212+
volumes = append(volumes,
213+
corev1.Volume{
214+
Name: "linkedin-secret",
215+
VolumeSource: corev1.VolumeSource{
216+
Secret: &corev1.SecretVolumeSource{
217+
SecretName: linkedInSecret,
218+
},
219+
},
220+
})
221+
222+
volumeMounts = append(volumeMounts, corev1.VolumeMount{
223+
Name: "linkedin-secret",
224+
MountPath: linkedInSecretMountPath,
225+
ReadOnly: true,
226+
})
227+
}
228+
return nil
229+
})
230+
208231
_ = ctx.WithExperimental(func(cfg *experimental.Config) error {
209232
if cfg.WebApp != nil && cfg.WebApp.Server != nil && cfg.WebApp.Server.GithubApp != nil {
210233
volumes = append(volumes,

install/installer/pkg/components/server/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ConfigSerialized struct {
3636
StripeSecretsFile string `json:"stripeSecretsFile"`
3737
StripeConfigFile string `json:"stripeConfigFile"`
3838
EnablePayment bool `json:"enablePayment"`
39+
LinkedInSecretsFile string `json:"linkedInSecretsFile"`
3940
PATSigningKeyFile string `json:"patSigningKeyFile"`
4041
ShowSetupModal bool `json:"showSetupModal"`
4142

install/installer/pkg/config/v1/experimental/experimental.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ type ServerConfig struct {
259259
ChargebeeSecret string `json:"chargebeeSecret"`
260260
StripeSecret string `json:"stripeSecret"`
261261
StripeConfig string `json:"stripeConfig"`
262+
LinkedInSecret string `json:"linkedInSecret"`
262263
DisableDynamicAuthProviderLogin bool `json:"disableDynamicAuthProviderLogin"`
263264
EnableLocalApp *bool `json:"enableLocalApp"`
264265
RunDbDeleter *bool `json:"runDbDeleter"`

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15162,6 +15162,11 @@ react-is@^17.0.1:
1516215162
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
1516315163
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
1516415164

15165+
react-linkedin-login-oauth2@^2.0.1:
15166+
version "2.0.1"
15167+
resolved "https://registry.yarnpkg.com/react-linkedin-login-oauth2/-/react-linkedin-login-oauth2-2.0.1.tgz#e322245b69f3248bf68273aa0a0deeed2ddf859d"
15168+
integrity sha512-vlmChbRhLjWZVU/A96I09GcpH3xLFTawQbT/05Dl3kuLVupdaVo7YZf2qrDQmrdfDm4Qj8svX1gkwHkybAMKDg==
15169+
1516515170
react-onclickoutside@^6.12.0:
1516615171
version "6.12.2"
1516715172
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz#8e6cf80c7d17a79f2c908399918158a7b02dda01"

0 commit comments

Comments
 (0)