Skip to content

Commit 712df54

Browse files
Merge pull request #19997 from enj/enj/f/gitlab_oidc
Update GitLab IDP to support OIDC
2 parents 95aa365 + 58757d9 commit 712df54

File tree

9 files changed

+177
-15
lines changed

9 files changed

+177
-15
lines changed

pkg/cmd/server/apis/config/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,14 @@ type GitLabIdentityProvider struct {
10701070
ClientID string
10711071
// ClientSecret is the oauth client secret
10721072
ClientSecret StringSource
1073+
// Legacy determines if OAuth2 or OIDC should be used
1074+
// If true, OAuth2 is used
1075+
// If false, OIDC is used
1076+
// If nil and the URL's host is gitlab.com, OIDC is used
1077+
// Otherwise, OAuth2 is used
1078+
// In a future release, nil will default to using OIDC
1079+
// Eventually this flag will be removed and only OIDC will be used
1080+
Legacy *bool
10731081
}
10741082

10751083
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

pkg/cmd/server/apis/config/v1/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,14 @@ type GitLabIdentityProvider struct {
10061006
ClientID string `json:"clientID"`
10071007
// ClientSecret is the oauth client secret
10081008
ClientSecret StringSource `json:"clientSecret"`
1009+
// Legacy determines if OAuth2 or OIDC should be used
1010+
// If true, OAuth2 is used
1011+
// If false, OIDC is used
1012+
// If nil and the URL's host is gitlab.com, OIDC is used
1013+
// Otherwise, OAuth2 is used
1014+
// In a future release, nil will default to using OIDC
1015+
// Eventually this flag will be removed and only OIDC will be used
1016+
Legacy *bool `json:"legacy,omitempty"`
10091017
}
10101018

10111019
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

pkg/cmd/server/apis/config/v1/zz_generated.deepcopy.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cmd/server/apis/config/zz_generated.deepcopy.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package gitlab
2+
3+
import (
4+
"net/http"
5+
"net/url"
6+
"strings"
7+
8+
"github.com/openshift/origin/pkg/oauthserver/oauth/external"
9+
10+
"github.com/golang/glog"
11+
)
12+
13+
// The hosted version of GitLab is guaranteed to be using the latest stable version
14+
// meaning that we can count on it having OIDC support (and no sub claim bug)
15+
const gitlabHostedDomain = "gitlab.com"
16+
17+
func NewProvider(providerName, URL, clientID, clientSecret string, transport http.RoundTripper, legacy *bool) (external.Provider, error) {
18+
if isLegacy(legacy, URL) {
19+
glog.Infof("Using legacy OAuth2 for GitLab identity provider %s url=%s clientID=%s", providerName, URL, clientID)
20+
return NewOAuthProvider(providerName, URL, clientID, clientSecret, transport)
21+
}
22+
glog.Infof("Using OIDC for GitLab identity provider %s url=%s clientID=%s", providerName, URL, clientID)
23+
return NewOIDCProvider(providerName, URL, clientID, clientSecret, transport)
24+
}
25+
26+
func isLegacy(legacy *bool, URL string) bool {
27+
// if a value is specified, honor it
28+
if legacy != nil {
29+
return *legacy
30+
}
31+
32+
// use OIDC if we know it will work since the hosted version is being used
33+
// validation handles URL parsing errors so we can ignore them here
34+
if u, err := url.Parse(URL); err == nil && strings.EqualFold(u.Hostname(), gitlabHostedDomain) {
35+
return false
36+
}
37+
38+
// otherwise use OAuth2 (to be safe for now)
39+
return true
40+
}

pkg/oauthserver/oauth/external/gitlab/gitlab.go renamed to pkg/oauthserver/oauth/external/gitlab/gitlab_oauth.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"io/ioutil"
88
"net/http"
99
"net/url"
10-
"path"
1110

