Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ internal MsalAccessTokenCacheItem(
string tenantId,
string homeAccountId,
string keyId = null,
string oboCacheKey = null)
string oboCacheKey = null,
string oboAssertionHash = null)
: this(
scopes: response.Scope, // token providers send pre-sorted (alphabetically) scopes
cachedAt: DateTimeOffset.UtcNow,
Expand All @@ -45,11 +46,12 @@ internal MsalAccessTokenCacheItem(
RawClientInfo = response.ClientInfo;
HomeAccountId = homeAccountId;
OboCacheKey = oboCacheKey;
OboAssertionHash = oboAssertionHash;

InitCacheKey();
}

internal /* for test */ MsalAccessTokenCacheItem(
internal MsalAccessTokenCacheItem(
string preferredCacheEnv,
string clientId,
string scopes,
Expand All @@ -63,7 +65,8 @@ internal MsalAccessTokenCacheItem(
string keyId = null,
DateTimeOffset? refreshOn = null,
string tokenType = StorageJsonValues.TokenTypeBearer,
string oboCacheKey = null)
string oboCacheKey = null,
string oboAssertionHash = null)
: this(scopes, cachedAt, expiresOn, extendedExpiresOn, refreshOn, tenantId, keyId, tokenType)
{
Environment = preferredCacheEnv;
Expand All @@ -72,6 +75,7 @@ internal MsalAccessTokenCacheItem(
RawClientInfo = rawClientInfo;
HomeAccountId = homeAccountId;
OboCacheKey = oboCacheKey;
OboAssertionHash = oboAssertionHash;

InitCacheKey();
}
Expand Down Expand Up @@ -121,7 +125,8 @@ internal MsalAccessTokenCacheItem WithExpiresOn(DateTimeOffset expiresOn)
KeyId,
RefreshOn,
TokenType,
OboCacheKey);
OboCacheKey,
OboAssertionHash);

return newAtItem;
}
Expand Down Expand Up @@ -186,10 +191,16 @@ internal string TenantId

/// <summary>
/// Used to find the token in the cache.
/// Can be a token assertion hash (normal OBO flow) or a user provided key (long-running OBO flow).
/// Can be a token assertion hash (normal OBO flow) or a user-provided key (long-running OBO flow).
/// </summary>
internal string OboCacheKey { get; set; }

/// <summary>
/// Only used in InitiateLongRunningProcessInWebApi to compare request and cached assertions.
/// Always set to the assertion hash.
/// </summary>
internal string OboAssertionHash { get; set; }

/// <summary>
/// Used when the token is bound to a public / private key pair which is identified by a key id (kid).
/// Currently used by PoP tokens
Expand Down Expand Up @@ -242,7 +253,8 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
extendedExpiresOnUnixTimestamp = ext_expires_on;
}
string tenantId = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.Realm);
string oboCacheKey = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.UserAssertionHash);
string oboCacheKey = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.UserAssertionHashCacheKey);
string oboAssertionHash = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.OboAssertionHash);
string keyId = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.KeyId);
string tokenType = JsonHelper.ExtractExistingOrDefault<string>(j, StorageJsonKeys.TokenType) ?? StorageJsonValues.TokenTypeBearer;
string scopes = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.Target);
Expand All @@ -258,6 +270,7 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
tokenType: tokenType);

item.OboCacheKey = oboCacheKey;
item.OboAssertionHash = oboAssertionHash;
item.PopulateFieldsFromJObject(j);

item.InitCacheKey();
Expand All @@ -272,7 +285,8 @@ internal override JObject ToJObject()
var extExpiresUnixTimestamp = DateTimeHelpers.DateTimeToUnixTimestamp(ExtendedExpiresOn);
SetItemIfValueNotNull(json, StorageJsonKeys.Realm, TenantId);
SetItemIfValueNotNull(json, StorageJsonKeys.Target, ScopeString);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, OboCacheKey);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHashCacheKey, OboCacheKey);
SetItemIfValueNotNull(json, StorageJsonKeys.OboAssertionHash, OboAssertionHash);
SetItemIfValueNotNull(json, StorageJsonKeys.CachedAt, DateTimeHelpers.DateTimeToUnixTimestamp(CachedAt));
SetItemIfValueNotNull(json, StorageJsonKeys.ExpiresOn, DateTimeHelpers.DateTimeToUnixTimestamp(ExpiresOn));
SetItemIfValueNotNull(json, StorageJsonKeys.ExtendedExpiresOn, extExpiresUnixTimestamp);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ internal static MsalRefreshTokenCacheItem FromJObject(JObject j)
{
var item = new MsalRefreshTokenCacheItem();
item.FamilyId = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.FamilyId);
item.OboCacheKey = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.UserAssertionHash);
item.OboCacheKey = JsonHelper.ExtractExistingOrEmptyString(j, StorageJsonKeys.UserAssertionHashCacheKey);

