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

Support Tenant Id Challenges / Hints #21378

Merged
merged 52 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c6f77b7
wip
christothes Apr 28, 2021
77ef7f4
merge
christothes May 11, 2021
08265aa
wip
christothes May 12, 2021
a06206a
Storage POC
christothes May 12, 2021
c7ce340
introduce IBearerTokenChallengeOptions
christothes May 14, 2021
4f01bb6
refactor StorageBearerTokenChallengeAuthorizationPolicy ctor
christothes May 18, 2021
437237b
needs more tests
christothes May 24, 2021
e0f4694
merge
christothes May 24, 2021
95c9d40
tests
christothes May 25, 2021
fed5849
tests
christothes May 26, 2021
afd81f1
tests and changelogs
christothes May 26, 2021
4c1c382
fb
christothes May 26, 2021
7a13b9a
export
christothes May 26, 2021
cca02ba
storage discovery defaults to false
christothes May 26, 2021
aba8dde
Update sdk/identity/Azure.Identity/src/UsernamePasswordCredential.cs
christothes May 27, 2021
e60928f
Merge remote-tracking branch 'upstream/master' into feature/multi-ten…
christothes Jun 2, 2021
75a6fd3
rename TenantIdHint
christothes Jun 3, 2021
76e4a8f
merge and pr fb
christothes Jun 8, 2021
21779f4
Merge branch 'master' into feature/multi-tenant-auth
christothes Jun 8, 2021
17c8e72
fb
christothes Jun 8, 2021
a565ecf
fixup refs
christothes Jun 8, 2021
407337a
test StorageBearerTokenChallengeAuthorizationPolicy
christothes Jun 8, 2021
3f6a988
merge
christothes Jun 8, 2021
dba065b
namespace
christothes Jun 8, 2021
18d9753
pass options to test setup
christothes Jun 9, 2021
2d78abd
recorded tests, make recordedTest attrbitue work, fixup test-resource…
christothes Jun 9, 2021
f94183a
merge
christothes Jun 9, 2021
0807556
relocate ISupportsTenantIdChallenges
christothes Jun 10, 2021
6a4a77c
add tenantId options to CLI and Pwsh creds
christothes Jun 11, 2021
59f2c7d
export and revert fixes to RecordedTest behavior
christothes Jun 11, 2021
66d0803
fb and merge
christothes Jun 15, 2021
49f572e
fb
christothes Jun 15, 2021
58fca22
options feedback
christothes Jun 17, 2021
559dc80
merge
christothes Jun 18, 2021
ced4693
merge
christothes Jun 18, 2021
d472066
fix recordings
christothes Jun 18, 2021
11f2e4c
fb
christothes Jun 21, 2021
868b336
Merge remote-tracking branch 'upstream/main' into feature/multi-tenan…
christothes Jun 21, 2021
e4c3e65
move SubscriptionValidationResponse ref
christothes Jun 21, 2021
8425bcd
fix ref
christothes Jun 21, 2021
983f9cf
export
christothes Jun 21, 2021
34945cd
AllowMultiTenantAuth defaults to false, and added env and config control
christothes Jun 21, 2021
a2882e4
merge
christothes Jun 22, 2021
6762805
api
christothes Jun 22, 2021
6328e3d
Changelog
christothes Jun 22, 2021
cc41a83
EnableLegacyTenantSelection rename
christothes Jun 22, 2021
4f3bb70
rename vars
christothes Jun 22, 2021
499e54b
Merge remote-tracking branch 'upstream/main' into feature/multi-tenan…
christothes Jun 23, 2021
5444660
regen snippets for schemaregistry
christothes Jun 23, 2021
38ffc87
Changelog cleanup
christothes Jun 23, 2021
47f5509
Update README.md
christothes Jun 23, 2021
3896a8a
Update README.md
christothes Jun 23, 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 @@ -136,7 +136,7 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPip
// If we succeed in getting the context, authenticate the request and pass it up the policy chain.
if (async)
{
if (await AuthorizeRequestOnChallengeAsync((message)).ConfigureAwait(false))
if (await AuthorizeRequestOnChallengeAsync(message).ConfigureAwait(false))
{
await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
}
Expand Down
17 changes: 17 additions & 0 deletions sdk/core/Azure.Core/src/Shared/IBearerTokenChallengeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Core
{
/// <summary>
/// Options to be exposed in client options classes related to bearer token authorization challenge scenarios.
/// </summary>
internal interface IBearerTokenChallengeOptions
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Disables tenant discovery through the authorization challenge when the client is configured to use a <see cref="TokenCredential"/>.
/// When disabled, the client will not attempt an initial un-authroized request to prompt a challenge in order to discover the correct tenant for the resource.
/// </summary>
public bool DisableTenantDiscovery { get; }
christothes marked this conversation as resolved.
Show resolved Hide resolved
}
}
19 changes: 19 additions & 0 deletions sdk/core/Azure.Core/src/TokenRequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public TokenRequestContext(string[] scopes, string? parentRequestId)
Scopes = scopes;
ParentRequestId = parentRequestId;
Claims = default;
TenantIdHint = default;
}