1211
"github.com/RangelReale/osincli"
1312
"github.com/golang/glog"
@@ -21,10 +20,8 @@ const (
2120
// and OAuth-Provider (http://doc.gitlab.com/ce/integration/oauth_provider.html)
2221
// with default OAuth scope (http://doc.gitlab.com/ce/api/users.html#current-user)
2322
// Requires GitLab 7.7.0 or higher
24-
gitlabAuthorizePath = "/oauth/authorize"
25-
gitlabTokenPath = "/oauth/token"
26-
gitlabUserAPIPath = "/api/v3/user"
27-
gitlabOAuthScope = "api"
23+
gitlabUserAPIPath = "/api/v3/user"
24+
gitlabOAuthScope = "api"
2825
)
2926

3027
type provider struct {
@@ -44,7 +41,7 @@ type gitlabUser struct {
4441
Name string
4542
}
4643

47-
func NewProvider(providerName string, transport http.RoundTripper, URL, clientID, clientSecret string) (external.Provider, error) {
44+
func NewOAuthProvider(providerName, URL, clientID, clientSecret string, transport http.RoundTripper) (external.Provider, error) {
4845
// Create service URLs
4946
u, err := url.Parse(URL)
5047
if err != nil {
@@ -62,11 +59,6 @@ func NewProvider(providerName string, transport http.RoundTripper, URL, clientID
6259
}, nil
6360
}
6461

65-
func appendPath(u url.URL, subpath string) string {
66-
u.Path = path.Join(u.Path, subpath)
67-
return u.String()
68-
}
69-
7062
func (p *provider) GetTransport() (http.RoundTripper, error) {
7163
return p.transport, nil
7264
}
@@ -86,8 +78,7 @@ func (p *provider) NewConfig() (*osincli.ClientConfig, error) {
8678
}
8779

8880
// AddCustomParameters implements external/interfaces/Provider.AddCustomParameters
89-
func (p *provider) AddCustomParameters(req *osincli.AuthorizeRequest) {
90-
}
81+
func (p *provider) AddCustomParameters(req *osincli.AuthorizeRequest) {}
9182

9283
// GetUserIdentity implements external/interfaces/Provider.GetUserIdentity
9384
func (p *provider) GetUserIdentity(data *osincli.AccessData) (authapi.UserIdentityInfo, bool, error) {

pkg/oauthserver/oauth/external/gitlab/gitlab_test.go renamed to pkg/oauthserver/oauth/external/gitlab/gitlab_oauth_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func TestGitLab(t *testing.T) {
11-
p, err := NewProvider("gitlab", nil, "https://gitlab.com/", "clientid", "clientsecret")
11+
p, err := NewOAuthProvider("gitlab", "https://gitlab.com/", "clientid", "clientsecret", nil)
1212
if err != nil {
1313
t.Fatalf("Unexpected error: %v", err)
1414
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package gitlab
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
"path"
8+
"regexp"
9+
"strconv"
10+
11+
"github.com/openshift/origin/pkg/oauthserver/oauth/external"
12+
"github.com/openshift/origin/pkg/oauthserver/oauth/external/openid"
13+
)
14+
15+
const (
16+
// https://gitlab.com/help/integration/openid_connect_provider.md
17+
// Uses GitLab OIDC, requires GitLab 11.1.0 or higher
18+
// Earlier versions do not work: https://gitlab.com/gitlab-org/gitlab-ce/issues/47791#note_81269161
19+
gitlabAuthorizePath = "/oauth/authorize"
20+
gitlabTokenPath = "/oauth/token"
21+
gitlabUserInfoPath = "/oauth/userinfo"
22+
23+
// https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/doorkeeper.en.yml
24+
// Authenticate using OpenID Connect
25+
// The ability to authenticate using GitLab, and read-only access to the user's profile information and group memberships
26+
gitlabOIDCScope = "openid"
27+
28+
// The ID of the user
29+
// See above comment about GitLab 11.1.0 and the custom IDTokenValidator below
30+
// Along with providerName, builds the identity object's Name field (see Identity.ProviderUserName)
31+
gitlabIDClaim = "sub"
32+
// The user's GitLab username
33+
// Used as the Name field of the user object (stored in Identity.Extra, see IdentityPreferredUsernameKey)
34+
gitlabPreferredUsernameClaim = "nickname"
35+
// The user's public email address
36+
// The value can optionally be used during manual provisioning (stored in Identity.Extra, see IdentityEmailKey)
37+
gitlabEmailClaim = "email"
38+
// The user's full name
39+
// Used as the FullName field of the user object (stored in Identity.Extra, see IdentityDisplayNameKey)
40+
gitlabDisplayNameClaim = "name"
41+
)
42+
43+
func NewOIDCProvider(providerName, URL, clientID, clientSecret string, transport http.RoundTripper) (external.Provider, error) {
44+
// Create service URLs
45+
u, err := url.Parse(URL)
46+
if err != nil {
47+
return nil, fmt.Errorf("gitlab host URL %q is invalid", URL)
48+
}
49+
50+
config := openid.Config{
51+
ClientID: clientID,
52+
ClientSecret: clientSecret,
53+
54+
AuthorizeURL: appendPath(*u, gitlabAuthorizePath),
55+
TokenURL: appendPath(*u, gitlabTokenPath),
56+
UserInfoURL: appendPath(*u, gitlabUserInfoPath),
57+
58+
Scopes: []string{gitlabOIDCScope},
59+
60+
IDClaims: []string{gitlabIDClaim},
61+
PreferredUsernameClaims: []string{gitlabPreferredUsernameClaim},
62+
EmailClaims: []string{gitlabEmailClaim},
63+
NameClaims: []string{gitlabDisplayNameClaim},
64+
65+
// make sure that gitlabIDClaim is a valid uint64, see above comment about GitLab 11.1.0
66+
IDTokenValidator: func(idTokenClaims map[string]interface{}) error {
67+
gitlabID, ok := idTokenClaims[gitlabIDClaim].(string)
68+
if !ok {
69+
return nil // this is an OIDC spec violation which is handled by the default code path
70+
}
71+
if reSHA256HexDigest.MatchString(gitlabID) {
72+
return fmt.Errorf("incompatible gitlab IDP, ID claim is SHA256 hex digest instead of digit, claims=%#v", idTokenClaims)
73+
}
74+
if !isValidUint64(gitlabID) {
75+
return fmt.Errorf("invalid gitlab IDP, ID claim is not a digit, claims=%#v", idTokenClaims)
76+
}
77+
return nil
78+
},
79+
}
80+
81+
return openid.NewProvider(providerName, transport, config)
82+
}
83+
84+
func appendPath(u url.URL, subpath string) string {
85+
u.Path = path.Join(u.Path, subpath)
86+
return u.String()
87+
}
88+
89+
// Have 256 bits from hex digest
90+
// In hexadecimal each digit encodes 4 bits
91+
// Thus we need 64 digits to represent 256 bits
92+
var reSHA256HexDigest = regexp.MustCompile(`^[[:xdigit:]]{64}$`)
93+
94+
func isValidUint64(s string) bool {
95+
_, err := strconv.ParseUint(s, 10, 64)
96+
return err == nil
97+
}

pkg/oauthserver/oauthserver/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ func (c *OAuthServerConfig) getOAuthProvider(identityProvider configapi.Identity
484484
if err != nil {
485485
return nil, err
486486
}
487-
return gitlab.NewProvider(identityProvider.Name, transport, provider.URL, provider.ClientID, clientSecret)
487+
return gitlab.NewProvider(identityProvider.Name, provider.URL, provider.ClientID, clientSecret, transport, provider.Legacy)
488488

489489
case (*configapi.GoogleIdentityProvider):
490490
clientSecret, err := configapi.ResolveStringValue(provider.ClientSecret)

0 commit comments

Comments
 (0)