item.PopulateFieldsFromJObject(j);
item.InitCacheKey();
Expand All @@ -168,7 +168,7 @@ internal override JObject ToJObject()
{
var json = base.ToJObject();
SetItemIfValueNotNull(json, StorageJsonKeys.FamilyId, FamilyId);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, OboCacheKey);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHashCacheKey, OboCacheKey);
return json;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ internal static class StorageJsonKeys
public const string WamAccountIds = "wam_account_ids";

// todo(cache): this needs to be added to the spec. needed for OBO flow on .NET.
public const string UserAssertionHash = "user_assertion_hash";
public const string UserAssertionHashCacheKey = "user_assertion_hash";
public const string OboAssertionHash = "user_assertion_hash_only";

// previous versions of MSAL used "ext_expires_on" instead of the correct "extended_expires_on".
// this is here for back compatibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok

CacheRefreshReason cacheInfoTelemetry = CacheRefreshReason.NotApplicable;

//Check if initiating a long running process
if (AuthenticationRequestParameters.ApiId == ApiEvent.ApiIds.InitiateLongRunningObo)
{
//Long running process should not use cached tokens
logger.Info("[OBO Request] Initiating long running process. Fetching OBO token from ESTS.");
return await FetchNewAccessTokenAsync(cancellationToken).ConfigureAwait(false);
}

if (!_onBehalfOfParameters.ForceRefresh && string.IsNullOrEmpty(AuthenticationRequestParameters.Claims))
{
// look for access token in the cache first.
Expand All @@ -64,6 +56,13 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
cachedAccessToken = await CacheManager.FindAccessTokenAsync().ConfigureAwait(false);
}

if (cachedAccessToken != null && AuthenticationRequestParameters.ApiId == ApiEvent.ApiIds.InitiateLongRunningObo &&
!AuthenticationRequestParameters.UserAssertion.AssertionHash.Equals(cachedAccessToken.OboAssertionHash, System.StringComparison.Ordinal))
{
logger.Info("[OBO request] InitiateLongRunningProcessInWebApi found cached token with a different assertion; fetching new tokens.");
cachedAccessToken = null;
}