/// <summary>
Expand All @@ -31,6 +32,19 @@ public TokenRequestContext(string[] scopes, string? parentRequestId = default, s
Scopes = scopes;
ParentRequestId = parentRequestId;
Claims = claims;
TenantIdHint = default;
}

/// <summary>
/// Creates a new TokenRequest with the specified scopes.
/// </summary>
/// <param name="options"> The options to initialized the <see cref="TokenRequestContext"/> </param>
public TokenRequestContext(TokenRequestContextOptions options)
{
Scopes = options.Scopes;
ParentRequestId = options.ParentRequestId;
Claims = options.Claims;
TenantIdHint = options.TenantIdHint;
}

/// <summary>
Expand All @@ -47,5 +61,10 @@ public TokenRequestContext(string[] scopes, string? parentRequestId = default, s
/// Additional claims to be included in the token. See <see href="https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter">https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter</see> for more information on format and content.
/// </summary>
public string? Claims { get; }

/// <summary>
/// A hint to indicate which tenantId is preferred.
/// </summary>
public string? TenantIdHint { get; }
christothes marked this conversation as resolved.
Show resolved Hide resolved
}
}
31 changes: 31 additions & 0 deletions sdk/core/Azure.Core/src/TokenRequestContextOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Core
{
/// <summary>
/// Options for <see cref="TokenRequestContext"/> initialization.
/// </summary>
public struct TokenRequestContextOptions
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// The scopes required for the token.
/// </summary>
public string[] Scopes { get; set; }

/// <summary>
/// The <see cref="Request.ClientRequestId"/> of the request requiring a token for authentication, if applicable.
/// </summary>
public string? ParentRequestId { get; set; }

/// <summary>
/// Additional claims to be included in the token. See <see href="https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter">https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter</see> for more information on format and content.
/// </summary>
public string? Claims { get; set; }

/// <summary>
/// A hint to indicate which tenantId is preferred.
/// </summary>
public string? TenantIdHint { get; set; }
}
}
1 change: 1 addition & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes and improvements

- Added `LoginHint` property to `InteractiveBrowserCredentialOptions` which allows a user name to be pre-selected for interactive logins. Setting this option skips the account selection prompt and immediately attempts to login with the specified account.
- TenantId values returned from service challenge responses can now be used to request tokens from the correct tenantId.

## 1.4.0 (2021-05-12)

Expand Down
52 changes: 29 additions & 23 deletions sdk/identity/Azure.Identity/src/AuthorizationCodeCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.Diagnostics;
using Azure.Core.Pipeline;
using Microsoft.Identity.Client;

