Skip to content

Commit 09da816

Browse files
authored
fix: implement thread-safe token caching for self-managed oidc (#677)
Introduce a synchronized TokenCache to prevent concurrent OIDC login requests from colliding and overloading the Pinniped Supervisor or upstream LDAP server.
1 parent 45c7f1e commit 09da816

2 files changed

Lines changed: 90 additions & 20 deletions

File tree

internal/authctx/client.go

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
package authctx
66

77
import (
8+
"sync"
9+
"time"
10+
811
"github.com/pkg/errors"
912

1013
"github.com/vmware/terraform-provider-tanzu-mission-control/internal/client"
@@ -43,6 +46,51 @@ type TanzuContext struct {
4346
VMWCloudEndPoint string // selfmanaged odic issuer is stored here
4447
TMCConnection *client.TanzuMissionControl
4548
TLSConfig *proxy.TLSConfig
49+
50+
// Cache for self-managed OIDC tokens
51+
smTokenCache *TokenCache
52+
}
53+
54+
// TokenCache manages the lifecycle of the self-managed OIDC tokens.
55+
type TokenCache struct {
56+
mu sync.Mutex
57+
cachedHeaders map[string]string
58+
expiry time.Time
59+
}
60+
61+
// NewTokenCache creates a new TokenCache instance.
62+
func NewTokenCache() *TokenCache {
63+
return &TokenCache{}
64+
}
65+
66+
// GetToken returns the cached headers if they are still valid (with a 1-minute buffer),
67+
// or fetches new headers using the provided fetch function.
68+
func (tc *TokenCache) GetToken(fetch func() (map[string]string, time.Time, error)) (map[string]string, error) {
69+
tc.mu.Lock()
70+
defer tc.mu.Unlock()
71+
72+
if tc.cachedHeaders != nil && time.Now().Add(1*time.Minute).Before(tc.expiry) {
73+
return tc.cachedHeaders, nil
74+
}
75+
76+
headers, expiry, err := fetch()
77+
if err != nil {
78+
return nil, errors.Wrap(err, "failed to refresh token")
79+
}
80+
81+
tc.cachedHeaders = headers
82+
tc.expiry = expiry
83+
84+
return headers, nil
85+
}
86+
87+
// Update explicitly updates the cached headers and expiry.
88+
func (tc *TokenCache) Update(headers map[string]string, expiry time.Time) {
89+
tc.mu.Lock()
90+
defer tc.mu.Unlock()
91+
92+
tc.cachedHeaders = headers
93+
tc.expiry = expiry
4694
}
4795

4896
func (cfg *TanzuContext) Setup() (err error) {
@@ -66,6 +114,10 @@ func (cfg *TanzuContext) SetupWithDefaultTransportForTesting() (err error) {
66114
}
67115

68116
func setup(cfg *TanzuContext) (err error) {
117+
if cfg.IsSelfManaged() {
118+
cfg.smTokenCache = NewTokenCache()
119+
}
120+
69121
fetchAuthHeaders := getUserAuthCtxHeaders(cfg)
70122

71123
md, err := fetchAuthHeaders()
@@ -93,18 +145,28 @@ func setup(cfg *TanzuContext) (err error) {
93145
}
94146

95147
func getUserAuthCtxHeaders(config *TanzuContext) func() (map[string]string, error) {
96-
issuerURL := config.VMWCloudEndPoint
97-
token := config.Token
98-
proxyConfig := config.TLSConfig
99-
100148
if config.IsSelfManaged() {
101-
username := config.SMUsername
102-
103149
return func() (map[string]string, error) {
104-
return getSMUserAuthCtx(issuerURL, username, token, proxyConfig)
150+
// For compatibility considerations.
151+
if config.smTokenCache == nil {
152+
headers, _, err := getSMUserAuthCtx(config.VMWCloudEndPoint, config.SMUsername, config.Token, config.TLSConfig)
153+
if err != nil {
154+
return nil, errors.Wrap(err, "failed to get self-managed user auth context headers")
155+
}
156+
157+
return headers, nil
158+
}
159+
160+
return config.smTokenCache.GetToken(func() (map[string]string, time.Time, error) {
161+
return getSMUserAuthCtx(config.VMWCloudEndPoint, config.SMUsername, config.Token, config.TLSConfig)
162+
})
105163
}
106164
}
107165

166+
issuerURL := config.VMWCloudEndPoint
167+
token := config.Token
168+
proxyConfig := config.TLSConfig
169+
108170
return func() (map[string]string, error) {
109171
return getSaaSUserAuthCtx(issuerURL, token, proxyConfig)
110172
}

internal/authctx/selfmanaged.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,41 @@ type smSession struct {
4545
}
4646

4747
// todo: proxy support is not added for the self-managed flow. Add it when there is a requirement.
48-
func getSMUserAuthCtx(pinnipedURL, uName, password string, config *proxy.TLSConfig) (metadata map[string]string, err error) {
48+
func getSMUserAuthCtx(pinnipedURL, uName, password string, config *proxy.TLSConfig) (metadata map[string]string, expiry time.Time, err error) {
4949
if pinnipedURL == "" || uName == "" || password == "" {
50-
return nil, errors.New("Invalid auth configuration for self_managed")
50+
return nil, time.Time{}, errors.New("Invalid auth configuration for self_managed")
5151
}
5252

5353
tlsConfig, err := proxy.GetConnectorTLSConfig(config)
5454
if err != nil {
55-
return nil, err
55+
return nil, time.Time{}, err
5656
}
5757

5858
session, err := initSession(pinnipedURL, uName, password, tlsConfig)
5959
if err != nil {
60-
return nil, err
60+
return nil, time.Time{}, err
6161
}
6262

6363
expectedRedirectURL, err := url.Parse(session.sharedOauthConfig.RedirectURL)
6464
if err != nil {
65-
return nil, errors.Wrapf(err, "failed to parse expected redirect URL %s", session.sharedOauthConfig.RedirectURL)
65+
return nil, time.Time{}, errors.Wrapf(err, "failed to parse expected redirect URL %s", session.sharedOauthConfig.RedirectURL)
6666
}
6767

6868
actualRedirectURL, err := session.initiateAuthorizeRequestUnamePwd()
6969
if err != nil {
70-
return nil, errors.Wrapf(err, "failed to initiate authorize request with issuer %s", session.issuerURL)
70+
return nil, time.Time{}, errors.Wrapf(err, "failed to initiate authorize request with issuer %s", session.issuerURL)
7171
}
7272

7373
// Check that the redirect was to the expected location.
7474
if actualRedirectURL.Scheme != expectedRedirectURL.Scheme ||
7575
actualRedirectURL.Host != expectedRedirectURL.Host || actualRedirectURL.Path != expectedRedirectURL.Path {
76-
return nil, fmt.Errorf("error getting authorization: redirected to the wrong location: %s",
76+
return nil, time.Time{}, fmt.Errorf("error getting authorization: redirected to the wrong location: %s",
7777
actualRedirectURL.String())
7878
}
7979

8080
// validate the state param to detect and prevent CSRF attacks.
8181
if err := session.stateVal.Validate(actualRedirectURL.Query().Get("state")); err != nil {
82-
return nil, errors.Wrap(err, "failed to validate state")
82+
return nil, time.Time{}, errors.Wrap(err, "failed to validate state")
8383
}
8484

8585
// Get the auth code or return the error from the server.
@@ -90,10 +90,10 @@ func getSMUserAuthCtx(pinnipedURL, uName, password string, config *proxy.TLSConf
9090

9191
optionalErrorDescription := actualRedirectURL.Query().Get("error_description")
9292
if optionalErrorDescription == "" {
93-
return nil, fmt.Errorf("login failed with code %q", requiredErrorCode)
93+
return nil, time.Time{}, fmt.Errorf("login failed with code %q", requiredErrorCode)
9494
}
9595

96-
return nil, fmt.Errorf("login failed with code %q: %s", requiredErrorCode, optionalErrorDescription)
96+
return nil, time.Time{}, fmt.Errorf("login failed with code %q: %s", requiredErrorCode, optionalErrorDescription)
9797
}
9898

9999
customClient := &http.Client{
@@ -111,7 +111,7 @@ func getSMUserAuthCtx(pinnipedURL, uName, password string, config *proxy.TLSConf
111111

112112
token, err := session.sharedOauthConfig.Exchange(tokenCtx, authCode, session.pkceCodePair.Verifier())
113113
if err != nil {
114-
return nil, errors.Wrapf(err, "failed to exchange auth code for oauth tokens")
114+
return nil, time.Time{}, errors.Wrapf(err, "failed to exchange auth code for oauth tokens")
115115
}
116116

117117
extraFields := map[string]interface{}{extraIDToken: token.Extra(extraIDToken).(string)}
@@ -123,7 +123,7 @@ func getSMUserAuthCtx(pinnipedURL, uName, password string, config *proxy.TLSConf
123123

124124
token = token.WithExtra(extraFields)
125125

126-
return getSMHeaders(token), nil
126+
return getSMHeaders(token), token.Expiry, nil
127127
}
128128

129129
// todo: if slowness is experienced, then we can avoid re-initialising same values again.
@@ -251,7 +251,15 @@ func (s *smSession) getAuthCodeURL() string {
251251
}
252252

253253
func refreshSMUserAuthCtx(config *TanzuContext) {
254-
md, _ := getSMUserAuthCtx(config.VMWCloudEndPoint, config.SMUsername, config.Token, config.TLSConfig)
254+
md, expiry, err := getSMUserAuthCtx(config.VMWCloudEndPoint, config.SMUsername, config.Token, config.TLSConfig)
255+
if err != nil {
256+
return
257+
}
258+
259+
if config.smTokenCache != nil {
260+
config.smTokenCache.Update(md, expiry)
261+
}
262+
255263
for key, value := range md {
256264
config.TMCConnection.Headers.Set(key, value)
257265
}

0 commit comments

Comments
 (0)