diff --git a/CHANGELOG.md b/CHANGELOG.md index 157382fbc85da..628fd6efa2010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ All notable changes to Sourcegraph are documented in this file. - - `golang.org/x/net/trace` instrumentation, previously available under `/debug/requests` and `/debug/events`, has been removed entirely from core Sourcegraph services. It remains available for Zoekt. [#53795](https://github.com/sourcegraph/sourcegraph/pull/53795) - Sourcegraph now supports more than one auth provider per URL. [#54289](https://github.com/sourcegraph/sourcegraph/pull/54289) +- GitLab auth providers now support an `ssoURL` option that facilitates scenarios where a GitLab group requires SAML/SSO. [#54957](https://github.com/sourcegraph/sourcegraph/pull/54957) ### Fixed diff --git a/doc/admin/auth/index.md b/doc/admin/auth/index.md index f9a9b2411d6e5..5b158f6d6753d 100644 --- a/doc/admin/auth/index.md +++ b/doc/admin/auth/index.md @@ -327,6 +327,21 @@ You can use the following filters to control how users can create accounts and s } ``` +### How to set up GitLab auth provider for use with GitLab group SAML/SSO + +GitLab groups can require SAML/SSO sign-in to have access to the group. The regular OAuth sign-in won't work in this case, as users will be redirected to the normal GitLab sign-in page, requesting a username/password. In this scenario, add a `ssoURL` to your GitLab auth provider configuration: + + ```json + { + "type": "gitlab", + // ... + "ssoURL": "https://gitlab.com/groups/your-group/-/saml/sso?token=xxxxxxxx" + ] + } + ``` + +The `token` parameter can be found on the **Settings > SAML SSO** page on GitLab. + ## Bitbucket Cloud [Create a Bitbucket Cloud OAuth consumer](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/). Set the following values, replacing `sourcegraph.example.com` with the IP or hostname of your diff --git a/enterprise/cmd/frontend/internal/auth/gitlaboauth/BUILD.bazel b/enterprise/cmd/frontend/internal/auth/gitlaboauth/BUILD.bazel index ead1f2918a3b9..c4a64d5df329d 100644 --- a/enterprise/cmd/frontend/internal/auth/gitlaboauth/BUILD.bazel +++ b/enterprise/cmd/frontend/internal/auth/gitlaboauth/BUILD.bazel @@ -41,6 +41,7 @@ go_test( timeout = "short", srcs = [ "config_test.go", + "login_test.go", "middleware_test.go", "session_test.go", ], @@ -64,9 +65,13 @@ go_test( "//lib/errors", "//schema", "@com_github_davecgh_go_spew//spew", + "@com_github_dghubble_gologin//oauth2", + "@com_github_dghubble_gologin//testutils", "@com_github_google_go_cmp//cmp", "@com_github_sergi_go_diff//diffmatchpatch", "@com_github_sourcegraph_log//logtest", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", "@org_golang_x_oauth2//:oauth2", ], ) diff --git a/enterprise/cmd/frontend/internal/auth/gitlaboauth/login.go b/enterprise/cmd/frontend/internal/auth/gitlaboauth/login.go index 32833ea559f2d..3ce8fd819cea9 100644 --- a/enterprise/cmd/frontend/internal/auth/gitlaboauth/login.go +++ b/enterprise/cmd/frontend/internal/auth/gitlaboauth/login.go @@ -15,6 +15,50 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) +// SSOLoginHandler is a custom implementation of github.com/dghubble/gologin/oauth2's LoginHandler method. +// It takes an extra ssoAuthURL parameter, and adds the original authURL as a redirect parameter to that +// URL. +// +// This is used in cases where customers use SAML/SSO on their GitLab configurations. The default +// way GitLab handles redirects for groups that require SSO sign-on does not work, and users +// need to sign into GitLab outside of Sourcegraph, and can only then come back and use OAuth. +// +// This implementaion allows users to be directed to their GitLab SSO sign-in page, and then +// the redirect query parameter will redirect them to the OAuth sign-in flow that Sourcegraph +// requires. +func SSOLoginHandler(config *oauth2.Config, failure http.Handler, ssoAuthURL string) http.Handler { + if failure == nil { + failure = gologin.DefaultFailureHandler + } + fn := func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + state, err := oauth2Login.StateFromContext(ctx) + if err != nil { + ctx = gologin.WithError(ctx, err) + failure.ServeHTTP(w, req.WithContext(ctx)) + return + } + authURL, err := url.Parse(config.AuthCodeURL(state)) + if err != nil { + ctx = gologin.WithError(ctx, err) + failure.ServeHTTP(w, req.WithContext(ctx)) + return + } + + ssoAuthURL, err := url.Parse(ssoAuthURL) + if err != nil { + ctx = gologin.WithError(ctx, err) + failure.ServeHTTP(w, req.WithContext(ctx)) + return + } + queryParams := ssoAuthURL.Query() + queryParams.Add("redirect", authURL.Path+"?"+authURL.RawQuery) + ssoAuthURL.RawQuery = queryParams.Encode() + http.Redirect(w, req, ssoAuthURL.String(), http.StatusFound) + } + return http.HandlerFunc(fn) +} + func LoginHandler(config *oauth2.Config, failure http.Handler) http.Handler { return oauth2Login.LoginHandler(config, failure) } diff --git a/enterprise/cmd/frontend/internal/auth/gitlaboauth/login_test.go b/enterprise/cmd/frontend/internal/auth/gitlaboauth/login_test.go new file mode 100644 index 0000000000000..badd96a16a4a8 --- /dev/null +++ b/enterprise/cmd/frontend/internal/auth/gitlaboauth/login_test.go @@ -0,0 +1,46 @@ +package gitlaboauth + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + oauth2Login "github.com/dghubble/gologin/oauth2" + "github.com/dghubble/gologin/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" +) + +func TestSSOLoginHandler(t *testing.T) { + expectedState := "state_val" + ssoURL := "https://api.example.com/-/saml/sso?token=1234" + expectedRedirectURL := "/authorize?client_id=client_id&redirect_uri=redirect_url&response_type=code&state=state_val" + config := &oauth2.Config{ + ClientID: "client_id", + ClientSecret: "client_secret", + RedirectURL: "redirect_url", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://api.example.com/authorize", + }, + } + failure := testutils.AssertFailureNotCalled(t) + + // SSOLoginHandler assert that: + // - redirects to the SSO URL, with a redirect to the authURL + // - redirect status code is 302 + // - redirect url is the OAuth2 Config RedirectURL with the ClientID and ctx state + loginHandler := SSOLoginHandler(config, failure, ssoURL) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + ctx := oauth2Login.WithState(context.Background(), expectedState) + loginHandler.ServeHTTP(w, req.WithContext(ctx)) + assert.Equal(t, http.StatusFound, w.Code) + locationURL, err := url.Parse(w.HeaderMap.Get("Location")) + require.NoError(t, err) + locationRedirectURL, err := url.QueryUnescape(locationURL.Query().Get("redirect")) + require.NoError(t, err) + assert.Equal(t, expectedRedirectURL, locationRedirectURL) +} diff --git a/enterprise/cmd/frontend/internal/auth/gitlaboauth/provider.go b/enterprise/cmd/frontend/internal/auth/gitlaboauth/provider.go index f840c4f14d15a..dc2f97f38f183 100644 --- a/enterprise/cmd/frontend/internal/auth/gitlaboauth/provider.go +++ b/enterprise/cmd/frontend/internal/auth/gitlaboauth/provider.go @@ -51,6 +51,12 @@ func parseProvider(logger log.Logger, db database.DB, callbackURL string, p *sch ServiceID: codeHost.ServiceID, ServiceType: codeHost.ServiceType, Login: func(oauth2Cfg oauth2.Config) http.Handler { + // If p.SsoURL is set, we want to use our own SSOLoginHandler + // that takes care of GitLab SSO sign-in redirects. + if p.SsoURL != "" { + return SSOLoginHandler(&oauth2Cfg, nil, p.SsoURL) + } + // Otherwise use the normal LoginHandler return LoginHandler(&oauth2Cfg, nil) }, Callback: func(oauth2Cfg oauth2.Config) http.Handler { diff --git a/schema/schema.go b/schema/schema.go index 0a4a72790111d..f3414490f0046 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -1220,6 +1220,8 @@ type GitLabAuthProvider struct { DisplayPrefix *string `json:"displayPrefix,omitempty"` Hidden bool `json:"hidden,omitempty"` Order int `json:"order,omitempty"` + // SsoURL description: An alternate sign-in URL used to ease SSO sign-in flows, such as https://gitlab.com/groups/your-group/saml/sso?token=xxxxxx + SsoURL string `json:"ssoURL,omitempty"` // TokenRefreshWindowMinutes description: Time in minutes before token expiry when we should attempt to refresh it TokenRefreshWindowMinutes int `json:"tokenRefreshWindowMinutes,omitempty"` Type string `json:"type"` diff --git a/schema/site.schema.json b/schema/site.schema.json index e41d1d5f1a79e..e91bc3a4ad326 100644 --- a/schema/site.schema.json +++ b/schema/site.schema.json @@ -2946,6 +2946,11 @@ "description": "URL of the GitLab instance, such as https://gitlab.com or https://gitlab.example.com.", "default": "https://gitlab.com/" }, + "ssoURL": { + "type": "string", + "description": "An alternate sign-in URL used to ease SSO sign-in flows, such as https://gitlab.com/groups/your-group/saml/sso?token=xxxxxx", + "default": "" + }, "clientID": { "type": "string", "description": "The Client ID of the GitLab OAuth app, accessible from https://gitlab.com/oauth/applications (or the same path on your private GitLab instance)."