if (cachedAccessToken != null)
{
var cachedIdToken = await CacheManager.GetIdTokenCacheItemAsync(cachedAccessToken).ConfigureAwait(false);
Expand Down Expand Up @@ -138,11 +137,15 @@ protected override async Task<AuthenticationResult> ExecuteAsync(CancellationTok
private async Task<AuthenticationResult> RefreshRtOrFetchNewAccessTokenAsync(CancellationToken cancellationToken)
{
var logger = AuthenticationRequestParameters.RequestContext.Logger;

if (ApiEvent.IsLongRunningObo(AuthenticationRequestParameters.ApiId))
{
AuthenticationRequestParameters.RequestContext.Logger.Info("[OBO request] Long-running OBO flow, trying to refresh using a refresh token flow.");

// InitiateLongRunningProcessInWebApi retrieves tokens only with assertion, not by refresh token.
// The reason why we don't use RT for InitiateLongRunningProcessInWebApi is because of https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3825.
// If tokens are revoked in AAD, refreshing with this cached RT will throw an AAD exception.
// Since we already have a user assertion, use it to fetch new tokens via OBO grant.
// The expectation is that this use assertion is newer than the one that was used to acquire cached RT.
if (AuthenticationRequestParameters.ApiId == ApiEvent.ApiIds.AcquireTokenInLongRunningObo)
{
AuthenticationRequestParameters.RequestContext.Logger.Info("[OBO request] AcquireTokenInLongRunningProcess, trying to refresh using an refresh token flow.");

// Look for a refresh token
MsalRefreshTokenCacheItem cachedRefreshToken = await CacheManager.FindRefreshTokenAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -170,17 +173,12 @@ private async Task<AuthenticationResult> RefreshRtOrFetchNewAccessTokenAsync(Can
return await CacheTokenResponseAndCreateAuthenticationResultAsync(msalTokenResponse).ConfigureAwait(false);
}

if (AuthenticationRequestParameters.ApiId == ApiEvent.ApiIds.AcquireTokenInLongRunningObo)
{
AuthenticationRequestParameters.RequestContext.Logger.Error("[OBO request] AcquireTokenInLongRunningProcess was called and no access or refresh tokens were found in the cache.");
throw new MsalClientException(MsalError.OboCacheKeyNotInCacheError, MsalErrorMessage.OboCacheKeyNotInCache);
}

AuthenticationRequestParameters.RequestContext.Logger.Info("[OBO request] No Refresh Token was found in the cache. Fetching OBO token from ESTS");
AuthenticationRequestParameters.RequestContext.Logger.Error("[OBO request] AcquireTokenInLongRunningProcess was called and no access or refresh tokens were found in the cache.");
throw new MsalClientException(MsalError.OboCacheKeyNotInCacheError, MsalErrorMessage.OboCacheKeyNotInCache);
}
else
{
logger.Info("[OBO request] Normal OBO flow, skipping to fetching access token via OBO flow.");
logger.Info("[OBO request] Fetching access token via OBO flow.");
}

return await FetchNewAccessTokenAsync(cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async Task<Tuple<MsalAccessTokenCacheItem, MsalIdTokenCacheItem, Account>> IToke

string suggestedWebCacheKey = CacheKeyFactory.GetExternalCacheKeyFromResponse(requestParams, homeAccountId);

// token could be comming from a different cloud than the one configured
// token could be coming from a different cloud than the one configured
if (requestParams.AppConfig.MultiCloudSupportEnabled && !string.IsNullOrEmpty(response.AuthorityUrl))
{
var url = new Uri(response.AuthorityUrl);
Expand All @@ -79,7 +79,8 @@ async Task<Tuple<MsalAccessTokenCacheItem, MsalIdTokenCacheItem, Account>> IToke
tenantId,
homeAccountId,
requestParams.AuthenticationScheme.KeyId,
CacheKeyFactory.GetOboKey(requestParams.LongRunningOboCacheKey, requestParams.UserAssertion));
CacheKeyFactory.GetOboKey(requestParams.LongRunningOboCacheKey, requestParams.UserAssertion),
requestParams.UserAssertion?.AssertionHash);
}

if (!string.IsNullOrEmpty(response.RefreshToken))
Expand Down
14 changes: 7 additions & 7 deletions tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ public static string GetFociTokenResponse()
"\"trace_id\":\"dd25f4fb-3e8d-458e-90e7-179524ce0000\",\"correlation_id\":" +
"\"f11508ab-067f-40d4-83cb-ccc67bf57e45\"}";

public static string GetDefaultTokenResponse(string accessToken = TestConstants.ATSecret)
public static string GetDefaultTokenResponse(string accessToken = TestConstants.ATSecret, string refreshToken = TestConstants.RTSecret)
{
return
"{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + accessToken + "\"" +
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
",\"refresh_token\":\"" + refreshToken + "\",\"client_info\"" +
":\"" + CreateClientInfo() + "\",\"id_token\"" +
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) + "\"}";
}
Expand All @@ -69,7 +69,7 @@ public static string GetPopTokenResponse()
return
"{\"token_type\":\"pop\",\"expires_in\":\"3599\",\"scope\":" +
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + TestConstants.ATSecret + "\"" +
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
",\"refresh_token\":\"" + TestConstants.RTSecret + "\",\"client_info\"" +
":\"" + CreateClientInfo() + "\",\"id_token\"" +
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
"\",\"id_token_expires_in\":\"3600\"}";
Expand All @@ -80,7 +80,7 @@ public static string GetHybridSpaTokenResponse(string spaCode)
return
"{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + TestConstants.ATSecret + "\"" +
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
",\"refresh_token\":\"" + TestConstants.RTSecret + "\",\"client_info\"" +
":\"" + CreateClientInfo() + "\",\"id_token\"" +
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
"\",\"spa_code\":\"" + spaCode + "\"" +
Expand All @@ -92,7 +92,7 @@ public static string GetBridgedHybridSpaTokenResponse(string spaAccountId)
return
"{\"token_type\":\"Bearer\",\"expires_in\":\"3599\",\"refresh_in\":\"2400\",\"scope\":" +
"\"r1/scope1 r1/scope2\",\"access_token\":\"" + TestConstants.ATSecret + "\"" +
",\"refresh_token\":\"" + Guid.NewGuid() + "\",\"client_info\"" +
",\"refresh_token\":\"" + TestConstants.RTSecret + "\",\"client_info\"" +
":\"" + CreateClientInfo() + "\",\"id_token\"" +
":\"" + CreateIdToken(TestConstants.UniqueId, TestConstants.DisplayableId) +
"\",\"spa_accountId\":\"" + spaAccountId + "\"" +
Expand Down Expand Up @@ -193,10 +193,10 @@ public static HttpResponseMessage CreateSuccessTokenResponseMessage(
scopes, idToken, clientInfo));
}

