Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ca cert or skip verification in login handler #803

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ require (
github.com/vmware-tanzu/carvel-ytt v0.40.0
github.com/vmware-tanzu/tanzu-cli/test/e2e/framework v0.0.0-00010101000000-000000000000
github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230523145612-1c6fbba34686
github.com/vmware-tanzu/tanzu-plugin-runtime v1.5.0-dev.0.20240828225551-9dd9ce28e85e
github.com/vmware-tanzu/tanzu-plugin-runtime v1.4.4
go.pinniped.dev v0.20.0
golang.org/x/mod v0.15.0
golang.org/x/oauth2 v0.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -738,8 +738,8 @@ github.com/vmware-tanzu/tanzu-framework/apis/run v0.0.0-20230419030809-7081502eb
github.com/vmware-tanzu/tanzu-framework/apis/run v0.0.0-20230419030809-7081502ebf68/go.mod h1:e1Uef+Ux5BIHpYwqbeP2ZZmOzehBcez2vUEWXHe+xHE=
github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230523145612-1c6fbba34686 h1:VcuXqUXFxm5WDqWkzAlU/6cJXua0ozELnqD59fy7J6E=
github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230523145612-1c6fbba34686/go.mod h1:AFGOXZD4tH+KhpmtV0VjWjllXhr8y57MvOsIxTtywc4=
github.com/vmware-tanzu/tanzu-plugin-runtime v1.5.0-dev.0.20240828225551-9dd9ce28e85e h1:WlMLdfI1PigZ/2X0GXv2s7uP+DpRBcdVxkSr9hYHnkU=
github.com/vmware-tanzu/tanzu-plugin-runtime v1.5.0-dev.0.20240828225551-9dd9ce28e85e/go.mod h1:0fTB0rR9BX9kS+xGcwH9O0p97bn8rSdILwWCHgvXzkw=
github.com/vmware-tanzu/tanzu-plugin-runtime v1.4.4 h1:n/7lQIR2CpEeKBtr2mIAMaGDidfDkmzIvg5SZxeZP0A=
github.com/vmware-tanzu/tanzu-plugin-runtime v1.4.4/go.mod h1:0fTB0rR9BX9kS+xGcwH9O0p97bn8rSdILwWCHgvXzkw=
github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw=
github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
Expand Down
133 changes: 126 additions & 7 deletions pkg/auth/common/login_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"bufio"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"html"
"io"
Expand All @@ -27,6 +29,7 @@ import (
"golang.org/x/oauth2"

"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
"github.com/vmware-tanzu/tanzu-plugin-runtime/log"
)

Expand Down Expand Up @@ -55,6 +58,8 @@ type TanzuLoginHandler struct {
isTTY func(int) bool
idpType config.IdpType
callbackHandlerMutex sync.Mutex
tlsSkipVerify bool
caCertData string
}

// LoginOption is an optional configuration for Login().
Expand Down Expand Up @@ -108,6 +113,15 @@ func WithOrgID(orgID string) LoginOption {
}
}

// WithCertInfo customizes cert verification information
func WithCertInfo(tlsSkipVerify bool, caCertData string) LoginOption {
return func(h *TanzuLoginHandler) error {
h.tlsSkipVerify = tlsSkipVerify
h.caCertData = caCertData
return nil
}
}

// WithListenerPort specifies a TCP listener port on localhost, which will be used for the redirect_uri and to handle the
// authorization code callback. By default, a random high port will be chosen which requires the authorization server
// to support wildcard port numbers as described by https://tools.ietf.org/html/rfc8252#section-7.3:
Expand Down Expand Up @@ -165,6 +179,51 @@ func (h *TanzuLoginHandler) getTokenWithRefreshToken() (*Token, error) {
}, nil
}

// Create or update the cert map entry for the issuer if necessary
func (h *TanzuLoginHandler) updateCertMap() {
// explicitly provided cert info was successfully used to authenticate, so as a
// best-effort: save them in the cert map for convenience if necessary
if h.tlsSkipVerify || h.caCertData != "" {
u, err := url.Parse(h.issuer)
if err != nil {
fmt.Printf("Unable to parse issuer %s: %v\n", h.issuer, err)
return
}
host := u.Hostname()
if host == "" {
host = h.issuer
}

var cert *configtypes.Cert
found, _ := config.GetCert(host)

tlsSkipVerifyStr := strconv.FormatBool(h.tlsSkipVerify)
if found != nil {
if found.CACertData != h.caCertData || found.SkipCertVerify != tlsSkipVerifyStr {
cert = &configtypes.Cert{
Host: host,
CACertData: h.caCertData,
SkipCertVerify: tlsSkipVerifyStr,
Insecure: found.Insecure,
}
err = config.SetCert(cert)
if err != nil {
log.Infof("Unable to update cert info: %v\n", err)
}
}
} else {
cert = &configtypes.Cert{
Host: host,
CACertData: h.caCertData,
SkipCertVerify: tlsSkipVerifyStr,
}
if err = config.SetCert(cert); err != nil {
log.Infof("Unable to create cert info: %v\n", err)
}
}
}
}