Expand All @@ -18,12 +17,13 @@ namespace Azure.Identity
/// </summary>
public class AuthorizationCodeCredential : TokenCredential
{
private readonly IConfidentialClientApplication _confidentialClient;
private readonly ClientDiagnostics _clientDiagnostics;
private readonly string _authCode;
private readonly string _clientId;
private readonly CredentialPipeline _pipeline;
private AuthenticationRecord _record;
private readonly string _tenantId;
private readonly TokenCredentialOptions _options;
Copy link
Member

Choose a reason for hiding this comment

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

Generally we only read the options in the constructor and cache the fields rather than caching the entire options object. This way if the values of the options object change after construction the credential behavior doesn't change.

Copy link
Member Author

Choose a reason for hiding this comment

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

I mainly did this so that the options could be passed to the TenantIdResolver. I could also just save off the PreferClientConfiguredTenantId property on the options, but I thought this allowed the logic in the resolver to stay a bit more encapsulated.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should capture AllowMultiTenantAuthentication from the options to avoid changing behavior after the credential is constructed.

private readonly MsalConfidentialClient _client;

/// <summary>
/// Protected constructor for mocking.
Expand Down Expand Up @@ -55,26 +55,26 @@ public AuthorizationCodeCredential(string tenantId, string clientId, string clie
/// See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow for more information.</param>
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service.</param>
public AuthorizationCodeCredential(string tenantId, string clientId, string clientSecret, string authorizationCode, TokenCredentialOptions options)
{
Validations.ValidateTenantId(tenantId, nameof(tenantId));

if (clientSecret is null) throw new ArgumentNullException(nameof(clientSecret));

_clientId = clientId ?? throw new ArgumentNullException(nameof(clientId));

_authCode = authorizationCode ?? throw new ArgumentNullException(nameof(authorizationCode));
: this(tenantId, clientId, clientSecret, authorizationCode, options, null)
{}

options ??= new TokenCredentialOptions();

_pipeline = CredentialPipeline.GetInstance(options);

_confidentialClient = ConfidentialClientApplicationBuilder.Create(clientId).WithHttpClientFactory(new HttpPipelineClientFactory(_pipeline.HttpPipeline)).WithTenantId(tenantId).WithClientSecret(clientSecret).Build();

_clientDiagnostics = new ClientDiagnostics(options);
internal AuthorizationCodeCredential(string tenantId, string clientId, string clientSecret, string authorizationCode, TokenCredentialOptions options, MsalConfidentialClient client)
{
Argument.AssertNotNull(clientSecret, nameof(clientSecret));
Argument.AssertNotNull(clientId, nameof(clientId));
Argument.AssertNotNull(authorizationCode, nameof(authorizationCode));
_tenantId = Validations.ValidateTenantId(tenantId, nameof(tenantId));
_clientId = clientId;
_authCode = authorizationCode ;
_options = options ?? new TokenCredentialOptions();
_pipeline = CredentialPipeline.GetInstance(_options);

_client = client ?? new MsalConfidentialClient(_pipeline, tenantId, clientId, clientSecret, options as ITokenCacheOptions);
}

/// <summary>
/// Obtains a token from the Azure Active Directory service, using the specified authorization code authenticate. This method is called automatically by Azure SDK client libraries. You may call this method directly, but you must also handle token caching and token refreshing.
/// Obtains a token from the Azure Active Directory service, using the specified authorization code authenticate. This method is called automatically by
/// Azure SDK client libraries. You may call this method directly, but you must also handle token caching and token refreshing.
/// </summary>
/// <param name="requestContext">The details of the authentication request.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
Expand All @@ -85,7 +85,8 @@ public override AccessToken GetToken(TokenRequestContext requestContext, Cancell
}

/// <summary>
/// Obtains a token from the Azure Active Directory service, using the specified authorization code authenticate. This method is called automatically by Azure SDK client libraries. You may call this method directly, but you must also handle token caching and token refreshing.
/// Obtains a token from the Azure Active Directory service, using the specified authorization code authenticate. This method is called automatically by
/// Azure SDK client libraries. You may call this method directly, but you must also handle token caching and token refreshing.
/// </summary>
/// <param name="requestContext">The details of the authentication request.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
Expand All @@ -101,19 +102,24 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC

try
{
AccessToken token = default;
AccessToken token;
var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _options);

if (_record is null)
{
AuthenticationResult result = await _confidentialClient.AcquireTokenByAuthorizationCode(requestContext.Scopes, _authCode).ExecuteAsync(async, cancellationToken).ConfigureAwait(false);
AuthenticationResult result = await _client
.AcquireTokenByAuthorizationCodeAsync(requestContext.Scopes, _authCode, tenantId, async, cancellationToken)
.ConfigureAwait(false);

_record = new AuthenticationRecord(result, _clientId);

token = new AccessToken(result.AccessToken, result.ExpiresOn);
}
else
{
AuthenticationResult result = await _confidentialClient.AcquireTokenSilent(requestContext.Scopes, (AuthenticationAccount)_record).ExecuteAsync(async, cancellationToken).ConfigureAwait(false);
AuthenticationResult result = await _client
.AcquireTokenSilentAsync(requestContext.Scopes, (AuthenticationAccount)_record, tenantId, async, cancellationToken)
.ConfigureAwait(false);

token = new AccessToken(result.AccessToken, result.ExpiresOn);
}
Expand Down
5 changes: 5 additions & 0 deletions sdk/identity/Azure.Identity/src/Azure.Identity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<!-- TODO: uncomment when Azure.Core ships
<PackageReference Include="Azure.Core" />
end TODO -->
<!-- TODO: revert when Azure.Core ships -->
<ProjectReference Include="..\..\..\core\Azure.Core\src\Azure.Core.csproj" />
<!-- end TODO-->
<PackageReference Include="System.Memory" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Threading.Tasks.Extensions" />
Expand Down
17 changes: 11 additions & 6 deletions sdk/identity/Azure.Identity/src/AzureCliCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC

try
{
AccessToken token = await RequestCliAccessTokenAsync(async, requestContext.Scopes, cancellationToken).ConfigureAwait(false);
AccessToken token = await RequestCliAccessTokenAsync(async, requestContext, cancellationToken).ConfigureAwait(false);
return scope.Succeeded(token);
}
catch (Exception e)
Expand All @@ -95,13 +95,14 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC
}
}

private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, string[] scopes, CancellationToken cancellationToken)
private async ValueTask<AccessToken> RequestCliAccessTokenAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken)
{
string resource = ScopeUtilities.ScopesToResource(scopes);
string resource = ScopeUtilities.ScopesToResource(context.Scopes);
string tenantId = TenantIdResolver.Resolve(null, context, null);

ScopeUtilities.ValidateScope(resource);

GetFileNameAndArguments(resource, out string fileName, out string argument);
GetFileNameAndArguments(resource, tenantId, out string fileName, out string argument);
ProcessStartInfo processStartInfo = GetAzureCliProcessStartInfo(fileName, argument);
using var processRunner = new ProcessRunner(_processService.Create(processStartInfo), TimeSpan.FromMilliseconds(CliProcessTimeoutMs), cancellationToken);

Expand Down Expand Up @@ -157,9 +158,13 @@ private ProcessStartInfo GetAzureCliProcessStartInfo(string fileName, string arg
Environment = { { "PATH", _path } }
};

private static void GetFileNameAndArguments(string resource, out string fileName, out string argument)
private static void GetFileNameAndArguments(string resource, string tenantId, out string fileName, out string argument)
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
string command = $"az account get-access-token --output json --resource {resource}";
string command = tenantId switch
Copy link
Contributor

Choose a reason for hiding this comment

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

use ternary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a benefit other than style?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nope, no other benefit.

{
null => $"az account get-access-token --output json --resource {resource}",
_ => $"az account get-access-token --output json --resource {resource} -tenant {tenantId}"
};

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Expand Down
20 changes: 13 additions & 7 deletions sdk/identity/Azure.Identity/src/AzurePowerShellCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class AzurePowerShellCredential : TokenCredential
private static readonly string DefaultWorkingDir =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows;

private readonly AzurePowerShellCredentialOptions _options;

private const int ERROR_FILE_NOT_FOUND = 2;

/// <summary>
Expand All @@ -59,6 +61,7 @@ public AzurePowerShellCredential(AzurePowerShellCredentialOptions options) : thi
internal AzurePowerShellCredential(AzurePowerShellCredentialOptions options, CredentialPipeline pipeline, IProcessService processService)
{
UseLegacyPowerShell = false;
_options = options;
_pipeline = pipeline ?? CredentialPipeline.GetInstance(options);
_processService = processService ?? ProcessService.Default;
}
Expand Down Expand Up @@ -91,15 +94,15 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC

try
{
AccessToken token = await RequestAzurePowerShellAccessTokenAsync(async, requestContext.Scopes, cancellationToken).ConfigureAwait(false);
AccessToken token = await RequestAzurePowerShellAccessTokenAsync(async, requestContext, cancellationToken).ConfigureAwait(false);
return scope.Succeeded(token);
}
catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_FILE_NOT_FOUND && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
UseLegacyPowerShell = true;
try
{
AccessToken token = await RequestAzurePowerShellAccessTokenAsync(async, requestContext.Scopes, cancellationToken).ConfigureAwait(false);
AccessToken token = await RequestAzurePowerShellAccessTokenAsync(async, requestContext, cancellationToken).ConfigureAwait(false);
return scope.Succeeded(token);
}
catch (Exception e)
Expand All @@ -113,13 +116,14 @@ private async ValueTask<AccessToken> GetTokenImplAsync(bool async, TokenRequestC
}
}

