Skip to content

Commit 6f5f5f1

Browse files
authored
Merge pull request #901 from AzureAD/avdunn/cache-refresh-metadata
Add CacheRefreshReason to telemetry and metadata
2 parents ffaa5d2 + 2de2b4a commit 6f5f5f1

File tree

10 files changed

+220
-76
lines changed

10 files changed

+220
-76
lines changed

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AcquireTokenSilentSupplier.java

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ AuthenticationResult execute() throws Exception {
5757

5858
shouldRefresh = shouldRefresh(silentRequest.parameters(), res);
5959

60-
if (shouldRefresh || clientApplication.serviceBundle().getServerSideTelemetry().getCurrentRequest().cacheInfo() == CacheTelemetry.REFRESH_REFRESH_IN.telemetryValue) {
60+
if (shouldRefresh) {
6161
if (!StringHelper.isBlank(res.refreshToken())) {
6262
//There are certain scenarios where the cached authority may differ from the client app's authority,
6363
// such as when a request is instance aware. Unless overridden by SilentParameters.authorityUrl, the
@@ -66,7 +66,8 @@ AuthenticationResult execute() throws Exception {
6666
requestAuthority = Authority.createAuthority(new URL(requestAuthority.authority().replace(requestAuthority.host(),
6767
res.account().environment())));
6868
}
69-
res = makeRefreshRequest(res, requestAuthority);
69+
70+
res = makeRefreshRequest(res, requestAuthority, clientApplication.serviceBundle().getServerSideTelemetry().getCurrentRequest().cacheInfo());
7071
} else {
7172
res = null;
7273
}
@@ -81,27 +82,32 @@ AuthenticationResult execute() throws Exception {
8182
return res;
8283
}
8384

84-
private AuthenticationResult makeRefreshRequest(AuthenticationResult cachedResult, Authority requestAuthority) throws Exception {
85+
private AuthenticationResult makeRefreshRequest(AuthenticationResult cachedResult, Authority requestAuthority, CacheRefreshReason refreshReason) throws Exception {
86+
8587
RefreshTokenRequest refreshTokenRequest = new RefreshTokenRequest(
8688
RefreshTokenParameters.builder(silentRequest.parameters().scopes(), cachedResult.refreshToken()).build(),
8789
silentRequest.application(),
8890
silentRequest.requestContext(),
8991
silentRequest);
9092

93+
//The ServiceBundle will have a new CurrentRequest object when the RefreshTokenRequest is made, so the telemetry value needs to be set again
94+
setCacheTelemetry(refreshReason);
95+
9196
AcquireTokenByAuthorizationGrantSupplier acquireTokenByAuthorisationGrantSupplier =
9297
new AcquireTokenByAuthorizationGrantSupplier(clientApplication, refreshTokenRequest, requestAuthority);
9398

9499
try {
95100
AuthenticationResult refreshedResult = acquireTokenByAuthorisationGrantSupplier.execute();
96101

97102
refreshedResult.metadata().tokenSource(TokenSource.IDENTITY_PROVIDER);
103+
refreshedResult.metadata().cacheRefreshReason(refreshReason);
98104

99105
log.info("Access token refreshed successfully.");
100106
return refreshedResult;
101107
} catch (MsalServiceException ex) {
102108
//If the token refresh attempt threw a MsalServiceException but the refresh attempt was done
103109
// only because of refreshOn, then simply return the existing cached token rather than throw an exception
104-
if (clientApplication.serviceBundle().getServerSideTelemetry().getCurrentRequest().cacheInfo() == CacheTelemetry.REFRESH_REFRESH_IN.telemetryValue) {
110+
if (refreshReason == CacheRefreshReason.PROACTIVE_REFRESH) {
105111
return cachedResult;
106112
}
107113
throw ex;
@@ -113,48 +119,48 @@ private boolean shouldRefresh(SilentParameters parameters, AuthenticationResult
113119

114120
//If forceRefresh is true, no reason to check any other option
115121
if (parameters.forceRefresh()) {
116-
setCacheTelemetry(CacheTelemetry.REFRESH_FORCE_REFRESH.telemetryValue);
117-
log.debug("Refreshing access token because forceRefresh parameter is true.");
122+
setCacheTelemetry(CacheRefreshReason.FORCE_REFRESH);
123+
log.debug(String.format("Refreshing access token. Cache refresh reason: %s", CacheRefreshReason.FORCE_REFRESH));
118124
return true;
119125
}
120126

121127
//If the request contains claims then the token should be refreshed, to ensure that the returned token has the correct claims
122128
// Note: these are the types of claims found in (for example) a claims challenge, and do not include client capabilities
123129
if (parameters.claims() != null) {
124-
setCacheTelemetry(CacheTelemetry.REFRESH_FORCE_REFRESH.telemetryValue);
125-
log.debug("Refreshing access token because the claims parameter is not null.");
130+
setCacheTelemetry(CacheRefreshReason.CLAIMS);
131+
log.debug(String.format("Refreshing access token. Cache refresh reason: %s", CacheRefreshReason.CLAIMS));
126132
return true;
127133
}
128134

129135
long currTimeStampSec = new Date().getTime() / 1000;
130136

131137
//If the access token is expired or within 5 minutes of becoming expired, refresh it
132138
if (!StringHelper.isBlank(cachedResult.accessToken()) && cachedResult.expiresOn() < (currTimeStampSec + ACCESS_TOKEN_EXPIRE_BUFFER_IN_SEC)) {
133-
setCacheTelemetry(CacheTelemetry.REFRESH_ACCESS_TOKEN_EXPIRED.telemetryValue);
134-
log.debug("Refreshing access token because it is expired.");
139+
setCacheTelemetry(CacheRefreshReason.EXPIRED);
140+
log.debug(String.format("Refreshing access token. Cache refresh reason: %s", CacheRefreshReason.EXPIRED));
135141
return true;
136142
}
137143

138144
//Certain long-lived tokens will have a 'refresh on' time that indicates a refresh should be attempted long before the token would expire
139145
if (!StringHelper.isBlank(cachedResult.accessToken()) &&
140146
cachedResult.refreshOn() != null && cachedResult.refreshOn() > 0 &&
141147
cachedResult.refreshOn() < currTimeStampSec && cachedResult.expiresOn() >= (currTimeStampSec + ACCESS_TOKEN_EXPIRE_BUFFER_IN_SEC)){
142-
setCacheTelemetry(CacheTelemetry.REFRESH_REFRESH_IN.telemetryValue);
143-
log.debug("Attempting to refresh access token because it is after the refreshOn time.");
148+
setCacheTelemetry(CacheRefreshReason.PROACTIVE_REFRESH);
149+
log.debug(String.format("Refreshing access token. Cache refresh reason: %s", CacheRefreshReason.PROACTIVE_REFRESH));
144150
return true;
145151
}
146152

147153
//If there is a refresh token but no access token, we should use the refresh token to get the access token
148154
if (StringHelper.isBlank(cachedResult.accessToken()) && !StringHelper.isBlank(cachedResult.refreshToken())) {
149-
setCacheTelemetry(CacheTelemetry.REFRESH_NO_ACCESS_TOKEN.telemetryValue);
150-
log.debug("Refreshing access token because it was missing from the cache.");
155+
setCacheTelemetry(CacheRefreshReason.NO_CACHED_ACCESS_TOKEN);
156+
log.debug(String.format("Refreshing access token. Cache refresh reason: %s", CacheRefreshReason.NO_CACHED_ACCESS_TOKEN));
151157
return true;
152158
}
153159

154160
return false;
155161
}
156162

157-
private void setCacheTelemetry(int cacheInfoValue){
163+
private void setCacheTelemetry(CacheRefreshReason cacheInfoValue){
158164
clientApplication.serviceBundle().getServerSideTelemetry().getCurrentRequest().cacheInfo(cacheInfoValue);
159165
}
160166
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthenticationResultMetadata.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@
2020
@Builder
2121
public class AuthenticationResultMetadata implements Serializable {
2222

23+
/**
24+
* The source of the tokens in the {@link AuthenticationResult}, see {@link TokenSource} for possible values
25+
*/
2326
private TokenSource tokenSource;
27+
28+
/**
29+
* When the token should be proactively refreshed. May be null or 0 if proactive refresh is not used
30+
*/
2431
private Long refreshOn;
32+
33+
/**
34+
* Specifies the reason for refreshing the access token, see {@link CacheRefreshReason} for possible values. Will be {@link CacheRefreshReason#NOT_APPLICABLE} if the token was returned from the cache or if the API used to fetch the token does not attempt to read the cache.
35+
*/
36+
@Builder.Default
37+
private CacheRefreshReason cacheRefreshReason = CacheRefreshReason.NOT_APPLICABLE;
2538
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
/**
7+
* Specifies the reason for fetching the access token from the identity provider when using {@link AbstractClientApplicationBase#acquireTokenSilently(SilentParameters)}
8+
*/
9+
public enum CacheRefreshReason {
10+
/**
11+
* Token did not need to be refreshed, or was retrieved in a non-silent call
12+
*/
13+
NOT_APPLICABLE(0),
14+
/**
15+
* Silent call was made with the force refresh option
16+
*/
17+
FORCE_REFRESH(1),
18+
/**
19+
* Silent call was made with claims set
20+
*/
21+
CLAIMS(1),
22+
/**
23+
* Access token was missing from the cache, but a valid refresh token was used to retrieve a new access token
24+
*/
25+
NO_CACHED_ACCESS_TOKEN(2),
26+
/**
27+
* Cached access token was expired and successfully refreshed
28+
*/
29+
EXPIRED(3),
30+
/**
31+
* Cached access token was not expired but was after the 'refresh_in' value, and was proactively refreshed before the expiration date
32+
*/
33+
PROACTIVE_REFRESH(4);
34+
35+
final int telemetryValue;
36+
37+
CacheRefreshReason(int telemetryValue) {
38+
this.telemetryValue = telemetryValue;
39+
}
40+
}

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CacheTelemetry.java

Lines changed: 0 additions & 26 deletions
This file was deleted.

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CurrentRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class CurrentRequest {
1414
private final PublicApi publicApi;
1515

1616
@Setter
17-
private int cacheInfo = -1;
17+
private CacheRefreshReason cacheInfo = CacheRefreshReason.NOT_APPLICABLE;
1818

1919
@Setter
2020
private String regionUsed = StringHelper.EMPTY_STRING;

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/ServerSideTelemetry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private synchronized String buildCurrentRequestHeader() {
6767
String currentRequestHeader = SCHEMA_VERSION + SCHEMA_PIPE_DELIMITER +
6868
currentRequest.publicApi().getApiId() +
6969
SCHEMA_COMMA_DELIMITER +
70-
(currentRequest.cacheInfo() == -1 ? "" : currentRequest.cacheInfo()) +
70+
currentRequest.cacheInfo().telemetryValue +
7171
SCHEMA_COMMA_DELIMITER +
7272
currentRequest.regionUsed() +
7373
SCHEMA_COMMA_DELIMITER +

msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/SilentRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class SilentRequest extends MsalRequest {
3232

3333
if (parameters.forceRefresh()) {
3434
application.serviceBundle().getServerSideTelemetry().getCurrentRequest().cacheInfo(
35-
CacheTelemetry.REFRESH_FORCE_REFRESH.telemetryValue);
35+
CacheRefreshReason.FORCE_REFRESH);
3636
}
3737
}
3838
}

msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AcquireTokenSilentlyTest.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
import static org.junit.jupiter.api.Assertions.assertNotNull;
1212
import static org.junit.jupiter.api.Assertions.assertEquals;
1313
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
14+
import static org.mockito.Mockito.*;
1415

1516
import java.io.IOException;
1617
import java.net.URISyntaxException;
1718
import java.nio.file.Files;
1819
import java.nio.file.Paths;
1920
import java.util.Collections;
21+
import java.util.HashMap;
2022
import java.util.concurrent.CompletableFuture;
2123
import java.util.concurrent.ExecutionException;
24+
import java.util.concurrent.TimeUnit;
2225

2326

2427
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -116,6 +119,97 @@ void confidentialAppAcquireTokenSilently_claimsSkipCache() throws Throwable {
116119
assertInstanceOf(MsalInteractionRequiredException.class, ex.getCause());
117120
}
118121

122+
@Test
123+
void testTokenRefreshReasons() throws Exception {
124+
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class);
125+
126+
ConfidentialClientApplication cca =
127+
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
128+
.authority("https://login.microsoftonline.com/tenant/")
129+
.instanceDiscovery(false)
130+
.validateAuthority(false)
131+
.httpClient(httpClientMock)
132+
.build();
133+
134+
HashMap<String, String> responseParameters = new HashMap<>();
135+
136+
//Acquire a token that expires at the same time it is acquired, so it will expire before the next acquire token call
137+
responseParameters.put("access_token", "expiredToken");
138+
responseParameters.put("id_token", TestHelper.createIdToken(new HashMap<>()));
139+
responseParameters.put("expires_in", "0");
140+
TestHelper.createTokenRequestMock(httpClientMock, TestHelper.getSuccessfulTokenResponse(responseParameters), 200);
141+
142+
OnBehalfOfParameters parameters = OnBehalfOfParameters.builder(Collections.singleton("someScopes"), new UserAssertion(TestHelper.signedAssertion)).build();
143+
IAuthenticationResult result = cca.acquireToken(parameters).get();
144+
145+
//There should be one token in the cache, and no refresh behavior should have happened yet
146+
assertRefreshedToken(result, "expiredToken", CacheRefreshReason.NOT_APPLICABLE, cca.tokenCache.accessTokens.size());
147+
148+
//Attempt to retrieve the cached token, however it is expired and should be refreshed.
149+
// In this test, it will be replaced with a token that expires in 1 minute
150+
responseParameters.put("access_token", "nearlyExpiredToken");
151+
responseParameters.put("expires_in", "60");
152+
TestHelper.createTokenRequestMock(httpClientMock, TestHelper.getSuccessfulTokenResponse(responseParameters), 200);
153+
154+
SilentParameters silentParameters = SilentParameters.builder(Collections.singleton("someScopes"), result.account()).build();
155+
result = cca.acquireTokenSilently(silentParameters).get();
156+
157+
//Ensure there is still one token in the cache, however it is the new refreshed token rather than the token from the first mocked call
158+
assertRefreshedToken(result, "nearlyExpiredToken", CacheRefreshReason.EXPIRED, cca.tokenCache.accessTokens.size());
159+
160+
//Attempt to retrieve the cached token, however it is within the 5-minute buffer and should be refreshed.
161+
// In this test, it will be replaced with a token that expires in 1 hour but has a refresh_in time of 1 second
162+
responseParameters.put("access_token", "refreshInToken");
163+
responseParameters.put("expires_in", "3600");
164+
responseParameters.put("refresh_in", "1");
165+
TestHelper.createTokenRequestMock(httpClientMock, TestHelper.getSuccessfulTokenResponse(responseParameters), 200);
166+
167+
silentParameters = SilentParameters.builder(Collections.singleton("someScopes"), result.account()).build();
168+
result = cca.acquireTokenSilently(silentParameters).get();
169+
170+
assertRefreshedToken(result, "refreshInToken", CacheRefreshReason.EXPIRED, cca.tokenCache.accessTokens.size());
171+
172+
//Attempt to retrieve the cached token, however it is within the 5-minute buffer and should be refreshed.
173+
// In this test, it will be replaced with a token that expires in 1 hour (and does not have a valid refresh_in time)
174+
responseParameters.put("access_token", "normalToken");
175+
responseParameters.put("expires_in", "3600");
176+
responseParameters.put("refresh_in", "0");
177+
TestHelper.createTokenRequestMock(httpClientMock, TestHelper.getSuccessfulTokenResponse(responseParameters), 200);
178+
179+
//refresh_in values are in seconds, so we must wait to guarantee it is past the proactive refresh time
180+
TimeUnit.SECONDS.sleep(2);
181+
182+
silentParameters = SilentParameters.builder(Collections.singleton("someScopes"), result.account()).build();
183+
result = cca.acquireTokenSilently(silentParameters).get();
184+
185+
assertRefreshedToken(result, "normalToken", CacheRefreshReason.PROACTIVE_REFRESH, cca.tokenCache.accessTokens.size());
186+
187+
//Force the token to be refreshed
188+
responseParameters.put("access_token", "forcedRefreshToken");
189+
TestHelper.createTokenRequestMock(httpClientMock, TestHelper.getSuccessfulTokenResponse(responseParameters), 200);
190+
191+
silentParameters = SilentParameters.builder(Collections.singleton("someScopes"), result.account()).forceRefresh(true).build();
192+
result = cca.acquireTokenSilently(silentParameters).get();
193+
194+
assertRefreshedToken(result, "forcedRefreshToken", CacheRefreshReason.FORCE_REFRESH, cca.tokenCache.accessTokens.size());
195+
196+
//Finally, force a refresh by setting claims
197+
responseParameters.put("access_token", "claimsToken");
198+
TestHelper.createTokenRequestMock(httpClientMock, TestHelper.getSuccessfulTokenResponse(responseParameters), 200);
199+
200+
silentParameters = SilentParameters.builder(Collections.singleton("someScopes"), result.account()).claims(new ClaimsRequest()).build();
201+
result = cca.acquireTokenSilently(silentParameters).get();
202+
203+
assertRefreshedToken(result, "claimsToken", CacheRefreshReason.CLAIMS, cca.tokenCache.accessTokens.size());
204+
}
205+
206+
//Asserts that there is one expected token in the cache, and that it was refreshed with the expected reason
207+
private void assertRefreshedToken(IAuthenticationResult result, String expectedToken, CacheRefreshReason expectedReason, int cacheSize) {
208+
assertEquals(1, cacheSize);
209+
assertEquals(expectedToken, result.accessToken());
210+
assertEquals(expectedReason, result.metadata().cacheRefreshReason());
211+
}
212+
119213
String readResource(String resource) {
120214
try {
121215
return new String(Files.readAllBytes(Paths.get(getClass().getResource(resource).toURI())));

0 commit comments

Comments
 (0)