Skip to content

Commit feb6058

Browse files
authored
[idp] Guarantee stable email address for IDP token for organization-bound users (#20199)
1 parent 790a670 commit feb6058

File tree

6 files changed

+140
-18
lines changed

6 files changed

+140
-18
lines changed

components/gitpod-protocol/go/gitpod-service.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2052,10 +2052,11 @@ type Identity struct {
20522052
AuthProviderID string `json:"authProviderId,omitempty"`
20532053

20542054
// This is a flag that triggers the HARD DELETION of this entity
2055-
Deleted bool `json:"deleted,omitempty"`
2056-
PrimaryEmail string `json:"primaryEmail,omitempty"`
2057-
Readonly bool `json:"readonly,omitempty"`
2058-
Tokens []*Token `json:"tokens,omitempty"`
2055+
Deleted bool `json:"deleted,omitempty"`
2056+
PrimaryEmail string `json:"primaryEmail,omitempty"`
2057+
Readonly bool `json:"readonly,omitempty"`
2058+
Tokens []*Token `json:"tokens,omitempty"`
2059+
LastSigninTime string `json:"lastSigninTime,omitempty"`
20592060
}
20602061

20612062
// User is the User message type

components/gitpod-protocol/go/user.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2024 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package protocol
6+
7+
import (
8+
"cmp"
9+
"slices"
10+
)
11+
12+
// GetSSOEmail returns the email of the user's last-used SSO identity, if any. It mirros the funcationality we have implemeted in TS here: https://github.com/gitpod-io/gitpod/blob/e4ccbf0b4d224714ffd16719a3b5c50630d6edbc/components/public-api/typescript-common/src/user-utils.ts#L24-L35
13+
func (u *User) GetSSOEmail() string {
14+
var ssoIdentities []*Identity
15+
for _, id := range u.Identities {
16+
// LastSigninTime is empty for non-SSO identities, and used as a filter here.
17+
if id == nil || id.Deleted || id.LastSigninTime == "" {
18+
continue
19+
}
20+
ssoIdentities = append(ssoIdentities, id)
21+
}
22+
if len(ssoIdentities) == 0 {
23+
return ""
24+
}
25+
26+
// We are looking for the latest-used SSO identity.
27+
slices.SortFunc(ssoIdentities, func(i, j *Identity) int {
28+
return cmp.Compare(j.LastSigninTime, i.LastSigninTime)
29+
})
30+
return ssoIdentities[0].PrimaryEmail
31+
}
32+
33+
// GetRandomEmail returns an email address of any of the user's identities.
34+
func (u *User) GetRandomEmail() string {
35+
for _, id := range u.Identities {
36+
if id == nil || id.Deleted || id.PrimaryEmail == "" {
37+
continue
38+
}
39+
return id.PrimaryEmail
40+
}
41+
return ""
42+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2024 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package protocol
6+
7+
import "testing"
8+
9+
func TestGetSSOEmail(t *testing.T) {
10+
u := &User{
11+
Identities: []*Identity{
12+
{
13+
PrimaryEmail: "[email protected]",
14+
LastSigninTime: "2022-01-01T00:00:00Z",
15+
},
16+
{
17+
PrimaryEmail: "[email protected]",
18+
LastSigninTime: "2022-03-01T00:00:00Z",
19+
},
20+
{
21+
PrimaryEmail: "[email protected]",
22+
LastSigninTime: "2022-02-01T00:00:00Z",
23+
},
24+
{
25+
PrimaryEmail: "[email protected]",
26+
LastSigninTime: "",
27+
},
28+
},
29+
}
30+
31+
expectedEmail := "[email protected]"
32+
actualEmail := u.GetSSOEmail()
33+
34+
if actualEmail != expectedEmail {
35+
t.Errorf("Expected SSO email to be %s, but got %s", expectedEmail, actualEmail)
36+
}
37+
}
38+
func TestGetRandomEmail(t *testing.T) {
39+
u := &User{
40+
Identities: []*Identity{
41+
{
42+
PrimaryEmail: "",
43+
LastSigninTime: "",
44+
},
45+
{
46+
PrimaryEmail: "[email protected]",
47+
LastSigninTime: "",
48+
Deleted: true,
49+
},
50+
{
51+
PrimaryEmail: "[email protected]",
52+
LastSigninTime: "",
53+
},
54+
{
55+
PrimaryEmail: "[email protected]",
56+
LastSigninTime: "2022-03-01T00:00:00Z",
57+
},
58+
{
59+
PrimaryEmail: "[email protected]",
60+
LastSigninTime: "2022-02-01T00:00:00Z",
61+
},
62+
{
63+
PrimaryEmail: "[email protected]",
64+
LastSigninTime: "",
65+
},
66+
},
67+
}
68+
69+
expectedEmail := "[email protected]"
70+
actualEmail := u.GetRandomEmail()
71+
72+
if actualEmail != expectedEmail {
73+
t.Errorf("Expected random email to be %s, but got %s", expectedEmail, actualEmail)
74+
}
75+
}

components/gitpod-protocol/src/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ export interface Identity {
564564
primaryEmail?: string;
565565
/** This is a flag that triggers the HARD DELETION of this entity */
566566
deleted?: boolean;
567-
// The last time this entry was touched during a signin.
567+
// The last time this entry was touched during a signin. It's only set for SSO identities.
568568
lastSigninTime?: string;
569569

570570
// @deprecated as no longer in use since '19

components/public-api-server/pkg/apiv1/identityprovider.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,6 @@ func (srv *IdentityProviderService) GetIDToken(ctx context.Context, req *connect
7575
return nil, proxy.ConvertError(err)
7676
}
7777

78-
var email string
79-
for _, id := range user.Identities {
80-
if id == nil || id.Deleted || id.PrimaryEmail == "" {
81-
continue
82-
}
83-
email = id.PrimaryEmail
84-
break
85-
}
86-
8778
if workspace.Workspace == nil {
8879
log.Extract(ctx).WithError(err).Error("Server did not return a workspace.")
8980
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("workspace not found"))
@@ -103,8 +94,21 @@ func (srv *IdentityProviderService) GetIDToken(ctx context.Context, req *connect
10394
if workspace.Workspace.Context != nil && workspace.Workspace.Context.Repository != nil && workspace.Workspace.Context.Repository.CloneURL != "" {
10495
userInfo.AppendClaims("repository", workspace.Workspace.Context.Repository.CloneURL)
10596
}
97+
98+
var email string
99+
var emailVerified bool
100+
if user.OrganizationId != "" {
101+
emailVerified = true
102+
email = user.GetSSOEmail()
103+
if email == "" {
104+
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SSO email is empty"))
105+
}
106+
} else {
107+
emailVerified = false
108+
email = user.GetRandomEmail()
109+
}
106110
if email != "" {
107-
userInfo.SetEmail(email, user.OrganizationId != "")
111+
userInfo.SetEmail(email, emailVerified)
108112
userInfo.AppendClaims("email", email)
109113
}
110114

components/public-api-server/pkg/apiv1/identityprovider_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestGetIDToken(t *testing.T) {
7676
Identities: []*protocol.Identity{
7777
nil,
7878
{Deleted: true, PrimaryEmail: "[email protected]"},
79-
{Deleted: false, PrimaryEmail: "[email protected]"},
79+
{Deleted: false, PrimaryEmail: "[email protected]", LastSigninTime: "2021-01-01T00:00:00Z"},
8080
},
8181
OrganizationId: "test",
8282
},
@@ -125,7 +125,7 @@ func TestGetIDToken(t *testing.T) {
125125
Identities: []*protocol.Identity{
126126
nil,
127127
{Deleted: true, PrimaryEmail: "[email protected]"},
128-
{Deleted: false, PrimaryEmail: "[email protected]"},
128+
{Deleted: false, PrimaryEmail: "[email protected]", LastSigninTime: "2021-01-01T00:00:00Z"},
129129
},
130130
},
131131
nil,
@@ -243,7 +243,7 @@ func TestGetIDToken(t *testing.T) {
243243
Identities: []*protocol.Identity{
244244
nil,
245245
{Deleted: true, PrimaryEmail: "[email protected]"},
246-
{Deleted: false, PrimaryEmail: "[email protected]"},
246+
{Deleted: false, PrimaryEmail: "[email protected]", LastSigninTime: "2021-01-01T00:00:00Z"},
247247
},
248248
OrganizationId: "test",
249249
},

0 commit comments

Comments
 (0)