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
Changes from 1 commit
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
Next Next commit
Add OBO cache key that can be used to find a token instead of a user …
…assertion.
  • Loading branch information
pmaytak committed Aug 12, 2021
commit cc0b587cece27a5de6d87deafc490cd26a9b7e8d
Original file line number Diff line number Diff line change
@@ -36,21 +36,41 @@ 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,
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>
///
/// </summary>
/// <param name="cacheKey"></param>
/// <returns></returns>
public AcquireTokenOnBehalfOfParameterBuilder WithCacheKey(string cacheKey)
{
Parameters.OboCacheKey = 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:
Original file line number Diff line number Diff line change
@@ -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;

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

return await handler.RunAsync(cancellationToken).ConfigureAwait(false);
}
@@ -58,9 +57,9 @@ public async Task<AuthenticationResult> ExecuteAsync(
commonParameters,
requestContext,
_confidentialClientApplication.AppTokenCacheInternal).ConfigureAwait(false);

requestParams.SendX5C = clientParameters.SendX5C;

var handler = new ClientCredentialRequest(
ServiceBundle,
requestParams,
@@ -83,6 +82,7 @@ public async Task<AuthenticationResult> ExecuteAsync(

requestParams.SendX5C = onBehalfOfParameters.SendX5C;
requestParams.UserAssertion = onBehalfOfParameters.UserAssertion;
requestParams.OboCacheKey = onBehalfOfParameters.OboCacheKey;

var handler = new OnBehalfOfRequest(
ServiceBundle,
@@ -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());
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@ namespace Microsoft.Identity.Client.ApiConfig.Parameters
internal class AcquireTokenOnBehalfOfParameters : IAcquireTokenParameters
{
public UserAssertion UserAssertion { get; set; }
public bool SendX5C { get; set;}
public string OboCacheKey { get; set; }
public bool SendX5C { get; set; }
public bool ForceRefresh { get; set; }

/// <inheritdoc />
@@ -19,6 +20,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("OboCacheKey set: " + !string.IsNullOrWhiteSpace(OboCacheKey));
logger.Info(builder.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -74,7 +74,7 @@ internal MsalAccessTokenCacheItem(
}

HomeAccountId = homeAccountId;
}
}

private string _tenantId;

@@ -90,6 +90,7 @@ internal string TenantId
internal string ExpiresOnUnixTimestamp { get; set; }
internal string ExtendedExpiresOnUnixTimestamp { get; set; }
internal string UserAssertionHash { get; set; }
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).
@@ -160,12 +161,13 @@ internal static MsalAccessTokenCacheItem FromJObject(JObject j)

var item = new MsalAccessTokenCacheItem(JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Target))
{
TenantId = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Realm),
TenantId = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.Realm),
CachedAt = cachedAt.ToString(CultureInfo.InvariantCulture),
ExpiresOnUnixTimestamp = expiresOn.ToString(CultureInfo.InvariantCulture),
ExtendedExpiresOnUnixTimestamp = extendedExpiresOn.ToString(CultureInfo.InvariantCulture),
RefreshOnUnixTimestamp = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.RefreshOn),
UserAssertionHash = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.UserAssertionHash),
OboCacheKey = JsonUtils.ExtractExistingOrEmptyString(j, StorageJsonKeys.OboCacheKey),
KeyId = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.KeyId),
TokenType = JsonUtils.ExtractExistingOrDefault<string>(j, StorageJsonKeys.TokenType) ?? StorageJsonValues.TokenTypeBearer
};
@@ -182,15 +184,16 @@ internal override JObject ToJObject()
SetItemIfValueNotNull(json, StorageJsonKeys.Realm, TenantId);
SetItemIfValueNotNull(json, StorageJsonKeys.Target, _scopes);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, UserAssertionHash);
SetItemIfValueNotNull(json, StorageJsonKeys.OboCacheKey, OboCacheKey);
pmaytak marked this conversation as resolved.
Show resolved Hide resolved
SetItemIfValueNotNull(json, StorageJsonKeys.CachedAt, CachedAt);
SetItemIfValueNotNull(json, StorageJsonKeys.ExpiresOn, ExpiresOnUnixTimestamp);
SetItemIfValueNotNull(json, StorageJsonKeys.ExtendedExpiresOn, ExtendedExpiresOnUnixTimestamp);
SetItemIfValueNotNull(json, StorageJsonKeys.KeyId, KeyId);
SetItemIfValueNotNullOrDefault(json, StorageJsonKeys.TokenType, TokenType, StorageJsonValues.TokenTypeBearer);
SetItemIfValueNotNull(json, StorageJsonKeys.RefreshOn, RefreshOnUnixTimestamp);

