// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.aad.msal4j;
import com.azure.json.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Cache used for storing tokens. For more details, see https://aka.ms/msal4j-token-cache
*
* Conditionally thread-safe
*/
public class TokenCache implements ITokenCache {
protected static final int MIN_ACCESS_TOKEN_EXPIRE_IN_SEC = 5 * 60;
private ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* Constructor for token cache
*
* @param tokenCacheAccessAspect {@link ITokenCacheAccessAspect}
*/
public TokenCache(ITokenCacheAccessAspect tokenCacheAccessAspect) {
this();
this.tokenCacheAccessAspect = tokenCacheAccessAspect;
}
/**
* Constructor for token cache
*/
public TokenCache() {
}
Map accessTokens = new LinkedHashMap<>();
Map refreshTokens = new LinkedHashMap<>();
Map idTokens = new LinkedHashMap<>();
Map accounts = new LinkedHashMap<>();
Map appMetadata = new LinkedHashMap<>();
ITokenCacheAccessAspect tokenCacheAccessAspect;
private String serializedCachedSnapshot;
@Override
public void deserialize(String data) {
if (StringHelper.isBlank(data)) {
return;
}
serializedCachedSnapshot = data;
try {
JsonReader jsonReader = JsonProviders.createReader(data);
deserializeFromJson(jsonReader);
} catch (IOException e) {
throw new MsalClientException(e);
}
}
private void deserializeFromJson(JsonReader jsonReader) throws IOException {
lock.writeLock().lock();
try {
jsonReader.readObject(reader -> {
while (reader.nextToken() != JsonToken.END_OBJECT) {
String fieldName = reader.getFieldName();
reader.nextToken();
switch (fieldName) {
case "AccessToken":
deserializeCollection(reader, accessTokens, AccessTokenCacheEntity::fromJson);
break;
case "RefreshToken":
deserializeCollection(reader, refreshTokens, RefreshTokenCacheEntity::fromJson);
break;
case "IdToken":
deserializeCollection(reader, idTokens, IdTokenCacheEntity::fromJson);
break;
case "Account":
deserializeCollection(reader, accounts, AccountCacheEntity::fromJson);
break;
case "AppMetadata":
deserializeCollection(reader, appMetadata, AppMetadataCacheEntity::fromJson);
break;
default:
reader.skipChildren();
break;
}
}
return null;
});
} finally {
lock.writeLock().unlock();
}
}
private void deserializeCollection(
JsonReader reader,
Map targetCollection,
ReadValueCallback deserializer) throws IOException {
reader.readObject(entityReader -> {
while (entityReader.nextToken() != JsonToken.END_OBJECT) {
String key = entityReader.getFieldName();
entityReader.nextToken();
T entity = deserializer.read(entityReader);
targetCollection.put(key, entity);
}
return null;
});
}
@Override
public String serialize() {
lock.readLock().lock();
try {
if (!StringHelper.isBlank(serializedCachedSnapshot)) {
String updatedCache = mergeWithExistingCache();
if (updatedCache != null) {
return updatedCache;
}
}
return serializeToJson();
} finally {
lock.readLock().unlock();
}
}
private String serializeToJson() {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) {
jsonWriter.writeStartObject();
// Write all collections
writeCollection(jsonWriter, "AccessToken", accessTokens);
writeCollection(jsonWriter, "RefreshToken", refreshTokens);
writeCollection(jsonWriter, "IdToken", idTokens);
writeCollection(jsonWriter, "Account", accounts);
writeCollection(jsonWriter, "AppMetadata", appMetadata);
jsonWriter.writeEndObject();
jsonWriter.flush();
return outputStream.toString(StandardCharsets.UTF_8.name());
} catch (IOException e) {
throw new MsalClientException(e);
}
}
private void writeCollection(
JsonWriter jsonWriter,
String collectionName,
Map collection) throws IOException {
jsonWriter.writeFieldName(collectionName);
jsonWriter.writeStartObject();
for (Map.Entry entry : collection.entrySet()) {
jsonWriter.writeFieldName(entry.getKey());
if (entry.getValue() instanceof JsonSerializable) {
((JsonSerializable>) entry.getValue()).toJson(jsonWriter);
}
}
jsonWriter.writeEndObject();
}
private String mergeWithExistingCache() {
try {
// Parse existing cache snapshot
TokenCache updatedCache = new TokenCache();
updatedCache.deserialize(serializedCachedSnapshot);
// Merge current in-memory cache with the snapshot
mergeCache(updatedCache);
// Serialize merged cache
return updatedCache.serializeToJson();
} catch (Exception e) {
return null;
}
}
private void mergeCache(TokenCache targetCache) {
targetCache.accessTokens.putAll(accessTokens);
targetCache.refreshTokens.putAll(refreshTokens);
targetCache.idTokens.putAll(idTokens);
targetCache.accounts.putAll(accounts);
targetCache.appMetadata.putAll(appMetadata);
// Handle removals by removing keys that are not in the current cache
targetCache.accessTokens.keySet().retainAll(accessTokens.keySet());
targetCache.refreshTokens.keySet().retainAll(refreshTokens.keySet());
targetCache.idTokens.keySet().retainAll(idTokens.keySet());
targetCache.accounts.keySet().retainAll(accounts.keySet());
targetCache.appMetadata.keySet().retainAll(appMetadata.keySet());
}
private class CacheAspect implements AutoCloseable {
ITokenCacheAccessContext context;
CacheAspect(ITokenCacheAccessContext context) {
if (tokenCacheAccessAspect != null) {
this.context = context;
tokenCacheAccessAspect.beforeCacheAccess(context);
}
}
@Override
public void close() {
if (tokenCacheAccessAspect != null) {
tokenCacheAccessAspect.afterCacheAccess(context);
}
}
}
void saveTokens(TokenRequestExecutor tokenRequestExecutor, AuthenticationResult authenticationResult, String environment) {
try (CacheAspect cacheAspect = new CacheAspect(
TokenCacheAccessContext.builder().
clientId(tokenRequestExecutor.getMsalRequest().application().clientId()).
tokenCache(this).
hasCacheChanged(true).build())) {
try {
lock.writeLock().lock();
if (!StringHelper.isBlank(authenticationResult.accessToken())) {
AccessTokenCacheEntity atEntity = createAccessTokenCacheEntity
(tokenRequestExecutor, authenticationResult, environment);
accessTokens.put(atEntity.getKey(), atEntity);
}
if (!StringHelper.isBlank(authenticationResult.familyId())) {
AppMetadataCacheEntity appMetadataCacheEntity =
createAppMetadataCacheEntity(tokenRequestExecutor, authenticationResult, environment);
appMetadata.put(appMetadataCacheEntity.getKey(), appMetadataCacheEntity);
}
if (!StringHelper.isBlank(authenticationResult.refreshToken())) {
RefreshTokenCacheEntity rtEntity = createRefreshTokenCacheEntity
(tokenRequestExecutor, authenticationResult, environment);
rtEntity.family_id(authenticationResult.familyId());
refreshTokens.put(rtEntity.getKey(), rtEntity);
}
if (!StringHelper.isBlank(authenticationResult.idToken())) {
IdTokenCacheEntity idTokenEntity = createIdTokenCacheEntity
(tokenRequestExecutor, authenticationResult, environment);
idTokens.put(idTokenEntity.getKey(), idTokenEntity);
AccountCacheEntity accountCacheEntity = authenticationResult.accountCacheEntity();
if(accountCacheEntity!=null) {
accountCacheEntity.environment(environment);
accounts.put(accountCacheEntity.getKey(), accountCacheEntity);
}
}
} finally {
lock.writeLock().unlock();
}
}
}
private static RefreshTokenCacheEntity createRefreshTokenCacheEntity(TokenRequestExecutor tokenRequestExecutor,
AuthenticationResult authenticationResult,
String environmentAlias) {
RefreshTokenCacheEntity rt = new RefreshTokenCacheEntity();
rt.credentialType(CredentialTypeEnum.REFRESH_TOKEN.value());
if (authenticationResult.account() != null) {
rt.homeAccountId(authenticationResult.account().homeAccountId());
}
rt.environment(environmentAlias);
rt.clientId(tokenRequestExecutor.getMsalRequest().application().clientId());
rt.secret(authenticationResult.refreshToken());
if (tokenRequestExecutor.getMsalRequest() instanceof OnBehalfOfRequest) {
OnBehalfOfRequest onBehalfOfRequest = (OnBehalfOfRequest) tokenRequestExecutor.getMsalRequest();
rt.userAssertionHash(onBehalfOfRequest.parameters.userAssertion().getAssertionHash());
}
return rt;
}
private static AccessTokenCacheEntity createAccessTokenCacheEntity(TokenRequestExecutor tokenRequestExecutor,
AuthenticationResult authenticationResult,
String environmentAlias) {
AccessTokenCacheEntity at = new AccessTokenCacheEntity();
at.credentialType(CredentialTypeEnum.ACCESS_TOKEN.value());
if (authenticationResult.account() != null) {
at.homeAccountId(authenticationResult.account().homeAccountId());
}
at.environment(environmentAlias);
at.clientId(tokenRequestExecutor.getMsalRequest().application().clientId());
at.secret(authenticationResult.accessToken());
at.realm(tokenRequestExecutor.tenant);
String scopes = !StringHelper.isBlank(authenticationResult.scopes()) ? authenticationResult.scopes() :
String.join(" ", tokenRequestExecutor.getMsalRequest().msalAuthorizationGrant().getScopes());
at.target(scopes);
if (tokenRequestExecutor.getMsalRequest() instanceof OnBehalfOfRequest) {
OnBehalfOfRequest onBehalfOfRequest = (OnBehalfOfRequest) tokenRequestExecutor.getMsalRequest();
at.userAssertionHash(onBehalfOfRequest.parameters.userAssertion().getAssertionHash());
}
long currTimestampSec = System.currentTimeMillis() / 1000;
at.cachedAt(Long.toString(currTimestampSec));
at.expiresOn(Long.toString(authenticationResult.expiresOn()));
if (authenticationResult.refreshOn() > 0) {
at.refreshOn(Long.toString(authenticationResult.refreshOn()));
}
if (authenticationResult.extExpiresOn() > 0) {
at.extExpiresOn(Long.toString(authenticationResult.extExpiresOn()));
}
return at;
}
private static IdTokenCacheEntity createIdTokenCacheEntity(TokenRequestExecutor tokenRequestExecutor,
AuthenticationResult authenticationResult,
String environmentAlias) {
IdTokenCacheEntity idToken = new IdTokenCacheEntity();
idToken.credentialType(CredentialTypeEnum.ID_TOKEN.value());
if (authenticationResult.account() != null) {
idToken.homeAccountId(authenticationResult.account().homeAccountId());
}
idToken.environment(environmentAlias);
idToken.clientId(tokenRequestExecutor.getMsalRequest().application().clientId());
idToken.secret(authenticationResult.idToken());
idToken.realm(tokenRequestExecutor.tenant);
if (tokenRequestExecutor.getMsalRequest() instanceof OnBehalfOfRequest) {
OnBehalfOfRequest onBehalfOfRequest = (OnBehalfOfRequest) tokenRequestExecutor.getMsalRequest();
idToken.userAssertionHash(onBehalfOfRequest.parameters.userAssertion().getAssertionHash());
}
return idToken;
}
private static AppMetadataCacheEntity createAppMetadataCacheEntity(TokenRequestExecutor tokenRequestExecutor,
AuthenticationResult authenticationResult,
String environmentAlias) {
AppMetadataCacheEntity appMetadataCacheEntity = new AppMetadataCacheEntity();
appMetadataCacheEntity.clientId(tokenRequestExecutor.getMsalRequest().application().clientId());
appMetadataCacheEntity.environment(environmentAlias);
appMetadataCacheEntity.familyId(authenticationResult.familyId());
return appMetadataCacheEntity;
}
Set getAccounts(String clientId) {
try (CacheAspect cacheAspect = new CacheAspect(
TokenCacheAccessContext.builder().
clientId(clientId).
tokenCache(this).
build())) {
try {
lock.readLock().lock();
Map rootAccounts = new HashMap<>();
for (AccountCacheEntity accCached : accounts.values()) {
IdTokenCacheEntity idToken = idTokens.get(getIdTokenKey(
accCached.homeAccountId(),
accCached.environment(),
clientId,
accCached.realm()));
ITenantProfile profile = null;
if (idToken != null) {
Map idTokenClaims = JsonHelper.parseJsonToMap(JsonHelper.getTokenPayloadClaims(idToken.secret));
profile = new TenantProfile(idTokenClaims, accCached.environment());
}
if (rootAccounts.get(accCached.homeAccountId()) == null) {
IAccount acc = accCached.toAccount();
((Account) acc).tenantProfiles = new HashMap<>();
rootAccounts.put(accCached.homeAccountId(), acc);
}
if (profile != null) {
((Account) rootAccounts.get(accCached.homeAccountId())).tenantProfiles.put(accCached.realm(), profile);
}
if (accCached.localAccountId() != null && accCached.homeAccountId().contains(accCached.localAccountId())) {
((Account) rootAccounts.get(accCached.homeAccountId())).username(accCached.username());
}
}
return new HashSet<>(rootAccounts.values());
} finally {
lock.readLock().unlock();
}
}
}
/**
* Returns a String representing a key of a cached ID token, formatted in the same way as {@link IdTokenCacheEntity#getKey}
*
* @return String representing a possible key of a cached ID token
*/
private String getIdTokenKey(String homeAccountId, String environment, String clientId, String realm) {
return String.join(Constants.CACHE_KEY_SEPARATOR,
Arrays.asList(homeAccountId,
environment,
"idtoken", clientId,
realm, "")).toLowerCase();
}
/**
* @return familyId status of application
*/
private String getApplicationFamilyId(String clientId, Set environmentAliases) {
for (AppMetadataCacheEntity data : appMetadata.values()) {
if (data.clientId().equals(clientId) &&
environmentAliases.contains(data.environment()) &&
!StringHelper.isBlank(data.familyId())) {
return data.familyId();
}
}
return null;
}
/**
* @return set of client ids which belong to the family
*/
private Set getFamilyClientIds(String familyId, Set environmentAliases) {
return appMetadata.values().stream().filter
(appMetadata -> environmentAliases.contains(appMetadata.environment()) &&
familyId.equals(appMetadata.familyId())
).map(AppMetadataCacheEntity::clientId).collect(Collectors.toSet());
}
/**
* Remove all cache entities related to account, including account cache entity
*
* @param clientId client id
* @param account account
*/
void removeAccount(String clientId, IAccount account) {
try (CacheAspect cacheAspect = new CacheAspect(
TokenCacheAccessContext.builder().
clientId(clientId).
tokenCache(this).
hasCacheChanged(true).
build())) {
try {
lock.writeLock().lock();
removeAccount(account);
} finally {
lock.writeLock().unlock();
}
}
}
private void removeAccount(IAccount account) {
Predicate> credentialToRemovePredicate =
e -> !StringHelper.isBlank(e.getValue().homeAccountId()) &&
!StringHelper.isBlank(e.getValue().environment()) &&
e.getValue().homeAccountId().equals(account.homeAccountId());
accessTokens.entrySet().removeIf(credentialToRemovePredicate);
refreshTokens.entrySet().removeIf(credentialToRemovePredicate);
idTokens.entrySet().removeIf(credentialToRemovePredicate);
accounts.entrySet().removeIf(
e -> !StringHelper.isBlank(e.getValue().homeAccountId()) &&
!StringHelper.isBlank(e.getValue().environment()) &&
e.getValue().homeAccountId().equals(account.homeAccountId()));
}
private boolean isMatchingScopes(AccessTokenCacheEntity accessTokenCacheEntity, Set scopes) {
Set accessTokenCacheEntityScopes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
accessTokenCacheEntityScopes.addAll
(Arrays.asList(accessTokenCacheEntity.target().split(Constants.SCOPES_SEPARATOR)));
return accessTokenCacheEntityScopes.containsAll(scopes);
}
private boolean userAssertionHashMatches(Credential credential, String userAssertionHash) {
if (userAssertionHash == null) {
return true;
}
return credential.userAssertionHash() != null &&
credential.userAssertionHash().equalsIgnoreCase(userAssertionHash);
}
private boolean userAssertionHashMatches(AccountCacheEntity accountCacheEntity, String userAssertionHash) {
if (userAssertionHash == null) {
return true;
}
return accountCacheEntity.userAssertionHash() != null &&
accountCacheEntity.userAssertionHash().equalsIgnoreCase(userAssertionHash);
}
private Optional getAccessTokenCacheEntity(
IAccount account,
Authority authority,
Set scopes,
String clientId,
Set environmentAliases) {
return accessTokens.values().stream().filter(
accessToken ->
accessToken.homeAccountId != null &&
accessToken.homeAccountId.equals(account.homeAccountId()) &&
environmentAliases.contains(accessToken.environment) &&
accessToken.realm.equals(authority.tenant()) &&
accessToken.clientId.equals(clientId) &&
isMatchingScopes(accessToken, scopes)
).findAny();
}
private Optional getApplicationAccessTokenCacheEntity(
Authority authority,
Set scopes,
String clientId,
Set environmentAliases,
String userAssertionHash) {
long currTimeStampSec = new Date().getTime() / 1000;
return accessTokens.values().stream().filter(
accessToken ->
userAssertionHashMatches(accessToken, userAssertionHash) &&
environmentAliases.contains(accessToken.environment) &&
Long.parseLong(accessToken.expiresOn()) > currTimeStampSec + MIN_ACCESS_TOKEN_EXPIRE_IN_SEC &&
accessToken.realm.equals(authority.tenant()) &&
accessToken.clientId.equals(clientId) &&
isMatchingScopes(accessToken, scopes))
.findAny();
}
private Optional getIdTokenCacheEntity(
IAccount account,
Authority authority,
String clientId,
Set environmentAliases) {
return idTokens.values().stream().filter(
idToken ->
idToken.homeAccountId.equals(account.homeAccountId()) &&
environmentAliases.contains(idToken.environment) &&
idToken.realm.equals(authority.tenant()) &&
idToken.clientId.equals(clientId)
).findAny();
}
private Optional getIdTokenCacheEntity(
Authority authority,
String clientId,
Set environmentAliases,
String userAssertionHash) {
return idTokens.values().stream().filter(
idToken ->
userAssertionHashMatches(idToken, userAssertionHash) &&
environmentAliases.contains(idToken.environment) &&
idToken.realm.equals(authority.tenant()) &&
idToken.clientId.equals(clientId)
).findAny();
}
private Optional getRefreshTokenCacheEntity(
String clientId,
Set environmentAliases,
String userAssertionHash) {
return refreshTokens.values().stream().filter(
refreshToken ->
userAssertionHashMatches(refreshToken, userAssertionHash) &&
environmentAliases.contains(refreshToken.environment) &&
refreshToken.clientId.equals(clientId)
).findAny();
}
private Optional getRefreshTokenCacheEntity(
IAccount account,
String clientId,
Set environmentAliases) {
return refreshTokens.values().stream().filter(
refreshToken ->
refreshToken.homeAccountId != null &&
refreshToken.homeAccountId.equals(account.homeAccountId()) &&
environmentAliases.contains(refreshToken.environment) &&
refreshToken.clientId.equals(clientId)
).findAny();
}
private Optional getAccountCacheEntity(
IAccount account,
Set environmentAliases) {
return accounts.values().stream().filter(
acc ->
acc.homeAccountId.equals(account.homeAccountId()) &&
environmentAliases.contains(acc.environment)
).findAny();
}
private Optional getAccountCacheEntity(
Set environmentAliases,
String userAssertionHash) {
return accounts.values().stream().filter(
acc -> userAssertionHashMatches(acc, userAssertionHash) &&
environmentAliases.contains(acc.environment)
).findAny();
}
private Optional getAnyFamilyRefreshTokenCacheEntity
(IAccount account, Set environmentAliases) {
return refreshTokens.values().stream().filter
(refreshToken -> refreshToken.homeAccountId.equals(account.homeAccountId()) &&
environmentAliases.contains(refreshToken.environment) &&
refreshToken.isFamilyRT()
).findAny();
}
AuthenticationResult getCachedAuthenticationResult(
IAccount account,
Authority authority,
Set scopes,
String clientId) {
AuthenticationResult.AuthenticationResultBuilder builder = AuthenticationResult.builder();
Set environmentAliases = AadInstanceDiscoveryProvider.getAliases(account.environment());
try (CacheAspect cacheAspect = new CacheAspect(
TokenCacheAccessContext.builder().
clientId(clientId).
tokenCache(this).
account(account).
build())) {
try {
lock.readLock().lock();
Optional accountCacheEntity =
getAccountCacheEntity(account, environmentAliases);
Optional atCacheEntity =
getAccessTokenCacheEntity(account, authority, scopes, clientId, environmentAliases);
Optional idTokenCacheEntity =
getIdTokenCacheEntity(account, authority, clientId, environmentAliases);
Optional rtCacheEntity;
if (!StringHelper.isBlank(getApplicationFamilyId(clientId, environmentAliases))) {
rtCacheEntity = getAnyFamilyRefreshTokenCacheEntity(account, environmentAliases);
if (!rtCacheEntity.isPresent()) {
rtCacheEntity = getRefreshTokenCacheEntity(account, clientId, environmentAliases);
}
} else {
rtCacheEntity = getRefreshTokenCacheEntity(account, clientId, environmentAliases);
if (!rtCacheEntity.isPresent()) {
rtCacheEntity = getAnyFamilyRefreshTokenCacheEntity(account, environmentAliases);
}
}
if (atCacheEntity.isPresent()) {
builder.
environment(atCacheEntity.get().environment).
accessToken(atCacheEntity.get().secret).
expiresOn(Long.parseLong(atCacheEntity.get().expiresOn()));
if (atCacheEntity.get().refreshOn() != null) {
builder.refreshOn(Long.parseLong(atCacheEntity.get().refreshOn()));
}
} else {
builder.environment(authority.host());
}
idTokenCacheEntity.ifPresent(tokenCacheEntity -> builder.idToken(tokenCacheEntity.secret));
rtCacheEntity.ifPresent(refreshTokenCacheEntity ->
builder.refreshToken(refreshTokenCacheEntity.secret));
accountCacheEntity.ifPresent(builder::accountCacheEntity);
} finally {
lock.readLock().unlock();
}
}
return builder.build();
}
AuthenticationResult getCachedAuthenticationResult(
Authority authority,
Set scopes,
String clientId,
IUserAssertion assertion) {
AuthenticationResult.AuthenticationResultBuilder builder = AuthenticationResult.builder();
Set environmentAliases = AadInstanceDiscoveryProvider.getAliases(authority.host);
builder.environment(authority.host());
try (CacheAspect cacheAspect = new CacheAspect(
TokenCacheAccessContext.builder().
clientId(clientId).
tokenCache(this).
build())) {
try {
lock.readLock().lock();
String userAssertionHash = assertion == null ? null : assertion.getAssertionHash();
Optional accountCacheEntity =
getAccountCacheEntity(environmentAliases, userAssertionHash);
accountCacheEntity.ifPresent(builder::accountCacheEntity);
Optional atCacheEntity =
getApplicationAccessTokenCacheEntity(authority, scopes, clientId, environmentAliases, userAssertionHash);
if (atCacheEntity.isPresent()) {
builder.
accessToken(atCacheEntity.get().secret).
expiresOn(Long.parseLong(atCacheEntity.get().expiresOn()));
if (atCacheEntity.get().refreshOn() != null) {
builder.refreshOn(Long.parseLong(atCacheEntity.get().refreshOn()));
}
}
Optional idTokenCacheEntity =
getIdTokenCacheEntity(authority, clientId, environmentAliases, userAssertionHash);
idTokenCacheEntity.ifPresent(tokenCacheEntity -> builder.idToken(tokenCacheEntity.secret));
Optional rtCacheEntity = getRefreshTokenCacheEntity(clientId, environmentAliases, userAssertionHash);
rtCacheEntity.ifPresent(refreshTokenCacheEntity ->
builder.refreshToken(refreshTokenCacheEntity.secret));
} finally {
lock.readLock().unlock();
}
return builder.build();
}
}
}