diff --git a/.golangci.yaml b/.golangci.yaml index ec1e153c..9578fd7e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -60,7 +60,7 @@ issues: linters-settings: funlen: - lines: 125 + lines: 150 statements: 50 goheader: template: |- diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 3754f06a..bf12e7be 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -4,7 +4,12 @@ package cmd import ( + "crypto/tls" + "crypto/x509" "encoding/json" + "fmt" + "io/ioutil" + "net/http" "os" "path/filepath" @@ -36,6 +41,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid scopes []string skipBrowser bool sessionCachePath string + caBundlePaths []string debugSessionCache bool ) cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.") @@ -44,6 +50,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid", "email", "profile"}, "OIDC scopes to request during login.") cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).") cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.") + cmd.Flags().StringSliceVar(&caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated).") cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.") mustMarkHidden(&cmd, "debug-session-cache") mustMarkRequired(&cmd, "issuer", "client-id") @@ -80,6 +87,27 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid })) } + if len(caBundlePaths) > 0 { + pool := x509.NewCertPool() + for _, p := range caBundlePaths { + pem, err := ioutil.ReadFile(p) + if err != nil { + return fmt.Errorf("could not read --ca-bundle: %w", err) + } + pool.AppendCertsFromPEM(pem) + } + tlsConfig := tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + opts = append(opts, oidcclient.WithClient(&http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tlsConfig, + }, + })) + } + tok, err := loginFunc(issuer, clientID, opts...) if err != nil { return err diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 637a7af4..a8bb03ed 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -40,6 +40,7 @@ func TestLoginOIDCCommand(t *testing.T) { oidc --issuer ISSUER --client-id CLIENT_ID [flags] Flags: + --ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated). --client-id string OpenID Connect client ID. -h, --help help for oidc --issuer string OpenID Connect issuer URL. diff --git a/internal/oidcclient/login.go b/internal/oidcclient/login.go index 5ab3b5ff..6fda48c9 100644 --- a/internal/oidcclient/login.go +++ b/internal/oidcclient/login.go @@ -44,6 +44,8 @@ type handlerState struct { scopes []string cache SessionCache + httpClient *http.Client + // Parameters of the localhost listener. listenAddr string callbackPath string @@ -122,6 +124,14 @@ func WithSessionCache(cache SessionCache) Option { } } +// WithClient sets the HTTP client used to make CLI-to-provider requests. +func WithClient(httpClient *http.Client) Option { + return func(h *handlerState) error { + h.httpClient = httpClient + return nil + } +} + // nopCache is a SessionCache that doesn't actually do anything. type nopCache struct{} @@ -144,6 +154,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) { callbackPath: "/callback", ctx: context.Background(), callbacks: make(chan callbackResult), + httpClient: http.DefaultClient, // Default implementations of external dependencies (to be mocked in tests). generateState: state.Generate, @@ -163,6 +174,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) { // Always set a long, but non-infinite timeout for this operation. ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute) defer cancel() + ctx = oidc.ClientContext(ctx, h.httpClient) h.ctx = ctx // Initialize login parameters. diff --git a/internal/oidcclient/login_test.go b/internal/oidcclient/login_test.go index ea12737f..35574ba0 100644 --- a/internal/oidcclient/login_test.go +++ b/internal/oidcclient/login_test.go @@ -416,6 +416,7 @@ func TestLogin(t *testing.T) { require.Equal(t, []*Token{&testToken}, cache.sawPutTokens) }) require.NoError(t, WithSessionCache(cache)(h)) + require.NoError(t, WithClient(&http.Client{Timeout: 10 * time.Second})(h)) h.openURL = func(actualURL string) error { parsedActualURL, err := url.Parse(actualURL)