Which version of MSAL Go are you using?
Latest in main + v1.4.2
Where is the issue?
- Public client
- Confidential client
- Token cache serialization
- 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.
Which version of MSAL Go are you using?
Latest in main + v1.4.2
Where is the issue?
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)?What operating system and processor architecture are you using (
go env)?go envOutputRepro
Adding this unit test to
managedidentitypackage: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
RefreshOnis returned after the first try. This voids the benefit ofRefreshOnit tries to introduce.Moreover, this unassigned
RefreshOnis 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
RefreshOnis assigned.Additional context / logs / screenshots
ARM clients in azure-sdk-for-go has been relying on
RefreshOn, thus currently suffering from this issue.