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

Implemement OnBehalfOfCredential #22146

Merged
merged 31 commits into from
Sep 4, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
264bff6
SupportsCaching property, OBO credential
christothes Apr 28, 2021
1eaca9f
export
christothes Jun 24, 2021
3abc6a3
BAP test
christothes Jun 24, 2021
568908a
missing xml docs
christothes Jun 24, 2021
5287642
CredentialTestBase
christothes Jun 25, 2021
66adcea
add new virtual to UnsafeTokenCacheOptions
christothes Jun 29, 2021
ac08680
refactor UserAssertionScope
christothes Jun 29, 2021
0bb3d6a
merge
christothes Jun 29, 2021
1a7b5c1
export
christothes Jun 30, 2021
ae2b022
UnsafeTokenCacheOptions and TokenCacheNotificationDetails
christothes Jun 30, 2021
05fee10
tweaks
christothes Jun 30, 2021
cc4ac48
Merge remote-tracking branch 'upstream/main' into chriss/OnBehalfOf
christothes Jul 12, 2021
5029352
Merge remote-tracking branch 'upstream/main' into chriss/OnBehalfOf
christothes Jul 19, 2021
64d2788
fb
christothes Jul 19, 2021
4a68ef1
refactor using AccessToken.RefreshOn
christothes Aug 12, 2021
6be7493
export
christothes Aug 12, 2021
7f52c10
fb
christothes Aug 12, 2021
8a6be33
tweaks
christothes Aug 12, 2021
0440c00
remove comment
christothes Aug 12, 2021
52fe1b1
Merge remote-tracking branch 'upstream/main' into chriss/OnBehalfOf
christothes Aug 16, 2021
916eed1
Merge branch 'chriss/OnBehalfOf' of https://github.com/christothes/az…
christothes Aug 16, 2021
d7b582d
use DateTimeOffset.MinValue as no-cache indicator
christothes Aug 16, 2021
3b10559
protect against datetimeoffset underflow
christothes Aug 17, 2021
cecffe1
update RefreshOn doc comment
christothes Aug 18, 2021
cc7fd2f
Simple OBO
christothes Aug 27, 2021
bab4afc
revert core changes
christothes Aug 27, 2021
837d1d6
proj tweak
christothes Aug 27, 2021
ea9066f
merge
christothes Aug 27, 2021
baed5fe
more ctor overloads
christothes Aug 30, 2021
cf193ab
fb
christothes Sep 3, 2021
aea16af
merge
christothes Sep 3, 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
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net461.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public virtual bool SupportsCaching { get { throw null; } }
christothes marked this conversation as resolved.
Show resolved Hide resolved
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net5.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public virtual bool SupportsCaching { get { throw null; } }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public virtual bool SupportsCaching { get { throw null; } }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
1 change: 1 addition & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public virtual bool SupportsCaching { get { throw null; } }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,18 @@ public async ValueTask<string> GetHeaderValueAsync(HttpMessage message, TokenReq
bool getTokenFromCredential;
TaskCompletionSource<HeaderValueInfo> headerValueTcs;
TaskCompletionSource<HeaderValueInfo>? backgroundUpdateTcs;
HeaderValueInfo info;
int maxCancellationRetries = 3;

if (_credential.SupportsCaching)
{
info = await GetHeaderValueFromCredentialAsync(context, async, message.CancellationToken).ConfigureAwait(false);
christothes marked this conversation as resolved.
Show resolved Hide resolved
return info.HeaderValue;
}

while (true)
{
(headerValueTcs, backgroundUpdateTcs, getTokenFromCredential) = GetTaskCompletionSources(context);
HeaderValueInfo info;
if (getTokenFromCredential)
{
if (backgroundUpdateTcs != null)
Expand Down
8 changes: 7 additions & 1 deletion sdk/core/Azure.Core/src/TokenCredential.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.Pipeline;

namespace Azure.Core
{
Expand All @@ -27,5 +27,11 @@ public abstract class TokenCredential
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
/// <returns>A valid <see cref="AccessToken"/>.</returns>
public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken);

/// <summary>
/// Indicates if a <see cref="TokenCredential"/> supports token caching.
/// If <c>true</c>, upstream components such as a <see cref="HttpPipelinePolicy"/> should rely on the credential itself to manage any token caching.
/// </summary>
public virtual bool SupportsCaching => false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class BearerTokenAuthenticationPolicyTests : SyncAsyncPolicyTestBase
public BearerTokenAuthenticationPolicyTests(bool isAsync) : base(isAsync) { }

[Test]
public async Task BearerTokenAuthenticationPolicy_UsesTokenProvidedByCredentials()
public async Task BearerTokenAuthenticationPolicy_UsesTokenProvidedByCredentials ()
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
var credential = new TokenCredentialStub(
(r, c) => r.Scopes.SequenceEqual(new[] { "scope1", "scope2" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default,
Expand All @@ -33,6 +33,32 @@ public async Task BearerTokenAuthenticationPolicy_UsesTokenProvidedByCredentials
Assert.AreEqual("Bearer token", authValue);
}

[Test]
public async Task BearerTokenAuthenticationPolicy_SupportsCaching_True()
{
int expectedCalls = 3;
int tokenCalls = 0;
var credential = new TokenCredentialStub(
(r, c) =>
{
tokenCalls++;
return r.Scopes.SequenceEqual(new[] { "scope1", "scope2" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default;
},
IsAsync);
credential.SetSupportsCaching(true);
var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope1", "scope2" });

MockTransport transport = CreateMockTransport(_ => new MockResponse(200));

for (int i = 0; i < expectedCalls; i++)
{
await SendGetRequest(transport, policy, uri: new Uri("https://example.com"));
}

Assert.That(transport.Requests.Select(r => r.Headers.Single()), Has.All.Property("Name").EqualTo("Authorization"));
Assert.AreEqual(expectedCalls, tokenCalls);
}

[Test]
public async Task BearerTokenAuthenticationPolicy_RequestsTokenEveryRequest()
{
Expand Down Expand Up @@ -804,6 +830,10 @@ public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext request

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> _getTokenHandler(requestContext, cancellationToken);

private bool _supportsCaching = false;
public void SetSupportsCaching(bool supportsCaching) => _supportsCaching = supportsCaching;
public override bool SupportsCaching => _supportsCaching;
}
}
}
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added support to `ManagedIdentityCredential` for Bridge to Kubernetes local development authentication.
- TenantId values returned from service challenge responses can now be used to request tokens from the correct tenantId. To support this feature, there is a new `AllowMultiTenantAuthentication` option on `TokenCredentialOptions`.
- By default, `AllowMultiTenantAuthentication` is false. When this option property is false and the tenant Id configured in the credential options differs from the tenant Id set in the `TokenRequestContext` sent to a credential, an `AuthorizationFailedException` will be thrown. This is potentially breaking change as it could be a different exception than what was thrown previously. This exception behavior can be overridden by either setting an `AppContext` switch named "Azure.Identity.EnableLegacyTenantSelection" to `true` or by setting the environment variable "AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION" to "true". Note: AppContext switches can also be configured via configuration like below:
- Added `OnBehalfOfFlowCredential` which enables support for AAD On-Behalf-Of (OBO) flow. See the [Azure Active Directory documentation](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to learn more about OBO flow scenarios.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this feature deserves a sample.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - I plan to add one in a follow up PR


```xml
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ public ManagedIdentityCredential(string clientId = null, Azure.Identity.TokenCre
public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public override System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public partial class OnBehalfOfCredential : Azure.Core.TokenCredential
{
protected OnBehalfOfCredential() { }
public OnBehalfOfCredential(string tenantId, string clientId, string clientSecret, Azure.Identity.TokenCredentialOptions options = null) { }
public override bool SupportsCaching { get { throw null; } }
public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
public override System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class SharedTokenCacheCredential : Azure.Core.TokenCredential
{
public SharedTokenCacheCredential() { }
Expand Down Expand Up @@ -266,6 +274,12 @@ protected UnsafeTokenCacheOptions() { }
protected internal abstract System.Threading.Tasks.Task<System.ReadOnlyMemory<byte>> RefreshCacheAsync();
protected internal abstract System.Threading.Tasks.Task TokenCacheUpdatedAsync(Azure.Identity.TokenCacheUpdatedArgs tokenCacheUpdatedArgs);
}
public partial class UserAssertionScope : System.IDisposable
{
public UserAssertionScope(Azure.Core.AccessToken accessToken) { }
public UserAssertionScope(string accessToken) { }
public void Dispose() { }
}
public partial class UsernamePasswordCredential : Azure.Core.TokenCredential
{
protected UsernamePasswordCredential() { }
Expand Down
20 changes: 20 additions & 0 deletions sdk/identity/Azure.Identity/src/MsalConfidentialClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,25 @@ public virtual async ValueTask<AuthenticationResult> AcquireTokenByAuthorization
.ExecuteAsync(async, cancellationToken)
.ConfigureAwait(false);
}

public virtual async ValueTask<AuthenticationResult> AcquireTokenOnBehalfOf(
string[] scopes,
string tenantId,
UserAssertion userAssertionValue,
bool async,
CancellationToken cancellationToken)
{
IConfidentialClientApplication client = await GetClientAsync(async, cancellationToken).ConfigureAwait(false);

var builder = client.AcquireTokenOnBehalfOf(scopes, userAssertionValue);

if (!string.IsNullOrEmpty(tenantId))
{
builder.WithAuthority(Pipeline.AuthorityHost.AbsoluteUri, tenantId);
}
return await builder
.ExecuteAsync(async, cancellationToken)
.ConfigureAwait(false);
}
}
}
95 changes: 95 additions & 0 deletions sdk/identity/Azure.Identity/src/OnBehalfOfCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.Pipeline;
using Microsoft.Identity.Client;

namespace Azure.Identity
{
/// <summary>
///
christothes marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public class OnBehalfOfCredential : TokenCredential
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly MsalConfidentialClient _client;
private readonly string _tenantId;
private readonly CredentialPipeline _pipeline;
private readonly bool _allowMultiTenantAuthentication;

/// <inheritdoc />
public override bool SupportsCaching => true;

/// <summary>
/// Protected constructor for mocking.
/// </summary>
protected OnBehalfOfCredential()
{ }

/// <summary>
/// Creates an instance of the <see cref="OnBehalfOfCredential"/> with the details needed to authenticate with Azure Active Directory.
/// Calls to <see cref="GetToken"/> and <see cref="GetTokenAsync"/> should be wrapped with a using block that constructs an instance of
/// <see cref="UserAssertionScope"/> with the user's <see cref="AccessToken"/> to be used in the On-Behalf-Of flow.
/// </summary>
/// <param name="tenantId">The Azure Active Directory tenant (directory) Id of the service principal.</param>
/// <param name="clientId">The client (application) ID of the service principal</param>
/// <param name="clientSecret">A client secret that was generated for the App Registration used to authenticate the client.</param>
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service.</param>
public OnBehalfOfCredential(
string tenantId,
string clientId,
string clientSecret,
TokenCredentialOptions options = null)
: this(tenantId, clientId, clientSecret, options, null, null)
{ }

internal OnBehalfOfCredential(
string tenantId,
string clientId,
string clientSecret,
christothes marked this conversation as resolved.
Show resolved Hide resolved
TokenCredentialOptions options,
CredentialPipeline pipeline,
MsalConfidentialClient client)
{
Argument.AssertNotNull(clientId, nameof(clientId));
Argument.AssertNotNull(clientSecret, nameof(clientSecret));

options ??= new TokenCredentialOptions();
_allowMultiTenantAuthentication = options.AllowMultiTenantAuthentication;
_tenantId = Validations.ValidateTenantId(tenantId, nameof(tenantId));
_pipeline = pipeline ?? CredentialPipeline.GetInstance(options);
_client = client ?? new MsalConfidentialClient(_pipeline, tenantId, clientId, clientSecret, options as ITokenCacheOptions);
}

/// <inheritdoc />
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
GetTokenInternalAsync(requestContext, false, cancellationToken).EnsureCompleted();

/// <inheritdoc />
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
GetTokenInternalAsync(requestContext, true, cancellationToken);

internal async ValueTask<AccessToken> GetTokenInternalAsync(TokenRequestContext requestContext, bool async, CancellationToken cancellationToken)
{
using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope("OnBehalfOfCredential.GetToken", requestContext);

try
{
var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _allowMultiTenantAuthentication);

AuthenticationResult result = await _client
.AcquireTokenOnBehalfOf(requestContext.Scopes, tenantId, UserAssertionScope.Current.UserAssertion, async, cancellationToken)
.ConfigureAwait(false);

return new AccessToken(result.AccessToken, result.ExpiresOn);
}
catch (Exception e)
{
throw scope.FailWrapAndThrow(e);
}
}
}
}
54 changes: 54 additions & 0 deletions sdk/identity/Azure.Identity/src/UserAssertionScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading;
using Azure.Core;
using Microsoft.Identity.Client;

namespace Azure.Identity
{
/// <summary>
/// Creates a scope which will be used by <see cref="OnBehalfOfCredential"/> to request tokens on behalf of the user's token used to initialize
/// the <see cref="UserAssertionScope"/> instance.
/// </summary>
public class UserAssertionScope : IDisposable
{
private static AsyncLocal<UserAssertionScope> _currentAsyncLocal = new();
internal UserAssertion UserAssertion { get; }

/// <summary>
/// The current value of the <see cref="AsyncLocal{UserAssertion}"/>.
/// </summary>
internal static UserAssertionScope Current => _currentAsyncLocal.Value;

/// <summary>
/// Initializes a new instance of <see cref="UserAssertionScope"/> using the supplied access token.
/// </summary>
/// <param name="accessToken">The access token that will be used bu <see cref="OnBehalfOfCredential"/> when requesting On-Behalf-Of tokens.</param>
public UserAssertionScope(string accessToken)
{
UserAssertion = new UserAssertion(accessToken);
_currentAsyncLocal.Value = this;
}

/// <summary>
/// Initializes a new instance of <see cref="UserAssertionScope"/> using the supplied <paramref name="accessToken"/>.
/// </summary>
/// <param name="accessToken">The <see cref="AccessToken"/> that will be used bu <see cref="OnBehalfOfCredential"/> when requesting On-Behalf-Of tokens.</param>
christothes marked this conversation as resolved.
Show resolved Hide resolved
public UserAssertionScope(AccessToken accessToken)
{
UserAssertion = new UserAssertion(accessToken.Token);
_currentAsyncLocal.Value = this;
}

/// <summary>
///
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
_currentAsyncLocal.Value = default;
}
}
}
Loading