private async ValueTask<AccessToken> RequestAzurePowerShellAccessTokenAsync(bool async, string[] scopes, CancellationToken cancellationToken)
private async ValueTask<AccessToken> RequestAzurePowerShellAccessTokenAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken)
{
string resource = ScopeUtilities.ScopesToResource(scopes);
string resource = ScopeUtilities.ScopesToResource(context.Scopes);

ScopeUtilities.ValidateScope(resource);
var tenantId = TenantIdResolver.Resolve(null, context, _options);

GetFileNameAndArguments(resource, out string fileName, out string argument);
GetFileNameAndArguments(resource, tenantId, out string fileName, out string argument);
ProcessStartInfo processStartInfo = GetAzurePowerShellProcessStartInfo(fileName, argument);
using var processRunner = new ProcessRunner(
_processService.Create(processStartInfo),
Expand Down Expand Up @@ -185,7 +189,7 @@ private static ProcessStartInfo GetAzurePowerShellProcessStartInfo(string fileNa
WorkingDirectory = DefaultWorkingDir
};

private void GetFileNameAndArguments(string resource, out string fileName, out string argument)
private void GetFileNameAndArguments(string resource, string tenantId, out string fileName, out string argument)
christothes marked this conversation as resolved.
Show resolved Hide resolved
{
string powershellExe = "pwsh -NonInteractive -EncodedCommand";

Expand All @@ -194,6 +198,8 @@ private void GetFileNameAndArguments(string resource, out string fileName, out s
powershellExe = "powershell -NonInteractive -EncodedCommand";
}

var tenantIdArg = tenantId == null ? string.Empty : $" -TenantId {tenantId}";

string command = @$"
$ErrorActionPreference = 'Stop'
[version]$minimumVersion = '2.2.0'
Expand All @@ -205,7 +211,7 @@ private void GetFileNameAndArguments(string resource, out string fileName, out s
exit
}}

$token = Get-AzAccessToken -ResourceUrl '{resource}'
$token = Get-AzAccessToken -ResourceUrl '{resource}'{tenantIdArg}

$x = $token | ConvertTo-Xml
return $x.Objects.FirstChild.OuterXml
Expand Down
Loading