Skip to content

Commit

Permalink
Add support for ca cert or skip verification in login handler
Browse files Browse the repository at this point in the history
The TanzuLoginHandler now leverages ca cert data or skipVerify options
provided via 'login' and 'context create' command-line flags, or
cert information store for the issuer host. Upon successful usage of the
flags, should they be provided, they will be persisted into
correspoinding cert information in the CLI configuration file.

Signed-off-by: Vui Lam <[email protected]>
  • Loading branch information
vuil committed Aug 30, 2024
1 parent d46034e commit 59c6b37
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 10 deletions.
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.5.0-dev.0.20240830162721-f5842d0e51d8
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.5.0-dev.0.20240830162721-f5842d0e51d8 h1:CIydkHbkiMdtxOsI3h9byXI1X6rRSlfyhCPViUGu9FE=
github.com/vmware-tanzu/tanzu-plugin-runtime v1.5.0-dev.0.20240830162721-f5842d0e51d8/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
134 changes: 127 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,79 @@ 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()
}
pool.AppendCertsFromPEM([]byte(caCertData))

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

0 comments on commit 59c6b37

Please sign in to comment.