Skip to content

Commit

Permalink
Add public API for long-running OBO methods with custom cache key and…
Browse files Browse the repository at this point in the history
… do not use refresh token for normal OBO (#2820)

* Add OBO cache key that can be used to find a token instead of a user assertion.

* Rename UserAssertionHash to OboCacheKey and logic to set it. Add method comments.

* Add tests. Minor fix in cache filtering.

* Update the public API, update related tests.

* Fix.

* Update logic if token in cache or not with the OBO cache key provided and throw errors.

* Fix merge issues.

* Create new ILongRunningWebApi interface. Fix how cache key factory creates OBO key.

* Nits: add comments, refactor.

* Remove RT from token response for normal OBO flow.

* Add mock in-memory partitioned cache.

* Fix tests.

* PR feedback: Rename OboCacheKey to LongRunningOboCacheKey; add logging.

* Add tests.

* Add exception comment to the public API methods. Remove exception if Initiate is called with already existing key. Update tests.

* Add tests for combinations of long-running and normal OBO calls.

* Nits. Add aka.ms link. Fix comments.
  • Loading branch information
pmaytak authored Nov 16, 2021
1 parent ff7abc5 commit bbd6eee
Show file tree
Hide file tree
Showing 41 changed files with 1,531 additions and 230 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,54 @@ internal AcquireTokenOnBehalfOfParameterBuilder(IConfidentialClientApplicationEx

internal static AcquireTokenOnBehalfOfParameterBuilder Create(
IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
IEnumerable<string> scopes,
IEnumerable<string> scopes,
UserAssertion userAssertion)
{
return new AcquireTokenOnBehalfOfParameterBuilder(confidentialClientApplicationExecutor)
.WithScopes(scopes)
.WithUserAssertion(userAssertion);
}

internal static AcquireTokenOnBehalfOfParameterBuilder Create(
IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
IEnumerable<string> scopes,
UserAssertion userAssertion,
string cacheKey)
{
return new AcquireTokenOnBehalfOfParameterBuilder(confidentialClientApplicationExecutor)
.WithScopes(scopes)
.WithUserAssertion(userAssertion)
.WithCacheKey(cacheKey);
}

internal static AcquireTokenOnBehalfOfParameterBuilder Create(
IConfidentialClientApplicationExecutor confidentialClientApplicationExecutor,
IEnumerable<string> scopes,
string cacheKey)
{
return new AcquireTokenOnBehalfOfParameterBuilder(confidentialClientApplicationExecutor)
.WithScopes(scopes)
.WithCacheKey(cacheKey);
}

private AcquireTokenOnBehalfOfParameterBuilder WithUserAssertion(UserAssertion userAssertion)
{
CommonParameters.AddApiTelemetryFeature(ApiTelemetryFeature.WithUserAssertion);
Parameters.UserAssertion = userAssertion;
return this;
}

/// <summary>
/// Specifies a key by which to look up the token in the cache instead of searching by an assertion.
/// </summary>
/// <param name="cacheKey">Key by which to look up the token in the cache</param>
/// <returns>A builder enabling you to add optional parameters before executing the token request</returns>
private AcquireTokenOnBehalfOfParameterBuilder WithCacheKey(string cacheKey)
{
Parameters.LongRunningOboCacheKey = cacheKey ?? throw new ArgumentNullException(nameof(cacheKey));
return this;
}

/// <summary>
/// Specifies if the x5c claim (public key of the certificate) should be sent to the STS.
/// Sending the x5c enables application developers to achieve easy certificate roll-over in Azure AD:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Instance;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Requests;

Expand Down Expand Up @@ -42,7 +41,7 @@ public async Task<AuthenticationResult> ExecuteAsync(
var handler = new ConfidentialAuthCodeRequest(
ServiceBundle,
requestParams,
authorizationCodeParameters);
authorizationCodeParameters);

return await handler.RunAsync(cancellationToken).ConfigureAwait(false);
}
Expand Down Expand Up @@ -83,6 +82,7 @@ public async Task<AuthenticationResult> ExecuteAsync(

requestParams.SendX5C = onBehalfOfParameters.SendX5C ?? false;
requestParams.UserAssertion = onBehalfOfParameters.UserAssertion;
requestParams.LongRunningOboCacheKey = onBehalfOfParameters.LongRunningOboCacheKey;

var handler = new OnBehalfOfRequest(
ServiceBundle,
Expand Down Expand Up @@ -113,7 +113,7 @@ public async Task<Uri> ExecuteAsync(
requestParameters.RedirectUri = new Uri(authorizationRequestUrlParameters.RedirectUri);
}

await requestParameters.AuthorityManager.RunInstanceDiscoveryAndValidationAsync().ConfigureAwait(false);
await requestParameters.AuthorityManager.RunInstanceDiscoveryAndValidationAsync().ConfigureAwait(false);
var handler = new AuthCodeRequestComponent(
requestParameters,
authorizationRequestUrlParameters.ToInteractiveParameters());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ namespace Microsoft.Identity.Client.ApiConfig.Parameters
{
internal class AcquireTokenOnBehalfOfParameters : AbstractAcquireTokenConfidentialClientParameters, IAcquireTokenParameters
{
/// <remarks>
/// User assertion is null when <see cref="ILongRunningWebApi.AcquireTokenInLongRunningProcess"/> is called.
/// </remarks>
public UserAssertion UserAssertion { get; set; }

/// <summary>
/// User-provided cache key for long-running OBO flow.
/// </summary>
public string LongRunningOboCacheKey { get; set; }
public bool ForceRefresh { get; set; }

/// <inheritdoc />
Expand All @@ -19,6 +25,8 @@ public void LogParameters(ICoreLogger logger)
builder.AppendLine("=== OnBehalfOfParameters ===");
builder.AppendLine("SendX5C: " + SendX5C);
builder.AppendLine("ForceRefresh: " + ForceRefresh);
builder.AppendLine("UserAssertion set: " + (UserAssertion != null));
builder.AppendLine("LongRunningOboCacheKey set: " + !string.IsNullOrWhiteSpace(LongRunningOboCacheKey));
logger.Info(builder.ToString());
}
}
Expand Down
22 changes: 13 additions & 9 deletions src/client/Microsoft.Identity.Client/Cache/CacheKeyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private static bool GetOboOrAppKey(AuthenticationRequestParameters requestParame
{
if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenOnBehalfOf)
{
key = requestParameters.UserAssertion.AssertionHash;
key = GetOboKey(requestParameters.LongRunningOboCacheKey, requestParameters.UserAssertion);
return true;
}

Expand All @@ -90,21 +90,25 @@ public static string GetClientCredentialKey(string clientId, string tenantId)
return $"{clientId}_{tenantId}_AppTokenCache";
}

public static string GetOboKey(string oboCacheKey, UserAssertion userAssertion)
{
return !string.IsNullOrEmpty(oboCacheKey) ? oboCacheKey : userAssertion?.AssertionHash;
}

public static string GetOboKey(string oboCacheKey, string homeAccountId)
{
return !string.IsNullOrEmpty(oboCacheKey) ? oboCacheKey : homeAccountId;
}

public static string GetKeyFromCachedItem(MsalAccessTokenCacheItem accessTokenCacheItem)
{
string partitionKey = !string.IsNullOrEmpty(accessTokenCacheItem.UserAssertionHash) ?
accessTokenCacheItem.UserAssertionHash :
accessTokenCacheItem.HomeAccountId;

string partitionKey = GetOboKey(accessTokenCacheItem.OboCacheKey, accessTokenCacheItem.HomeAccountId);
return partitionKey;
}

public static string GetKeyFromCachedItem(MsalRefreshTokenCacheItem refreshTokenCacheItem)
{
string partitionKey = !string.IsNullOrEmpty(refreshTokenCacheItem.UserAssertionHash) ?
refreshTokenCacheItem.UserAssertionHash :
refreshTokenCacheItem.HomeAccountId;

string partitionKey = GetOboKey(refreshTokenCacheItem.OboCacheKey, refreshTokenCacheItem.HomeAccountId);
return partitionKey;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private async Task RefreshCacheForReadOperationsAsync(CacheEvent.TokenTypes cach
TokenType = cacheEventType
};

_requestParams.RequestContext.Logger.Verbose($"[Cache Session Manager] Enterering the cache semaphore. { TokenCacheInternal.Semaphore.GetCurrentCountLogMessage()}");
_requestParams.RequestContext.Logger.Verbose($"[Cache Session Manager] Entering the cache semaphore. { TokenCacheInternal.Semaphore.GetCurrentCountLogMessage()}");
await TokenCacheInternal.Semaphore.WaitAsync(_requestParams.RequestContext.UserCancellationToken).ConfigureAwait(false);
_requestParams.RequestContext.Logger.Verbose("[Cache Session Manager] Entered cache semaphore");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal MsalAccessTokenCacheItem(
string tenantId,
string homeAccountId,
string keyId = null,
string assertionHash = null)
string oboCacheKey = null)
: this(
scopes: response.Scope, // token providers send pre-sorted (alphabetically) scopes
cachedAt: DateTimeOffset.UtcNow,
Expand All @@ -37,7 +37,7 @@ internal MsalAccessTokenCacheItem(
Secret = response.AccessToken;
RawClientInfo = response.ClientInfo;
HomeAccountId = homeAccountId;
UserAssertionHash = assertionHash;
OboCacheKey = oboCacheKey;
}

internal /* for test */ MsalAccessTokenCacheItem(
Expand All @@ -54,15 +54,15 @@ internal MsalAccessTokenCacheItem(
string keyId = null,
DateTimeOffset? refreshOn = null,
string tokenType = StorageJsonValues.TokenTypeBearer,
string userAssertionHash = null)
string oboCacheKey = null)
: this(scopes, cachedAt, expiresOn, extendedExpiresOn, refreshOn, tenantId, keyId, tokenType)
{
Environment = preferredCacheEnv;
ClientId = clientId;
Secret = secret;
RawClientInfo = rawClientInfo;
HomeAccountId = homeAccountId;
UserAssertionHash = userAssertionHash;
OboCacheKey = oboCacheKey;
}

private MsalAccessTokenCacheItem(
Expand Down Expand Up @@ -110,7 +110,7 @@ internal MsalAccessTokenCacheItem WithExpiresOn(DateTimeOffset expiresOn)
KeyId,
RefreshOn,
TokenType,
UserAssertionHash);
OboCacheKey);

return newAtItem;
}
Expand All @@ -134,7 +134,11 @@ internal string TenantId
get; private set;
}

internal string UserAssertionHash { get; private set; }
/// <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).
/// </summary>
internal string OboCacheKey { get; set; }

/// <summary>
/// Used when the token is bound to a public / private key pair which is identified by a key id (kid).
Expand Down Expand Up @@ -182,9 +186,8 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
{
extendedExpiresOnUnixTimestamp = ext_expires_on;
}

string tenantId = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Realm);
string userAssertionHash = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.UserAssertionHash);
string oboCacheKey = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.UserAssertionHash);
string keyId = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.KeyId);
string tokenType = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.TokenType) ?? StorageJsonValues.TokenTypeBearer;
string scopes = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Target);
Expand All @@ -199,7 +202,7 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)
keyId: keyId,
tokenType: tokenType);