// 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, ExtendedExpiresOnUnixTimestamp);

return json;
@@ -208,7 +211,7 @@ internal MsalAccessTokenCacheKey GetKey()
TenantId,
HomeAccountId,
ClientId,
_scopes,
_scopes,
TokenType);
}

Original file line number Diff line number Diff line change
@@ -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)
{
}
@@ -53,6 +53,7 @@ internal MsalRefreshTokenCacheItem(
public string FamilyId { get; set; }

internal string UserAssertionHash { get; set; }
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
@@ -79,6 +80,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.OboCacheKey);

item.PopulateFieldsFromJObject(j);

@@ -90,6 +92,7 @@ internal override JObject ToJObject()
var json = base.ToJObject();
SetItemIfValueNotNull(json, StorageJsonKeys.FamilyId, FamilyId);
SetItemIfValueNotNull(json, StorageJsonKeys.UserAssertionHash, UserAssertionHash);
SetItemIfValueNotNull(json, StorageJsonKeys.OboCacheKey, OboCacheKey);
return json;
}

5 changes: 3 additions & 2 deletions src/client/Microsoft.Identity.Client/Cache/StorageJsonKeys.cs
Original file line number Diff line number Diff line change
@@ -34,9 +34,10 @@ 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";
public const string OboCacheKey = "obo_cache_key";

// 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";
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.Identity.Client.Internal.Requests;

