Skip to content

Commit a11b3cd

Browse files
authored
feat(gitlab): implement TokenIdentity method (#4606)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent 3ab0947 commit a11b3cd

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed

connector/gitlab/gitlab.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ type connectorData struct {
8787
}
8888

8989
var (
90-
_ connector.CallbackConnector = (*gitlabConnector)(nil)
91-
_ connector.RefreshConnector = (*gitlabConnector)(nil)
90+
_ connector.CallbackConnector = (*gitlabConnector)(nil)
91+
_ connector.RefreshConnector = (*gitlabConnector)(nil)
92+
_ connector.TokenIdentityConnector = (*gitlabConnector)(nil)
9293
)
9394

9495
type gitlabConnector struct {
@@ -243,6 +244,34 @@ func (c *gitlabConnector) Refresh(ctx context.Context, s connector.Scopes, ident
243244
}
244245
}
245246

247+
// TokenIdentity is used for token exchange, verifying a GitLab access token
248+
// and returning the associated user identity. This enables direct authentication
249+
// with Dex using an existing GitLab token without going through the OAuth flow.
250+
//
251+
// Note: The connector decides whether to fetch groups based on its configuration
252+
// (groups filter, getGroupsPermission), not on the scopes from the token exchange request.
253+
// The server will then decide whether to include groups in the final token based on
254+
// the requested scopes. This matches the behavior of other connectors (e.g., OIDC).
255+
func (c *gitlabConnector) TokenIdentity(ctx context.Context, _, subjectToken string) (connector.Identity, error) {
256+
if c.httpClient != nil {
257+
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
258+
}
259+
260+
token := &oauth2.Token{
261+
AccessToken: subjectToken,
262+
TokenType: "Bearer", // GitLab tokens are typically Bearer tokens even if the type is not explicitly provided.
263+
}
264+
265+
// For token exchange, we determine if groups should be fetched based on connector configuration.
266+
// If the connector has groups filter or getGroupsPermission enabled, we fetch groups.
267+
scopes := connector.Scopes{
268+
// Scopes are not provided in token exchange, so we request groups every time and return only if configured.
269+
Groups: true,
270+
}
271+
272+
return c.identity(ctx, scopes, token)
273+
}
274+
246275
func (c *gitlabConnector) groupsRequired(groupScope bool) bool {
247276
return len(c.groups) > 0 || groupScope
248277
}

connector/gitlab/gitlab_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,88 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) {
485485
t.Errorf("Expected %+v to equal %+v", a, b)
486486
}
487487
}
488+
489+
func TestTokenIdentity(t *testing.T) {
490+
// Note: These tests verify that the connector returns groups based on its configuration.
491+
// The actual inclusion of groups in the final Dex token depends on the 'groups' scope
492+
// in the token exchange request, which is handled by the Dex server, not the connector.
493+
tests := []struct {
494+
name string
495+
userInfo userInfo
496+
groups []string
497+
getGroupsPermission bool
498+
useLoginAsID bool
499+
expectUserID string
500+
expectGroups []string
501+
}{
502+
{
503+
name: "without groups config",
504+
expectUserID: "12345678",
505+
expectGroups: nil,
506+
},
507+
{
508+
name: "with groups filter",
509+
userInfo: userInfo{
510+
Groups: []string{"team-1", "team-2"},
511+
},
512+
groups: []string{"team-1"},
513+
expectUserID: "12345678",
514+
expectGroups: []string{"team-1"},
515+
},
516+
{
517+
name: "with groups permission",
518+
userInfo: userInfo{
519+
Groups: []string{"ops", "dev"},
520+
OwnerPermission: []string{"ops"},
521+
DeveloperPermission: []string{"dev"},
522+
MaintainerPermission: []string{},
523+
},
524+
getGroupsPermission: true,
525+
expectUserID: "12345678",
526+
expectGroups: []string{"ops", "dev", "ops:owner", "dev:developer"},
527+
},
528+
{
529+
name: "with useLoginAsID",
530+
useLoginAsID: true,
531+
expectUserID: "joebloggs",
532+
expectGroups: nil,
533+
},
534+
}
535+
536+
for _, tc := range tests {
537+
t.Run(tc.name, func(t *testing.T) {
538+
responses := map[string]interface{}{
539+
"/api/v4/user": gitlabUser{
540+
Email: "some@email.com",
541+
ID: 12345678,
542+
Name: "Joe Bloggs",
543+
Username: "joebloggs",
544+
},
545+
"/oauth/userinfo": tc.userInfo,
546+
}
547+
548+
s := newTestServer(responses)
549+
defer s.Close()
550+
551+
c := gitlabConnector{
552+
baseURL: s.URL,
553+
httpClient: newClient(),
554+
groups: tc.groups,
555+
getGroupsPermission: tc.getGroupsPermission,
556+
useLoginAsID: tc.useLoginAsID,
557+
}
558+
559+
accessToken := "test-access-token"
560+
ctx := context.Background()
561+
identity, err := c.TokenIdentity(ctx, "urn:ietf:params:oauth:token-type:access_token", accessToken)
562+
563+
expectNil(t, err)
564+
expectEquals(t, identity.UserID, tc.expectUserID)
565+
expectEquals(t, identity.Username, "Joe Bloggs")
566+
expectEquals(t, identity.PreferredUsername, "joebloggs")
567+
expectEquals(t, identity.Email, "some@email.com")
568+
expectEquals(t, identity.EmailVerified, true)
569+
expectEquals(t, identity.Groups, tc.expectGroups)
570+
})
571+
}
572+
}

0 commit comments

Comments
 (0)