item.UserAssertionHash = userAssertionHash;
item.OboCacheKey = oboCacheKey;
item.PopulateFieldsFromJObject(j);

return item;
Expand All @@ -212,7 +215,7 @@ internal override JObject ToJObject()
var extExpiresUnixTimestamp = DateTimeHelpers.DateTimeToUnixTimestamp(ExtendedExpiresOn);
SetItemIfValueNotNull(json, StorageJsonKeys.Realm, TenantId);
SetItemIfValueNotNull(json, StorageJsonKeys.Target, ScopeString);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, UserAssertionHash);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, OboCacheKey);
SetItemIfValueNotNull(json, StorageJsonKeys.CachedAt, DateTimeHelpers.DateTimeToUnixTimestamp(CachedAt));
SetItemIfValueNotNull(json, StorageJsonKeys.ExpiresOn, DateTimeHelpers.DateTimeToUnixTimestamp(ExpiresOn));
SetItemIfValueNotNull(json, StorageJsonKeys.ExtendedExpiresOn, extExpiresUnixTimestamp);
Expand All @@ -222,9 +225,9 @@ internal override JObject ToJObject()
json,
StorageJsonKeys.RefreshOn,
RefreshOn.HasValue ? DateTimeHelpers.DateTimeToUnixTimestamp(RefreshOn.Value) : null);