func (h *TanzuLoginHandler) browserLogin() (*Token, error) {
var err error
if h.pkceCodePair, err = pkce.Generate(); err != nil {
Expand Down Expand Up @@ -213,6 +272,9 @@ func (h *TanzuLoginHandler) browserLogin() (*Token, error) {
if h.token == nil || h.token.Extra("id_token").(string) == "" {
return nil, errors.Errorf("token issuer %s did not return expected tokens", h.issuer)
}

h.updateCertMap()

return &Token{
IDToken: h.token.Extra("id_token").(string),
AccessToken: h.token.AccessToken,
Expand Down Expand Up @@ -366,21 +428,78 @@ func (h *TanzuLoginHandler) promptAndLoginWithAuthCode(ctx context.Context, auth
return wg.Wait
}

// Returns custom TLS configuration if explicitly provided to the handler,
// of if persisted cert information associated with the issuer endpoint is found,
// with the provided information taking precedence over persisted information.
func (h *TanzuLoginHandler) getTLSConfig() *tls.Config {
var savedCertData string
var savedSkipVerify bool

c, _ := config.GetCert(h.issuer)

if c != nil {
savedCertData = c.CACertData
savedSkipVerify, _ = strconv.ParseBool(c.SkipCertVerify)
}

if savedSkipVerify || h.tlsSkipVerify {
//nolint:gosec // skipTLSVerify: true is only possible if the user has explicitly enabled it
return &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}
}

caCertData := savedCertData
if h.caCertData != "" {
caCertData = h.caCertData
}

if caCertData != "" {
var pool *x509.CertPool
var err error

decodedCACertData, err := base64.StdEncoding.DecodeString(caCertData)
if err != nil {
log.Infof("unable to use custom cert for '%s' endpoint. Error: %s", h.issuer, err.Error())
return nil
}

pool, err = x509.SystemCertPool()
if err != nil || pool == nil {
pool = x509.NewCertPool()
}

if ok := pool.AppendCertsFromPEM(decodedCACertData); !ok {
log.Infof("unable to use custom cert for %s endpoint", h.issuer)
return nil
}
return &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}
}

return nil
}

func (h *TanzuLoginHandler) getTokenUsingAuthCode(ctx context.Context, code string) (*oauth2.Token, error) {
// TODO(vuil) support custom CA cert for UAA
if h.idpType == config.UAAIdpType {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // to update
tlsConfig := h.getTLSConfig()
if tlsConfig != nil {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = tlsConfig

sslcli := &http.Client{Transport: tr}
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
}
sslcli := &http.Client{Transport: tr}
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
}

token, err := h.oauthConfig.Exchange(ctx, code, h.pkceCodePair.Verifier())
if err != nil {
errMsg := fmt.Sprintf("failed to exchange auth code for OAuth tokens, err=%v", err)
errString := err.Error()
errMsg := fmt.Sprintf("failed to exchange auth code for OAuth tokens, err=%s", errString)

println()
log.Info(errMsg)
return nil, errors.New(errMsg)
if strings.Contains(errString, "failed to verify certificate") {
log.Info("Consider using 'tanzu config cert add' to configure certificate verification settings")
}
return nil, err
}
return token, nil
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/auth/common/login_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"

"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"

"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
Expand Down Expand Up @@ -217,3 +221,65 @@ func TestPromptAndLoginWithAuthCode(t *testing.T) {
t.Error("promptAndLoginWithAuthCode set the token with context canceled while waiting for user input")
}
}

func TestUpdateCertMap(t *testing.T) {
assert := assert.New(t)
testCertHost := "test-host"

testCases := []struct {
originalSkipVerify string
originalCACertData string
providedCACertData string
providedSkipVerify bool
expectError bool
}{
{
originalSkipVerify: "false",
originalCACertData: "",
providedSkipVerify: true,
providedCACertData: "DUMMYDATA",
},
{
originalSkipVerify: "false",
originalCACertData: "OLDDUMMYDATA",
providedSkipVerify: false,
providedCACertData: "DUMMYDATA",
},
{
originalSkipVerify: "true",
originalCACertData: "",
providedSkipVerify: false,
providedCACertData: "DUMMYDATA",
},
}

for _, tc := range testCases {
// set up cert entry if needed
if tc.originalSkipVerify != "false" || tc.originalCACertData != "" {
cert := &configtypes.Cert{
Host: testCertHost,
CACertData: tc.originalCACertData,
SkipCertVerify: tc.originalSkipVerify,
}
err := config.SetCert(cert)
assert.NoError(err)
}

lh := &TanzuLoginHandler{
issuer: testCertHost,
tlsSkipVerify: tc.providedSkipVerify,
caCertData: tc.providedCACertData,
}

lh.updateCertMap()

cert, err := config.GetCert(testCertHost)
assert.NoError(err)
assert.NotNil(cert)
assert.Equal(cert.CACertData, tc.providedCACertData)
assert.Equal(cert.SkipCertVerify, strconv.FormatBool(tc.providedSkipVerify))

err = config.DeleteCert(testCertHost)
assert.NoError(err)
}
}
12 changes: 12 additions & 0 deletions pkg/command/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package command

import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -823,6 +824,17 @@ func doInteractiveLoginAndUpdateContext(c *configtypes.Context, issuerURL string
if idpType == config.CSPIdpType {
token, err = csp.TanzuLogin(issuerURL, loginOptions...)
} else if idpType == config.UAAIdpType {
var endpointCACertData string
if endpointCACertPath != "" {
fileBytes, err := os.ReadFile(endpointCACertPath)
if err != nil {
return nil, errors.Wrapf(err, "error reading certificate file %s", endpointCACertPath)
}
endpointCACertData = base64.StdEncoding.EncodeToString(fileBytes)
}
if skipTLSVerify || endpointCACertData != "" {
loginOptions = append(loginOptions, commonauth.WithCertInfo(skipTLSVerify, endpointCACertData))
}
token, err = uaa.TanzuLogin(issuerURL, loginOptions...)
} else {
return nil, errors.New(invalidIdpType)
Expand Down
Loading