Change:
<!-- Please only use this template for submitting enhancement requests --What would you like to be : Currently, when AAD login is configured, refresh token timeout would make kubectl print an error, not able to re-login without manually remove the tokens. Expect kubectl to promote user to login again. Why is this needed: Configure refresh-token timeout from Azure is a way to force re-login after inactivity. kubectl doesn't promote re-login makes users harder to apply timeout policy.
/sig azure
The label(s) cannot be appled. These labels are supported: <detailsIn response to : /sig azure Instructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
The label(s) cannot be appled. These labels are supported: <detailsIn response to : /sig platform/azure Instructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
/area provider/azure
the root cause of this issue is in this first gets the cached token (result of device code flow) at it's trying to refresh an expired token at and failed. An error is returned. Since the expired token is still in the cache, it will always hit this path until user removes the token from kubeconfig. the fix should be: the token from cache a retry to re-initiate the device code flow in
The label(s) cannot be appled. These labels are supported: <detailsIn response to : /sig platform/azure Instructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
/sig cli These SIGs are my best guesses for this issue. Please comment if I am incorrect about one. I am a bot run by vllry. ‍
I'm working on it.
are you working on it? I will grab it if I dont hear it from you :)
have the code ready will try PR this weekend Best, Ping
/assign
Issues go stale after 90d of inactivity. Mark the issue as fresh with . Stale issues rot after an additional 30d of inactivity and eventually close. If this issue is safe to close now please do so with . Send feedback to sig-testing, kubernetes/test-infra and/or . /lifecycle stale
/remove-lifecycle stale
Closing as merged