// previous versions of msal used "ext_expires_on" instead of the correct "extended_expires_on".
// this is here for back compat
// previous versions of MSAL used "ext_expires_on" instead of the correct "extended_expires_on".
// this is here for back compatibility
SetItemIfValueNotNull(json, StorageJsonKeys.ExtendedExpiresOn_MsalCompat, extExpiresUnixTimestamp);

return json;
Expand All @@ -242,7 +245,7 @@ internal MsalAccessTokenCacheKey GetKey()
TenantId,
HomeAccountId,
ClientId,
ScopeString,
ScopeString,
TokenType);
}

Expand All @@ -251,8 +254,6 @@ internal MsalIdTokenCacheKey GetIdTokenItemKey()
return new MsalIdTokenCacheKey(Environment, TenantId, HomeAccountId, ClientId);
}



internal bool IsExpiredWithBuffer()
{
return ExpiresOn < DateTime.UtcNow + Constants.AccessTokenExpirationBuffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ internal MsalRefreshTokenCacheItem(
MsalTokenResponse response,
string homeAccountId)
: this(
preferredCacheEnv,
clientId,
response.RefreshToken,
response.ClientInfo,
response.FamilyId,
preferredCacheEnv,
clientId,
response.RefreshToken,
response.ClientInfo,
response.FamilyId,
homeAccountId)
{
}
Expand All @@ -52,7 +52,11 @@ internal MsalRefreshTokenCacheItem(
/// </summary>
public string FamilyId { get; set; }

internal string UserAssertionHash { get; set; }
/// <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).
/// </summary>
internal string OboCacheKey { get; set; }

/// <summary>
/// Family Refresh Tokens, can be used for all clients part of the family
Expand All @@ -78,7 +82,7 @@ internal static MsalRefreshTokenCacheItem FromJObject(JObject j)
{
var item = new MsalRefreshTokenCacheItem();
item.FamilyId = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.FamilyId);
item.UserAssertionHash = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.UserAssertionHash);
item.OboCacheKey = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.UserAssertionHash);

item.PopulateFieldsFromJObject(j);

Expand All @@ -89,7 +93,7 @@ internal override JObject ToJObject()
{
var json = base.ToJObject();
SetItemIfValueNotNull(json, StorageJsonKeys.FamilyId, FamilyId);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, UserAssertionHash);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, OboCacheKey);
return json;
}

Expand Down
4 changes: 2 additions & 2 deletions src/client/Microsoft.Identity.Client/Cache/StorageJsonKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ internal static class StorageJsonKeys
// todo(cache): this needs to be added to the spec. needed for OBO flow on .NET.
public const string UserAssertionHash = "user_assertion_hash";

// previous versions of msal used "ext_expires_on" instead of the correct "extended_expires_on".
// this is here for back compat
// previous versions of MSAL used "ext_expires_on" instead of the correct "extended_expires_on".
// this is here for back compatibility
public const string ExtendedExpiresOn_MsalCompat = "ext_expires_on";
}
}
Loading

0 comments on commit bbd6eee

Please sign in to comment.