public static HttpResponseMessage CreateSuccessTokenResponseMessage(bool foci = false, string accessToken = TestConstants.ATSecret)
public static HttpResponseMessage CreateSuccessTokenResponseMessage(bool foci = false, string accessToken = TestConstants.ATSecret, string refreshToken = TestConstants.RTSecret)
{
return CreateSuccessResponseMessage(
foci ? GetFociTokenResponse() : GetDefaultTokenResponse(accessToken));
foci ? GetFociTokenResponse() : GetDefaultTokenResponse(accessToken, refreshToken));
}

public static HttpResponseMessage CreateSuccessTokenResponseMessageWithUid(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ internal static void PopulateCache(
extendedAccessTokenExpiresOn,
clientInfo,
homeAccId,
oboCacheKey: userAssertionHash);
oboCacheKey: userAssertionHash,
oboAssertionHash: userAssertionHash);

// add access token
accessor.SaveAccessToken(atItem);
Expand Down Expand Up @@ -385,7 +386,7 @@ public static void ExpireAccessToken(ITokenCacheInternal tokenCache, MsalAccessT

public static MsalAccessTokenCacheItem WithRefreshOn(this MsalAccessTokenCacheItem atItem, DateTimeOffset? refreshOn)
{
MsalAccessTokenCacheItem newAtItem = new MsalAccessTokenCacheItem(
return new MsalAccessTokenCacheItem(
atItem.Environment,
atItem.ClientId,
atItem.ScopeString,
Expand All @@ -399,14 +400,13 @@ public static MsalAccessTokenCacheItem WithRefreshOn(this MsalAccessTokenCacheIt
atItem.KeyId,
refreshOn,
atItem.TokenType,
atItem.OboCacheKey);

return newAtItem;
atItem.OboCacheKey,
atItem.OboAssertionHash);
}

public static MsalAccessTokenCacheItem WithUserAssertion(this MsalAccessTokenCacheItem atItem, string assertion)
{
MsalAccessTokenCacheItem newAtItem = new MsalAccessTokenCacheItem(
return new MsalAccessTokenCacheItem(
atItem.Environment,
atItem.ClientId,
atItem.ScopeString,
Expand All @@ -420,9 +420,8 @@ public static MsalAccessTokenCacheItem WithUserAssertion(this MsalAccessTokenCac
atItem.KeyId,
atItem.RefreshOn,
atItem.TokenType,
assertion,
assertion);

return newAtItem;
}

public static void UpdateUserAssertions(ConfidentialClientApplication app)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ public async Task AcquireTokenByObo_LongRunningThenNormalObo_WithTheSameKey_Test
// Expire AT
TokenCacheHelper.ExpireAllAccessTokens(cca.UserTokenCacheInternal);

// InitiateLR - AT from IdP via RT flow(new AT, RT cached)
// InitiateLR - AT from IdP via OBO flow (new AT, RT cached)
result = await cca.InitiateLongRunningProcessInWebApi(s_scopes, userAuthResult.AccessToken, ref oboCacheKey)
.ExecuteAsync().ConfigureAwait(false);

Expand Down Expand Up @@ -452,11 +452,11 @@ public async Task AcquireTokenByObo_NormalOboThenLongRunningInitiate_WithTheSame
Assert.AreEqual(1, cca.UserTokenCacheInternal.Accessor.GetAllAccessTokens().Count);
Assert.AreEqual(0, cca.UserTokenCacheInternal.Accessor.GetAllRefreshTokens().Count);

// InitiateLR - AT from IdentityProvider
// InitiateLR - AT from cache
result = await cca.InitiateLongRunningProcessInWebApi(s_scopes, userAuthResult.AccessToken, ref oboCacheKey)
.ExecuteAsync().ConfigureAwait(false);

Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);
Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);

// Expire AT
TokenCacheHelper.ExpireAllAccessTokens(cca.UserTokenCacheInternal);
Expand Down
Loading