Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add public API for long-running OBO methods with custom cache key and do not use refresh token for normal OBO #2820

Merged
merged 23 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cc0b587
Add OBO cache key that can be used to find a token instead of a user …
pmaytak Aug 12, 2021
0d27b48
Rename UserAssertionHash to OboCacheKey and logic to set it. Add meth…
pmaytak Aug 14, 2021
84185f1
Merge remote-tracking branch 'origin/master' into pmaytak/obo-key
pmaytak Aug 14, 2021
51a7fa3
Add tests. Minor fix in cache filtering.
pmaytak Aug 17, 2021
0c388a6
Merge remote-tracking branch 'origin/master' into pmaytak/obo-key
pmaytak Aug 17, 2021
dfb40f7
Update the public API, update related tests.
pmaytak Aug 27, 2021
9cea76f
Merge master
pmaytak Aug 27, 2021
fc93c0e
Fix.
pmaytak Aug 27, 2021
e667465
Update logic if token in cache or not with the OBO cache key provided…
pmaytak Aug 29, 2021
dc8af6c
Merge origin/master
pmaytak Nov 3, 2021
48a2f5a
Fix merge issues.
pmaytak Nov 4, 2021
474b810
Create new ILongRunningWebApi interface. Fix how cache key factory cr…
pmaytak Nov 6, 2021
8b8c781
Nits: add comments, refactor.
pmaytak Nov 9, 2021
ebcc5d9
Remove RT from token response for normal OBO flow.
pmaytak Nov 9, 2021
26da477
Add mock in-memory partitioned cache.
pmaytak Nov 9, 2021
61969ba
Fix tests.
pmaytak Nov 10, 2021
5db4195
PR feedback: Rename OboCacheKey to LongRunningOboCacheKey; add logging.
pmaytak Nov 10, 2021
b4e8fbd
Add tests.
pmaytak Nov 10, 2021
627b4d9
Add exception comment to the public API methods. Remove exception if …
pmaytak Nov 12, 2021
f89db0b
Add tests for combinations of long-running and normal OBO calls.
pmaytak Nov 12, 2021
37280d5
Merge remote-tracking branch 'origin/master' into pmaytak/obo-key
pmaytak Nov 12, 2021
52ff2e1
Nits. Add aka.ms link. Fix comments.
pmaytak Nov 16, 2021
8c2f8d4
Merge remote-tracking branch 'origin/master' into pmaytak/obo-key
pmaytak Nov 16, 2021
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 @@ -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; }
pmaytak marked this conversation as resolved.
Show resolved Hide resolved

/// <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