Skip to content

Commit 75a8fb3

Browse files
authored
[server] update OIDC users on sign-in (email) – WEB-370 (#17633)
* [server] update OIDC users on sign-in (email) * adding simple tests for update of email attribute
1 parent 8ae8acf commit 75a8fb3

File tree

3 files changed

+82
-6
lines changed

3 files changed

+82
-6
lines changed

components/server/src/iam/iam-session-app.spec.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import * as request from "supertest";
2020
import * as chai from "chai";
2121
import { OIDCCreateSessionPayload } from "./iam-oidc-create-session-payload";
2222
import { BUILTIN_INSTLLATION_ADMIN_USER_ID, TeamDB } from "@gitpod/gitpod-db/lib";
23-
import { TeamMemberInfo } from "@gitpod/gitpod-protocol";
23+
import { TeamMemberInfo, User } from "@gitpod/gitpod-protocol";
2424
const expect = chai.expect;
2525

2626
@suite(timeout(10000))
@@ -33,20 +33,25 @@ class TestIamSessionApp {
3333
protected knownSubjectID = "111";
3434
protected knownEmail = "[email protected]";
3535

36+
protected knownUser: Partial<User> = {
37+
id: "id-known-user",
38+
identities: [],
39+
};
40+
3641
protected userServiceMock: Partial<UserService> = {
3742
createUser: (params) => {
3843
return { id: "id-new-user" } as any;
3944
},
4045

4146
findUserForLogin: async (params) => {
4247
if (params.candidate?.authId === this.knownSubjectID) {
43-
return { id: "id-known-user" } as any;
48+
return this.knownUser as any;
4449
}
4550
return undefined;
4651
},
4752
findOrgOwnedUser: async (params) => {
4853
if (params.email === this.knownEmail) {
49-
return { id: "id-known-user" } as any;
54+
return this.knownUser as any;
5055
}
5156
return undefined;
5257
},
@@ -88,6 +93,7 @@ class TestIamSessionApp {
8893

8994
public before() {
9095
this.teamDbMock.memberships.clear();
96+
this.knownUser.identities = [];
9197

9298
const container = new Container();
9399
container.load(
@@ -231,6 +237,58 @@ class TestIamSessionApp {
231237
expect(this.teamDbMock.memberships.has(BUILTIN_INSTLLATION_ADMIN_USER_ID)).to.be.false;
232238
expect(this.teamDbMock.memberships.has("id-new-user")).to.be.true;
233239
}
240+
241+
@test public async testSessionRequest_updates_existing_user() {
242+
const payload: OIDCCreateSessionPayload = { ...this.payload };
243+
payload.claims.sub = this.knownSubjectID; // `userServiceMock.findUserForLogin` will match this value
244+
245+
this.knownUser.identities = [
246+
{
247+
authId: payload.claims.sub,
248+
authProviderId: payload.claims.aud,
249+
authName: "Test User",
250+
primaryEmail: "[email protected]",
251+
},
252+
];
253+
254+
let newEmail: string | undefined;
255+
this.userServiceMock.updateUserIdentity = async (user, updatedIdentity) => {
256+
newEmail = updatedIdentity.primaryEmail;
257+
};
258+
259+
const result = await request(this.app.create())
260+
.post("/session")
261+
.set("Content-Type", "application/json")
262+
.send(JSON.stringify(payload));
263+
264+
expect(result.statusCode, JSON.stringify(result.body)).to.equal(200);
265+
expect(newEmail, "update was not called").not.to.be.undefined;
266+
expect(newEmail).to.equal(payload.claims.email);
267+
}
268+
269+
@test public async testSessionRequest_no_update_if_same_email() {
270+
this.knownUser.identities = [
271+
{
272+
authId: this.payload.claims.sub,
273+
authProviderId: this.payload.claims.aud,
274+
authName: "Test User",
275+
primaryEmail: this.payload.claims.email,
276+
},
277+
];
278+
279+
let updateUserIdentityCalled = false;
280+
this.userServiceMock.updateUserIdentity = async () => {
281+
updateUserIdentityCalled = true;
282+
};
283+
284+
const result = await request(this.app.create())
285+
.post("/session")
286+
.set("Content-Type", "application/json")
287+
.send(JSON.stringify(this.payload));
288+
289+
expect(result.statusCode, JSON.stringify(result.body)).to.equal(200);
290+
expect(updateUserIdentityCalled).to.be.false;
291+
}
234292
}
235293

236294
module.exports = new TestIamSessionApp();

components/server/src/iam/iam-session-app.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export class IamSessionApp {
5959
const payload = OIDCCreateSessionPayload.validate(req.body);
6060

6161
const existingUser = await this.findExistingOIDCUser(payload);
62+
if (existingUser) {
63+
await this.updateOIDCUserOnSign(existingUser, payload);
64+
}
65+
6266
const user = existingUser || (await this.createNewOIDCUser(payload));
6367

6468
await new Promise<void>((resolve, reject) => {
@@ -138,6 +142,20 @@ export class IamSessionApp {
138142
return existingUser;
139143
}
140144

145+
/**
146+
* Updates `User.identities[current IdP].primaryEmail`
147+
*/
148+
protected async updateOIDCUserOnSign(user: User, payload: OIDCCreateSessionPayload) {
149+
const recent = this.mapOIDCProfileToIdentity(payload);
150+
const existing = user.identities.find((identity) => identity.authId === recent.authId);
151+
152+
// Update email
153+
if (existing && !!recent.primaryEmail && existing.primaryEmail !== recent.primaryEmail) {
154+
existing.primaryEmail = recent.primaryEmail;
155+
await this.userService.updateUserIdentity(user, existing);
156+
}
157+
}
158+
141159
protected async createNewOIDCUser(payload: OIDCCreateSessionPayload): Promise<User> {
142160
const { claims, organizationId } = payload;
143161

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,20 @@ export class UserService {
302302
user.name = user.name || authUser.name || authUser.primaryEmail;
303303
user.avatarUrl = user.avatarUrl || authUser.avatarUrl;
304304
await this.onAfterUserLoad(user);
305-
await this.updateUserIdentity(user, candidate, token);
305+
await this.updateUserIdentity(user, candidate);
306+
await this.userDb.storeSingleToken(candidate, token);
306307
}
307308

308309
async onAfterUserLoad(user: User): Promise<User> {
309310
return user;
310311
}
311312

312-
async updateUserIdentity(user: User, candidate: Identity, token: Token) {
313+
async updateUserIdentity(user: User, candidate: Identity) {
313314
// ensure single identity per auth provider instance
314315
user.identities = user.identities.filter((i) => i.authProviderId !== candidate.authProviderId);
315316
user.identities.push(candidate);
316317

317318
await this.userDb.storeUser(user);
318-
await this.userDb.storeSingleToken(candidate, token);
319319
}
320320

321321
async deauthorize(user: User, authProviderId: string) {

0 commit comments

Comments
 (0)