-
Notifications
You must be signed in to change notification settings - Fork 100
Description
Which version of MSAL Go are you using?
Latest in main + v1.4.2
Where is the issue?
- Public client
- Device code flow
- Username/Password (ROPC grant)
- Authorization code flow
- Confidential client
- Authorization code flow
- Client credentials:
- client secret
- client certificate
- Token cache serialization
- In-memory cache
- Other (please describe)
- Managed identity client/token caching
Is this a new or an existing app?
The app is in production and I have upgraded to a new version of Microsoft Authentication Library for Go.
What version of Go are you using (go version)?
$ go version go version go1.24.3 linux/amd64
What operating system and processor architecture are you using (go env)?
go env Output
$ go env
Repro
Adding this unit test to managedidentity package:
package managedidentity
import (
"context"
"encoding/json"
"net/http"
"testing"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/mock"
)
func TestRefreshOnNotPersistedToCache(t *testing.T) {
resource := "https://management.azure.com"
miType := SystemAssigned()
setEnvVars(t, DefaultToIMDS)
before := cacheManager
defer func() { cacheManager = before }()
cacheManager = storage.New(nil)
tokenLifetime := 24 * time.Hour
accessToken := "[0]"
mockClient := mock.NewClient()
responseBody, err := json.Marshal(SuccessfulResponse{
AccessToken: accessToken,
ExpiresIn: int64(tokenLifetime / time.Second),
ExpiresOn: time.Now().Add(tokenLifetime).Unix(),
Resource: resource,
TokenType: "Bearer",
// Note: RefreshOn is intentionally not set in the response
})
if err != nil {
t.Fatal(err)
}
mockClient.AppendResponse(
mock.WithHTTPStatusCode(200),
mock.WithBody(responseBody),
mock.WithCallback(func(r *http.Request) {
t.Logf("token provider server providing token: %s", accessToken)
}),
)
client, err := New(miType, WithHTTPClient(mockClient))
if err != nil {
t.Fatal(err)
}
for i := 1; i <= 3; i++ {
result, err := client.AcquireToken(context.Background(), resource)
if err != nil {
t.Fatal(err)
}
t.Logf("Call %d - token: %s, ExpiresOn: %s, RefreshOn: %s", i, result.AccessToken, result.ExpiresOn, result.Metadata.RefreshOn)
}
}Or, create an app that calls AcquireToken(...) multiple times on the same scope, such as from different ARM clients in azure-sdk-for-go.
Expected behavior
Correct RefreshOn is returned after the first try. This allows the caller to call AcquireToken() again to refresh as soon as the recommended time passes.
Actual behavior
Unassigned RefreshOn is returned after the first try. This voids the benefit of RefreshOn it tries to introduce.
=== RUN TestRefreshOnNotPersistedToCache
refresh_on_cache_test.go:41: token provider server providing token: [0]
refresh_on_cache_test.go:55: Call 1 - token: [0], ExpiresOn: 2025-06-13 17:57:03 -0700 PDT, RefreshOn: 2025-06-13 05:57:03.480066915 -0700 PDT m=+43199.520639356
refresh_on_cache_test.go:55: Call 2 - token: [0], ExpiresOn: 2025-06-14 00:57:03 +0000 UTC, RefreshOn: 0001-01-01 00:00:00 +0000 UTC
refresh_on_cache_test.go:55: Call 3 - token: [0], ExpiresOn: 2025-06-14 00:57:03 +0000 UTC, RefreshOn: 0001-01-01 00:00:00 +0000 UTC
Moreover, this unassigned RefreshOn is stored in the cache, which means the logic to refresh the token in this layer will gatekeep the new token from being requested until hardcoded 5-minute before expiration. This increases the risk of token expiring before it is used by the caller.
Possible solution
Write to the cache after RefreshOn is assigned.
Additional context / logs / screenshots
ARM clients in azure-sdk-for-go has been relying on RefreshOn, thus currently suffering from this issue.