Discussion:
type tokenSource interface { Token() (*azureToken, error) <ins> Refresh(*azureToken) (*azureToken, error) </ins> } type azureTokenSource struct {

var err error token := ts.cache.getToken(azureTokenKey) <ins> if token != nil && !token.token.IsExpired() { return token, nil } // retrieve from config if no cache </ins> if token == nil { <del> token, err = ts.retrieveTokenFromCfg() if err != nil { token, err = ts.source.Token() if err != nil { return nil, fmt.Errorf("acquiring a new fresh token: %v", err) } </del> <ins> tokenFromCfg, err := ts.retrieveTokenFromCfg() if err == nil { token = tokenFromCfg </ins> } <ins> } if token != nil { // cache and return if the token is as good // avoids frequent persistor calls </ins> if !token.token.IsExpired() { ts.cache.setToken(azureTokenKey, token) <del> err = ts.storeTokenInCfg(token) if err != nil { return nil, fmt.Errorf("storing the token in configuration: %v", err) } </del> <ins> return token, nil </ins> } <del> } if token.token.IsExpired() { token, err = ts.refreshToken(token) if err != nil { return nil, fmt.Errorf("refreshing the expired token: %v", err) </del> <ins> klog.V(4).Info("Refreshing token.") tokenFromRefresh, err := ts.Refresh(token) switch { case err == nil: token = tokenFromRefresh case autorest.IsTokenRefreshError(err): klog.V(4).Infof("Failed to refresh expired token, proceed to auth: %v", err) // reset token to nil so that the token source will be used to acquire new token = nil default: return nil, fmt.Errorf("unexpected error when refreshing token: %v", err) </ins> } <del> ts.cache.setToken(azureTokenKey, token) err = ts.storeTokenInCfg(token) </del> <ins> } if token == nil { tokenFromSource, err := ts.source.Token() </ins> if err != nil { <del> return nil, fmt.Errorf("storing the refreshed token in configuration: %v", err) </del> <ins> return nil, fmt.Errorf("failed acquiring new token: %v", err) </ins> } <ins> token = tokenFromSource } // sanity check if token == nil { return nil, fmt.Errorf("unable to acquire token") } // corner condition, newly got token is valid but expired if token.token.IsExpired() { return nil, fmt.Errorf("newly acquired token is expired") </ins> } <ins> err = ts.storeTokenInCfg(token) if err != nil { return nil, fmt.Errorf("storing the refreshed token in configuration: %v", err) } ts.cache.setToken(azureTokenKey, token) </ins> return token, nil }

return nil } <del> func (ts *azureTokenSource) refreshToken(token *azureToken) (*azureToken, error) { </del> <ins> func (ts *azureTokenSource) Refresh(token *azureToken) (*azureToken, error) { return ts.source.Refresh(token) } // refresh outdated token with adal. // adal.RefreshTokenError will be returned if error occur during refreshing. func (ts *azureTokenSourceDeviceCode) Refresh(token *azureToken) (*azureToken, error) { </ins> env, err := azure.EnvironmentFromName(token.environment) if err != nil { return nil, err

import ( "encoding/json" <ins> "errors" "net/http" </ins> "strconv" "strings" "sync"

for i, configMode := range configModes { t.Run("validate token against cache", func(t *testing.T) { fakeAccessToken := "fake token 1" <del> fakeSource := fakeTokenSource{ accessToken: fakeAccessToken, expiresOn:   strconv.FormatInt(time.Now().Add(3600*time.Second).Unix(), 10), } </del> <ins> fakeSource := fakeTokenSource{token: newFakeAzureToken(fakeAccessToken, time.Now().Add(3600*time.Second))} </ins> cfg := make(map[string]string) persiter := &fakePersister{cache: make(map[string]string)} tokenCache := newAzureTokenCache()

} } <del> fakeSource.accessToken = "fake token 2" </del> <ins> fakeSource.token = newFakeAzureToken("fake token 2", time.Now().Add(3600*time.Second)) </ins> token, err = tokenSource.Token() if err != nil { t.Errorf("failed to retrieve the cached token: %v", err)

} } <ins> func TestAzureTokenSourceScenarios(t *testing.T) { configMode := configModeDefault expiredToken := newFakeAzureToken("expired token", time.Now().Add(-time.Second)) extendedToken := newFakeAzureToken("extend token", time.Now().Add(1000*time.Second)) fakeToken := newFakeAzureToken("fake token", time.Now().Add(1000*time.Second)) wrongToken := newFakeAzureToken("wrong token", time.Now().Add(1000*time.Second)) tests := []struct { name         string sourceToken  *azureToken refreshToken *azureToken cachedToken  *azureToken configToken  *azureToken expectToken  *azureToken tokenErr     error refreshErr   error expectErr    string tokenCalls   uint refreshCalls uint persistCalls uint }{ { name:         "new config", sourceToken:  fakeToken, expectToken:  fakeToken, tokenCalls:   1, persistCalls: 1, }, { name:        "load token from cache", sourceToken: wrongToken, cachedToken: fakeToken, configToken: wrongToken, expectToken: fakeToken, }, { name:        "load token from config", sourceToken: wrongToken, configToken: fakeToken, expectToken: fakeToken, }, { name:         "cached token timeout, extend success, config token should never load", cachedToken:  expiredToken, refreshToken: extendedToken, configToken:  wrongToken, expectToken:  extendedToken, refreshCalls: 1, persistCalls: 1, }, { name:         "config token timeout, extend failure, acquire new token", configToken:  expiredToken, refreshErr:   fakeTokenRefreshError{message: "FakeError happened when refreshing"}, sourceToken:  fakeToken, expectToken:  fakeToken, refreshCalls: 1, tokenCalls:   1, persistCalls: 1, }, { name:         "unexpected error when extend", configToken:  expiredToken, refreshErr:   errors.New("unexpected refresh error"), sourceToken:  fakeToken, expectErr:    "unexpected refresh error", refreshCalls: 1, }, { name:       "token error", tokenErr:   errors.New("tokenerr"), expectErr:  "tokenerr", tokenCalls: 1, }, { name:        "Token() got expired token", sourceToken: expiredToken, expectErr:   "newly acquired token is expired", tokenCalls:  1, }, { name:        "Token() got nil but no error", sourceToken: nil, expectErr:   "unable to acquire token", tokenCalls:  1, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { persister := newFakePersister() cfg := map[string]string{} if tc.configToken != nil { cfg = token2Cfg(tc.configToken) } tokenCache := newAzureTokenCache() if tc.cachedToken != nil { tokenCache.setToken(azureTokenKey, tc.cachedToken) } fakeSource := fakeTokenSource{ token:        tc.sourceToken, tokenErr:     tc.tokenErr, refreshToken: tc.refreshToken, refreshErr:   tc.refreshErr, } tokenSource := newAzureTokenSource(&fakeSource, tokenCache, cfg, configMode, &persister) token, err := tokenSource.Token() if fakeSource.tokenCalls != tc.tokenCalls { t.Errorf("expecting tokenCalls: %v, got: %v", tc.tokenCalls, fakeSource.tokenCalls) } if fakeSource.refreshCalls != tc.refreshCalls { t.Errorf("expecting refreshCalls: %v, got: %v", tc.refreshCalls, fakeSource.refreshCalls) } if persister.calls != tc.persistCalls { t.Errorf("expecting persister calls: %v, got: %v", tc.persistCalls, persister.calls) } if tc.expectErr != "" { if !strings.Contains(err.Error(), tc.expectErr) { t.Errorf("expecting error %v, got %v", tc.expectErr, err) } if token != nil { t.Errorf("token should be nil in err situation, got %v", token) } } else { if err != nil { t.Fatalf("error should be nil, got %v", err) } if token.token.AccessToken != tc.expectToken.token.AccessToken { t.Errorf("token should have accessToken %v, got %v", token.token.AccessToken, tc.expectToken.token.AccessToken) } } }) } } </ins> type fakePersister struct { lock  sync.Mutex cache map[string]string <ins> calls uint } func newFakePersister() fakePersister { return fakePersister{cache: make(map[string]string), calls: 0} </ins> } func (p *fakePersister) Persist(cache map[string]string) error { p.lock.Lock() defer p.lock.Unlock() <ins> p.calls++ </ins> p.cache = map[string]string{} for k, v := range cache { p.cache[k] = v

return ret } <ins> // a simple token source simply always returns the token property </ins> type fakeTokenSource struct { <del> expiresOn   string accessToken string </del> <ins> token        *azureToken tokenCalls   uint tokenErr     error refreshToken *azureToken refreshCalls uint refreshErr   error </ins> } func (ts *fakeTokenSource) Token() (*azureToken, error) { <del> return &azureToken{ token:       newFackeAzureToken(ts.accessToken, ts.expiresOn), environment: "testenv", clientID:    "fake", tenantID:    "fake", apiserverID: "fake", }, nil </del> <ins> ts.tokenCalls++ return ts.token, ts.tokenErr } func (ts *fakeTokenSource) Refresh(*azureToken) (*azureToken, error) { ts.refreshCalls++ return ts.refreshToken, ts.refreshErr </ins> } func token2Cfg(token *azureToken) map[string]string {

return cfg } <del> func newFackeAzureToken(accessToken string, expiresOn string) adal.Token { </del> <ins> func newFakeAzureToken(accessToken string, expiresOnTime time.Time) *azureToken { return &azureToken{ token:       newFakeADALToken(accessToken, strconv.FormatInt(expiresOnTime.Unix(), 10)), environment: "testenv", clientID:    "fake", tenantID:    "fake", apiserverID: "fake", } } func newFakeADALToken(accessToken string, expiresOn string) adal.Token { </ins> return adal.Token{ AccessToken:  accessToken, RefreshToken: "fake",

Type:         "fake", } } <ins> // copied from go-autorest/adal type fakeTokenRefreshError struct { message string resp    *http.Response } // Error implements the error interface which is part of the TokenRefreshError interface. func (tre fakeTokenRefreshError) Error() string { return tre.message } // Response implements the TokenRefreshError interface, it returns the raw HTTP response from the refresh operation. func (tre fakeTokenRefreshError) Response() *http.Response { return tre.resp } </ins>