namespace Microsoft.Identity.Client.Cache
@@ -50,15 +49,15 @@ private static bool GetOboOrAppKey(AuthenticationRequestParameters requestParame
{
if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenOnBehalfOf)
{
key = requestParameters.UserAssertion.AssertionHash;
key = requestParameters.OboCacheKey ?? requestParameters.UserAssertion.AssertionHash;
return true;
}

if (requestParameters.ApiId == TelemetryCore.Internal.Events.ApiEvent.ApiIds.AcquireTokenForClient)
{
string tenantId = requestParameters.Authority.TenantId ?? "";
key = GetClientCredentialKey(requestParameters.AppConfig.ClientId, tenantId);

return true;
}

@@ -67,7 +66,7 @@ private static bool GetOboOrAppKey(AuthenticationRequestParameters requestParame
}

public /* for test */ static string GetClientCredentialKey(string clientId, string tenantId)
{
{
return $"{clientId}_{tenantId}_AppTokenCache";
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Identity.Client.Internal.Requests;
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Identity.Client.Instance;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.TelemetryCore;
using System.Threading;
using Microsoft.Identity.Client.ApiConfig;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Http;
using Microsoft.Identity.Client.ApiConfig.Executors;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Cache.CacheImpl;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Internal.Requests;

namespace Microsoft.Identity.Client
{
@@ -30,7 +24,7 @@ namespace Microsoft.Identity.Client
/// and never directly exposed. For details see https://aka.ms/msal-net-client-applications
/// </remarks>
#if !SUPPORTS_CONFIDENTIAL_CLIENT
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidentail client on mobile
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] // hide confidential client on mobile
#endif
public sealed partial class ConfidentialClientApplication
: ClientApplicationBase,
@@ -51,7 +45,7 @@ internal ConfidentialClientApplication(
GuardMobileFrameworks();

InMemoryPartitionedCacheSerializer = new InMemoryPartitionedCacheSerializer(ServiceBundle.ApplicationLogger);
AppTokenCacheInternal = configuration.AppTokenCacheInternalForTest ??
AppTokenCacheInternal = configuration.AppTokenCacheInternalForTest ??
new TokenCache(ServiceBundle, true, InMemoryPartitionedCacheSerializer);
Certificate = configuration.ClientCredentialCertificate;

@@ -91,7 +85,7 @@ public AcquireTokenByAuthorizationCodeParameterBuilder AcquireTokenByAuthorizati
/// <param name="scopes">scopes requested to access a protected API. For this flow (client credentials), the scopes
/// should be of the form "{ResourceIdUri/.default}" for instance <c>https://management.azure.net/.default</c> or, for Microsoft
/// Graph, <c>https://graph.microsoft.com/.default</c> as the requested scopes are defined statically with the application registration
/// in the portal, and cannot be overriden in the application.</param>
/// in the portal, and cannot be overridden in the application.</param>
/// <returns>A builder enabling you to add optional parameters before executing the token request</returns>
/// <remarks>You can also chain the following optional parameters:
/// <see cref="AcquireTokenForClientParameterBuilder.WithForceRefresh(bool)"/>
@@ -135,6 +129,27 @@ public AcquireTokenOnBehalfOfParameterBuilder AcquireTokenOnBehalfOf(
userAssertion);
}

/// <summary>
///
/// </summary>
/// <param name="scopes"></param>
/// <param name="cacheKey"></param>
/// <returns></returns>
public AcquireTokenOnBehalfOfParameterBuilder AcquireTokenOnBehalfOf(
IEnumerable<string> scopes,
string cacheKey)
{
if (cacheKey == null)
{
throw new ArgumentNullException(nameof(cacheKey));
}

return AcquireTokenOnBehalfOfParameterBuilder.Create(
ClientExecutorFactory.CreateConfidentialClientExecutor(this),
scopes,
cacheKey);
}

/// <summary>
/// Computes the URL of the authorization request letting the user sign-in and consent to the application accessing specific scopes in
/// the user's name. The URL targets the /authorize endpoint of the authority configured in the application.
Original file line number Diff line number Diff line change
@@ -14,8 +14,8 @@
namespace Microsoft.Identity.Client.Internal.Requests
{
/// <summary>
/// This class is responsible for merging app level and request level params.
/// Not all params need to be merged - app level params can be accessed via AppConfig property
/// This class is responsible for merging app level and request level parameters.
/// Not all parameters need to be merged - app level parameters can be accessed via AppConfig property
/// </summary>
internal class AuthenticationRequestParameters
{
@@ -75,18 +75,18 @@ public IEnumerable<KeyValuePair<string, string>> GetApiTelemetryFeatures()

#region Authority

public AuthorityManager AuthorityManager { get; set; }
public AuthorityManager AuthorityManager { get; set; }

/// <summary>
/// Authority is the URI used by MSAL for communication and storage
/// During a request it can be updated:
/// - with the preffered enviroment
/// - with the proffered environment
pmaytak marked this conversation as resolved.
Show resolved Hide resolved
/// - with actual tenant
/// </summary>
public Authority Authority => AuthorityManager.Authority;

public AuthorityInfo AuthorityInfo => AuthorityManager.Authority.AuthorityInfo;

public AuthorityInfo AuthorityOverride => _commonParameters.AuthorityOverride;

#endregion
@@ -160,6 +160,7 @@ public bool IsConfidentialClient
}
}
public UserAssertion UserAssertion { get; set; }
public string OboCacheKey { get; set; }

public KeyValuePair<string, string>? CcsRoutingHint { get; set; }

@@ -186,6 +187,8 @@ public void LogParameters()
builder.AppendLine("IsBrokerConfigured - " + AppConfig.IsBrokerEnabled);
builder.AppendLine("HomeAccountId - " + HomeAccountId);
builder.AppendLine("CorrelationId - " + CorrelationId);
builder.AppendLine("UserAssertion set: " + (UserAssertion != null));
builder.AppendLine("OboCacheKey set: " + !string.IsNullOrWhiteSpace(OboCacheKey));

string messageWithPii = builder.ToString();

@@ -203,6 +206,8 @@ public void LogParameters()
builder.AppendLine("IsBrokerConfigured - " + AppConfig.IsBrokerEnabled);
builder.AppendLine("HomeAccountId - " + !string.IsNullOrEmpty(HomeAccountId));
builder.AppendLine("CorrelationId - " + CorrelationId);
builder.AppendLine("UserAssertion set: " + (UserAssertion != null));
builder.AppendLine("OboCacheKey set: " + !string.IsNullOrWhiteSpace(OboCacheKey));

logger.InfoPii(messageWithPii, builder.ToString());
}
Loading