From c701a11cbad9d36c77e1a5ebc490587f1e2a9af9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 3 Nov 2023 15:54:59 +0000 Subject: [PATCH] fix: add support for custom auth header with client secret This fixes OAuth2 with JFrog Artifactory. --- coderd/externalauth/externalauth.go | 28 ++++++++++++++++ coderd/externalauth/externalauth_test.go | 41 ++++++++++++++++++++++++ codersdk/externalauth.go | 1 + site/src/api/typesGenerated.ts | 2 ++ 4 files changed, 72 insertions(+) diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go index 8802b5d5f6108..04d8dae9bc99f 100644 --- a/coderd/externalauth/externalauth.go +++ b/coderd/externalauth/externalauth.go @@ -457,6 +457,9 @@ func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([ if entry.Type == string(codersdk.EnhancedExternalAuthProviderAzureDevops) { oauthConfig = &jwtConfig{oc} } + if entry.Type == string(codersdk.EnhancedExternalAuthProviderJFrog) { + oauthConfig = &exchangeWithClientSecret{oc} + } cfg := &Config{ OAuth2Config: oauthConfig, @@ -619,3 +622,28 @@ func (c *jwtConfig) Exchange(ctx context.Context, code string, opts ...oauth2.Au )..., ) } + +// exchangeWithClientSecret wraps an OAuth config and adds the client secret +// to the Exchange request as a Bearer header. This is used by JFrog Artifactory. +type exchangeWithClientSecret struct { + *oauth2.Config +} + +func (e *exchangeWithClientSecret) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + httpClient, ok := ctx.Value(oauth2.HTTPClient).(*http.Client) + if httpClient == nil || !ok { + httpClient = http.DefaultClient + } + oldTransport := httpClient.Transport + httpClient.Transport = roundTripper(func(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", "Bearer "+e.ClientSecret) + return oldTransport.RoundTrip(req) + }) + return e.Config.Exchange(context.WithValue(ctx, oauth2.HTTPClient, httpClient), code, opts...) +} + +type roundTripper func(req *http.Request) (*http.Response, error) + +func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return r(req) +} diff --git a/coderd/externalauth/externalauth_test.go b/coderd/externalauth/externalauth_test.go index 2108063b91f46..387bdc77382aa 100644 --- a/coderd/externalauth/externalauth_test.go +++ b/coderd/externalauth/externalauth_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "net/http" + "net/http/httptest" "net/url" "testing" "time" @@ -298,6 +299,40 @@ func TestRefreshToken(t *testing.T) { }) } +func TestExchangeWithClientSecret(t *testing.T) { + t.Parallel() + // This ensures a provider that requires the custom + // client secret exchange works. + configs, err := externalauth.ConvertConfig([]codersdk.ExternalAuthConfig{{ + // JFrog just happens to require this custom type. + + Type: codersdk.EnhancedExternalAuthProviderJFrog.String(), + ClientID: "id", + ClientSecret: "secret", + }}, &url.URL{}) + require.NoError(t, err) + config := configs[0] + + client := &http.Client{ + Transport: roundTripper(func(req *http.Request) (*http.Response, error) { + require.Equal(t, "Bearer secret", req.Header.Get("Authorization")) + rec := httptest.NewRecorder() + rec.WriteHeader(http.StatusOK) + body, err := json.Marshal(&oauth2.Token{ + AccessToken: "bananas", + }) + if err != nil { + return nil, err + } + _, err = rec.Write(body) + return rec.Result(), err + }), + } + + _, err = config.Exchange(context.WithValue(context.Background(), oauth2.HTTPClient, client), "code") + require.NoError(t, err) +} + func TestConvertYAML(t *testing.T) { t.Parallel() for _, tc := range []struct { @@ -438,3 +473,9 @@ func setupOauth2Test(t *testing.T, settings testConfig) (*oidctest.FakeIDP, *ext return fake, config, link } + +type roundTripper func(req *http.Request) (*http.Response, error) + +func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return r(req) +} diff --git a/codersdk/externalauth.go b/codersdk/externalauth.go index 0167ca8156259..60800af7195de 100644 --- a/codersdk/externalauth.go +++ b/codersdk/externalauth.go @@ -35,6 +35,7 @@ const ( EnhancedExternalAuthProviderGitLab EnhancedExternalAuthProvider = "gitlab" EnhancedExternalAuthProviderBitBucket EnhancedExternalAuthProvider = "bitbucket" EnhancedExternalAuthProviderSlack EnhancedExternalAuthProvider = "slack" + EnhancedExternalAuthProviderJFrog EnhancedExternalAuthProvider = "jfrog" ) type ExternalAuth struct { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ce7facdc55522..1485121342a32 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1684,12 +1684,14 @@ export type EnhancedExternalAuthProvider = | "bitbucket" | "github" | "gitlab" + | "jfrog" | "slack"; export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = [ "azure-devops", "bitbucket", "github", "gitlab", + "jfrog", "slack", ];