From 5a6a0fa0e84f9a1d0e94c0e03ff68f282e8d79ee Mon Sep 17 00:00:00 2001 From: Scott Schaab Date: Wed, 14 Sep 2022 15:25:00 -0700 Subject: [PATCH 1/3] [Identity] Adding AdditionallyAllowedTenants to constrain multi-tenant auth (#31037) * [Identity] Adding AdditionallyAllowedTenants to constrain multi-tenant auth * updating API spec * adding dev-time credentials * adding user-auth credentials * refactor additional tenants to base options * adding default and environment credentials * update/add tests * update API spec * update changelog and breaking_changes * update assembly version * Update sdk/identity/Azure.Identity/CHANGELOG.md Co-authored-by: Heath Stewart * Update sdk/identity/Azure.Identity/CHANGELOG.md Co-authored-by: Heath Stewart * Update sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredentialOptions.cs Co-authored-by: Christopher Scott * Update sdk/identity/Azure.Identity/src/TenantIdResolver.cs Co-authored-by: Christopher Scott * Update sdk/identity/Azure.Identity/CHANGELOG.md Co-authored-by: Heath Stewart * fb * fb * fb * fb * fb * Update sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/AzureCliCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/CHANGELOG.md Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/TokenCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * Update sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredentialOptions.cs Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * updating troubleshooting.md * update snippets * undo snippet indent Co-authored-by: Heath Stewart Co-authored-by: Christopher Scott Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> --- .../Azure.Identity/BREAKING_CHANGES.md | 26 ++ sdk/identity/Azure.Identity/CHANGELOG.md | 25 ++ .../Azure.Identity/TROUBLESHOOTING.md | 8 + .../api/Azure.Identity.netstandard2.0.cs | 18 + .../Azure.Identity/src/Azure.Identity.csproj | 4 +- .../AuthorizationCodeCredential.cs | 6 +- .../AuthorizationCodeCredentialOptions.cs | 7 + .../src/Credentials/AzureCliCredential.cs | 10 +- .../Credentials/AzureCliCredentialOptions.cs | 11 +- .../Credentials/AzurePowerShellCredential.cs | 12 +- .../AzurePowerShellCredentialOptions.cs | 11 +- .../Credentials/ClientAssertionCredential.cs | 15 +- .../ClientAssertionCredentialOptions.cs | 7 + .../ClientCertificateCredential.cs | 8 +- .../ClientCertificateCredentialOptions.cs | 7 + .../src/Credentials/ClientSecretCredential.cs | 8 +- .../ClientSecretCredentialOptions.cs | 7 + .../src/Credentials/DefaultAzureCredential.cs | 65 +-- .../DefaultAzureCredentialOptions.cs | 179 +++++++- .../src/Credentials/DeviceCodeCredential.cs | 5 +- .../DeviceCodeCredentialOptions.cs | 8 + .../src/Credentials/EnvironmentCredential.cs | 12 + .../InteractiveBrowserCredential.cs | 11 +- .../InteractiveBrowserCredentialOptions.cs | 8 + .../Credentials/ManagedIdentityCredential.cs | 6 +- .../src/Credentials/OnBehalfOfCredential.cs | 7 +- .../OnBehalfOfCredentialOptions.cs | 7 + .../Credentials/SharedTokenCacheCredential.cs | 21 +- .../src/Credentials/TokenCredentialOptions.cs | 9 + .../Credentials/UsernamePasswordCredential.cs | 7 +- .../UsernamePasswordCredentialOptions.cs | 7 + .../Credentials/VisualStudioCodeCredential.cs | 13 +- .../VisualStudioCodeCredentialOptions.cs | 9 +- .../src/Credentials/VisualStudioCredential.cs | 14 +- .../VisualStudioCredentialOptions.cs | 11 +- .../src/DefaultAzureCredentialFactory.cs | 161 ++++++- .../src/EnvironmentVariables.cs | 4 + .../src/ManagedIdentityClient.cs | 5 +- .../Azure.Identity/src/TenantIdResolver.cs | 32 +- .../tests/AuthorizationCodeCredentialTests.cs | 29 +- .../tests/AzureCliCredentialTests.cs | 23 +- .../tests/AzurePowerShellCredentialsTests.cs | 23 +- .../tests/ClientAssertionCredentialTests.cs | 51 +++ .../tests/ClientCertificateCredentialTests.cs | 30 +- .../tests/ClientSecretCredentialTests.cs | 27 +- .../tests/CredentialTestBase.cs | 75 ++++ .../DefaultAzureCredentialFactoryTests.cs | 373 +++++++++++++++++ .../tests/DefaultAzureCredentialLiveTests.cs | 16 +- .../DefaultAzureCredentialOptionsTests.cs | 179 ++++++++ .../tests/DefaultAzureCredentialTests.cs | 393 +++--------------- .../tests/DeviceCodeCredentialTests.cs | 23 +- .../InteractiveBrowserCredentialTests.cs | 21 +- .../Mock/MockDefaultAzureCredentialFactory.cs | 33 +- .../tests/Mock/MockMsalPublicClient.cs | 11 + .../Mock/TestDefaultAzureCredentialFactory.cs | 61 ++- .../tests/OnBehalfOfCredentialTests.cs | 28 +- .../tests/SharedTokenCacheCredentialTests.cs | 8 +- .../tests/TenantIdResolverTests.cs | 65 ++- .../tests/TestAccessorExtensions.cs | 10 + .../tests/UsernamePasswordCredentialTests.cs | 28 +- .../VisualStudioCodeCredentialLiveTests.cs | 2 +- .../tests/VisualStudioCodeCredentialTests.cs | 38 +- .../tests/VisualStudioCredentialTests.cs | 28 +- .../tests/samples/BreakingChangesSnippets.cs | 20 + .../tests/samples/CustomCredentialSnippets.cs | 8 +- 65 files changed, 1821 insertions(+), 573 deletions(-) create mode 100644 sdk/identity/Azure.Identity/tests/ClientAssertionCredentialTests.cs create mode 100644 sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs create mode 100644 sdk/identity/Azure.Identity/tests/DefaultAzureCredentialOptionsTests.cs diff --git a/sdk/identity/Azure.Identity/BREAKING_CHANGES.md b/sdk/identity/Azure.Identity/BREAKING_CHANGES.md index 846d26424cd0..aa0523ac4c00 100644 --- a/sdk/identity/Azure.Identity/BREAKING_CHANGES.md +++ b/sdk/identity/Azure.Identity/BREAKING_CHANGES.md @@ -1,5 +1,31 @@ # Breaking Changes +## 1.7.0 + +### Behavioral change to credential types supporting multi-tenant authentication + +As of `Azure.Identity` 1.7.0, the default behavior of credentials supporting multi-tenant authentication has changed. Each of these credentials will throw an `AuthenticationFailedException` if the requested `TenantId` doesn't match the tenant ID originally configured on the credential. Apps must now do one of the following things: + +- Add all IDs, of tenants from which tokens should be acquired, to the `AdditionallyAllowedTenants` list in the credential options. For example: + +```C# Snippet:Identity_BreakingChanges_AddExplicitAdditionallyAllowedTenants +var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions +{ + AdditionallyAllowedTenants = { "", "" } +}); +``` + +- Add `*` to enable token acquisition from any tenant. This is the original behavior and is compatible with versions 1.5.0 through 1.6.1. For example: + +```C# Snippet:Identity_BreakingChanges_AddAllAdditionallyAllowedTenants +var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions +{ + AdditionallyAllowedTenants = { "*" } +}); +``` + +Note: Credential types which do not require a `TenantId` on construction will only throw `AuthenticationFailedException` when the application has provided a value for `TenantId` either in the options or via a constructor overload. If no `TenantId` is specified when constructing the credential, the credential will acquire tokens for any requested `TenantId` regardless of the value of `AdditionallyAllowedTenants`. + ## 1.4.0 ### Changed `ExcludeSharedTokenCacheCredential` default value from __false__ to __true__ on `DefaultAzureCredentialsOptions` diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 13b8b4c3868e..4cde941e8e0f 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -1,5 +1,30 @@ # Release History +## 1.7.0 (2022-09-13) + +### Features Added +- Added `AdditionallyAllowedTenants` to the following credential options to force explicit opt-in behavior for multi-tenant authentication: + - `AuthorizationCodeCredentialOptions` + - `AzureCliCredentialOptions` + - `AzurePowerShellCredentialOptions` + - `ClientAssertionCredentialOptions` + - `ClientCertificateCredentialOptions` + - `ClientSecretCredentialOptions` + - `DefaultAzureCredentialOptions` + - `OnBehalfOfCredentialOptions` + - `UsernamePasswordCredentialOptions` + - `VisualStudioCodeCredentialOptions` + - `VisualStudioCredentialOptions` +- Added `TenantId` to `DefaultAzureCredentialOptions` to avoid having to set `InteractiveBrowserTenantId`, `SharedTokenCacheTenantId`, `VisualStudioCodeTenantId`, and `VisualStudioTenantId` individually. + +### Breaking Changes +- Credential types supporting multi-tenant authentication will now throw `AuthenticationFailedException` if the requested tenant ID doesn't match the credential's tenant ID, and is not included in the `AdditionallyAllowedTenants` option. Applications must now explicitly add additional tenants to the `AdditionallyAllowedTenants` list, or add '*' to list, to enable acquiring tokens from tenants other than the originally specified tenant ID. See [BREAKING_CHANGES.md](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/BREAKING_CHANGES.md#170). + +## 1.7.0-beta.1 (2022-08-09) + +### Features Added +- `ManagedIdentityCredential` will now internally cache tokens. Apps can call `GetToken` or `GetTokenAsync` directly without needing to cache to avoid throttling. + ## 1.6.1 (2022-08-08) ### Bugs Fixed diff --git a/sdk/identity/Azure.Identity/TROUBLESHOOTING.md b/sdk/identity/Azure.Identity/TROUBLESHOOTING.md index 4755b278758c..ba1350f2a938 100644 --- a/sdk/identity/Azure.Identity/TROUBLESHOOTING.md +++ b/sdk/identity/Azure.Identity/TROUBLESHOOTING.md @@ -21,6 +21,7 @@ This troubleshooting guide covers failure investigation techniques, common error - [Troubleshoot VisualStudioCredential Authentication Issues](#troubleshoot-visualstudiocredential-authenticaton-issues) - [Troubleshoot AzureCliCredential Authentication Issues](#troubleshoot-azureclicredential-authentication-issues) - [Troubleshoot AzurePowerShellCredential Authentication Issues](#troubleshoot-azurepowershellcredential-authentication-issues) +- [Troubleshoot Multi Tenant Authentication Issues](#troubleshoot-multi-tenant-authentication-issues) - [Get Additional Help](#get-additional-help) ## Handle Azure Identity Exceptions @@ -246,6 +247,13 @@ Get-AzAccessToken -ResourceUrl "https://management.core.windows.net" ``` >Note that output of this command will contain a valid access token, and SHOULD NOT BE SHARED to avoid compromising account security. +## Troubleshoot Multi Tenant Authentication Issues +`AuthenticationFailedException` + +| Error Message |Description| Mitigation | +|---|---|---| +|The current credential is not configured to acquire tokens for tenant |The application must configure the credential to allow acquiring tokens from the requested tenant.|Add the requested tenant ID it to the AdditionallyAllowedTenants on the credential options, or add \"*\" to AdditionallyAllowedTenants to allow acquiring tokens for any tenant.

This exception was added as part of functional a breaking change to multi tenant authentication in version `1.7.0`. Users experiencing this error after upgrading can find details on the change and migration in [BREAKING_CHANGES.md](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/BREAKING_CHANGES.md#170) | + ## Get Additional Help Additional information on ways to reach out for support can be found in the [SUPPORT.md](https://github.com/Azure/azure-sdk-for-net/blob/main/SUPPORT.md) at the root of the repo. diff --git a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs index fcfd14424150..d2162c6c9098 100644 --- a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs +++ b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs @@ -39,6 +39,7 @@ public AuthorizationCodeCredential(string tenantId, string clientId, string clie public partial class AuthorizationCodeCredentialOptions : Azure.Identity.TokenCredentialOptions { public AuthorizationCodeCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public System.Uri RedirectUri { get { throw null; } set { } } } public static partial class AzureAuthorityHosts @@ -58,6 +59,7 @@ public AzureCliCredential(Azure.Identity.AzureCliCredentialOptions options) { } public partial class AzureCliCredentialOptions : Azure.Identity.TokenCredentialOptions { public AzureCliCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public string TenantId { get { throw null; } set { } } } public partial class AzurePowerShellCredential : Azure.Core.TokenCredential @@ -70,6 +72,7 @@ public AzurePowerShellCredential(Azure.Identity.AzurePowerShellCredentialOptions public partial class AzurePowerShellCredentialOptions : Azure.Identity.TokenCredentialOptions { public AzurePowerShellCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public string TenantId { get { throw null; } set { } } } public partial class ChainedTokenCredential : Azure.Core.TokenCredential @@ -89,6 +92,7 @@ public ClientAssertionCredential(string tenantId, string clientId, System.Func AdditionallyAllowedTenants { get { throw null; } } } public partial class ClientCertificateCredential : Azure.Core.TokenCredential { @@ -105,6 +109,7 @@ public ClientCertificateCredential(string tenantId, string clientId, string clie public partial class ClientCertificateCredentialOptions : Azure.Identity.TokenCredentialOptions { public ClientCertificateCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public bool SendCertificateChain { get { throw null; } set { } } public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } set { } } } @@ -120,6 +125,7 @@ public ClientSecretCredential(string tenantId, string clientId, string clientSec public partial class ClientSecretCredentialOptions : Azure.Identity.TokenCredentialOptions { public ClientSecretCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } set { } } } public partial class CredentialUnavailableException : Azure.Identity.AuthenticationFailedException @@ -138,6 +144,7 @@ public DefaultAzureCredential(bool includeInteractiveCredentials = false) { } public partial class DefaultAzureCredentialOptions : Azure.Identity.TokenCredentialOptions { public DefaultAzureCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public bool ExcludeAzureCliCredential { get { throw null; } set { } } public bool ExcludeAzurePowerShellCredential { get { throw null; } set { } } public bool ExcludeEnvironmentCredential { get { throw null; } set { } } @@ -147,12 +154,17 @@ public DefaultAzureCredentialOptions() { } public bool ExcludeVisualStudioCodeCredential { get { throw null; } set { } } public bool ExcludeVisualStudioCredential { get { throw null; } set { } } public string InteractiveBrowserCredentialClientId { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string InteractiveBrowserTenantId { get { throw null; } set { } } public string ManagedIdentityClientId { get { throw null; } set { } } public Azure.Core.ResourceIdentifier ManagedIdentityResourceId { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string SharedTokenCacheTenantId { get { throw null; } set { } } public string SharedTokenCacheUsername { get { throw null; } set { } } + public string TenantId { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string VisualStudioCodeTenantId { get { throw null; } set { } } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public string VisualStudioTenantId { get { throw null; } set { } } } public partial class DeviceCodeCredential : Azure.Core.TokenCredential @@ -173,6 +185,7 @@ public DeviceCodeCredential(System.Func AdditionallyAllowedTenants { get { throw null; } } public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } } public string ClientId { get { throw null; } set { } } public System.Func DeviceCodeCallback { get { throw null; } set { } } @@ -223,6 +236,7 @@ public InteractiveBrowserCredential(string tenantId, string clientId, Azure.Iden public partial class InteractiveBrowserCredentialOptions : Azure.Identity.TokenCredentialOptions { public InteractiveBrowserCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } } public string ClientId { get { throw null; } set { } } public bool DisableAutomaticAuthentication { get { throw null; } set { } } @@ -252,6 +266,7 @@ public OnBehalfOfCredential(string tenantId, string clientId, string clientSecre public partial class OnBehalfOfCredentialOptions : Azure.Identity.TokenCredentialOptions { public OnBehalfOfCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public bool SendCertificateChain { get { throw null; } set { } } public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } set { } } } @@ -333,6 +348,7 @@ public UsernamePasswordCredential(string username, string password, string tenan public partial class UsernamePasswordCredentialOptions : Azure.Identity.TokenCredentialOptions { public UsernamePasswordCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } set { } } } public partial class VisualStudioCodeCredential : Azure.Core.TokenCredential @@ -345,6 +361,7 @@ public VisualStudioCodeCredential(Azure.Identity.VisualStudioCodeCredentialOptio public partial class VisualStudioCodeCredentialOptions : Azure.Identity.TokenCredentialOptions { public VisualStudioCodeCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public string TenantId { get { throw null; } set { } } } public partial class VisualStudioCredential : Azure.Core.TokenCredential @@ -357,6 +374,7 @@ public VisualStudioCredential(Azure.Identity.VisualStudioCredentialOptions optio public partial class VisualStudioCredentialOptions : Azure.Identity.TokenCredentialOptions { public VisualStudioCredentialOptions() { } + public System.Collections.Generic.IList AdditionallyAllowedTenants { get { throw null; } } public string TenantId { get { throw null; } set { } } } } diff --git a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj index 5fbd51dd5ff7..5e1b464a381d 100644 --- a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj +++ b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj @@ -2,9 +2,9 @@ This is the implementation of the Azure SDK Client Library for Azure Identity Microsoft Azure.Identity Component - 1.6.1 + 1.7.0 - 1.6.0 + 1.6.1 Microsoft Azure Identity;$(PackageCommonTags) $(RequiredTargetFrameworks) $(NoWarn);3021;AZC0011 diff --git a/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredential.cs index 4f7c623c7a33..164b75b9fdce 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredential.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -25,6 +26,7 @@ public class AuthorizationCodeCredential : TokenCredential private readonly MsalConfidentialClient _client; private readonly string _redirectUri; private readonly string _tenantId; + private readonly string[] _additionallyAllowedTenantIds; /// /// Protected constructor for mocking. @@ -101,6 +103,8 @@ internal AuthorizationCodeCredential(string tenantId, string clientId, string cl clientSecret, _redirectUri, options); + + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -134,7 +138,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC try { AccessToken token; - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _additionallyAllowedTenantIds); if (_record is null) { diff --git a/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredentialOptions.cs index d58279ee1cc7..2b91afaf986e 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AuthorizationCodeCredentialOptions.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -20,5 +22,10 @@ public class AuthorizationCodeCredentialOptions : TokenCredentialOptions /// The redirect Uri that will be sent with the GetToken request. /// public Uri RedirectUri { get; set; } + + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs index 5cce5ae52da0..8165cd0da80e 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredential.cs @@ -46,9 +46,10 @@ public class AzureCliCredential : TokenCredential private readonly CredentialPipeline _pipeline; private readonly IProcessService _processService; - private readonly string _tenantId; private readonly bool _logPII; private readonly bool _logAccountDetails; + internal string TenantId { get; } + internal string[] AdditionallyAllowedTenantIds { get; } /// /// Create an instance of CliCredential class. @@ -72,7 +73,8 @@ internal AzureCliCredential(CredentialPipeline pipeline, IProcessService process _pipeline = pipeline; _path = !string.IsNullOrEmpty(EnvironmentVariables.Path) ? EnvironmentVariables.Path : DefaultPath; _processService = processService ?? ProcessService.Default; - _tenantId = options?.TenantId; + TenantId = options?.TenantId; + AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -115,7 +117,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC private async ValueTask RequestCliAccessTokenAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken) { string resource = ScopeUtilities.ScopesToResource(context.Scopes); - string tenantId = TenantIdResolver.Resolve(_tenantId, context); + string tenantId = TenantIdResolver.Resolve(TenantId, context, AdditionallyAllowedTenantIds); ScopeUtilities.ValidateScope(resource); @@ -167,7 +169,7 @@ private async ValueTask RequestCliAccessTokenAsync(bool async, Toke if (_logAccountDetails) { var accountDetails = TokenHelper.ParseAccountInfoFromToken(token.Token); - AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(accountDetails.ClientId, accountDetails.TenantId ?? _tenantId, accountDetails.Upn, accountDetails.ObjectId); + AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(accountDetails.ClientId, accountDetails.TenantId ?? TenantId, accountDetails.Upn, accountDetails.ObjectId); } return token; diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredentialOptions.cs index 03b3f90859b3..f876e98c59d2 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzureCliCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -9,8 +11,15 @@ namespace Azure.Identity public class AzureCliCredentialOptions : TokenCredentialOptions { /// - /// The Azure Active Directory tenant (directory) Id of the service principal + /// The ID of the tenant to which the credential will authenticate by default. If not specified, the credential will authenticate to any requested tenant, and will default to the tenant provided to the 'az login' command. /// public string TenantId { get; set; } + + /// + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for this option will have no effect, and the credential will acquire tokens for any requested tenant. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs index 069ab0f5f9dd..1b40126b8623 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs @@ -36,7 +36,8 @@ public class AzurePowerShellCredential : TokenCredential private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.System); private const string DefaultWorkingDirNonWindows = "/bin/"; private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows; - private readonly string _tenantId; + internal string TenantId { get; } + internal string[] AdditionallyAllowedTenantIds { get; } private readonly bool _logPII; private readonly bool _logAccountDetails; internal const string AzurePowerShellNotLogInError = "Please run 'Connect-AzAccount' to set up account."; @@ -62,9 +63,10 @@ internal AzurePowerShellCredential(AzurePowerShellCredentialOptions options, Cre UseLegacyPowerShell = false; _logPII = options?.IsLoggingPIIEnabled ?? false; _logAccountDetails = options?.Diagnostics?.IsAccountIdentifierLoggingEnabled ?? false; - _tenantId = options?.TenantId; + TenantId = options?.TenantId; _pipeline = pipeline ?? CredentialPipeline.GetInstance(options); _processService = processService ?? ProcessService.Default; + AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -99,7 +101,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC if (_logAccountDetails) { var accountDetails = TokenHelper.ParseAccountInfoFromToken(token.Token); - AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(accountDetails.ClientId, accountDetails.TenantId ?? _tenantId, accountDetails.Upn, accountDetails.ObjectId); + AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(accountDetails.ClientId, accountDetails.TenantId ?? TenantId, accountDetails.Upn, accountDetails.ObjectId); } return scope.Succeeded(token); } @@ -115,7 +117,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC if (_logAccountDetails) { - AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(null, _tenantId, null, null); + AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(null, TenantId, null, null); } return scope.Succeeded(token); @@ -136,7 +138,7 @@ private async ValueTask RequestAzurePowerShellAccessTokenAsync(bool string resource = ScopeUtilities.ScopesToResource(context.Scopes); ScopeUtilities.ValidateScope(resource); - var tenantId = TenantIdResolver.Resolve(_tenantId, context); + var tenantId = TenantIdResolver.Resolve(TenantId, context, AdditionallyAllowedTenantIds); GetFileNameAndArguments(resource, tenantId, out string fileName, out string argument); ProcessStartInfo processStartInfo = GetAzurePowerShellProcessStartInfo(fileName, argument); diff --git a/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredentialOptions.cs index 4853f19fefbd..0a22f998177b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -9,8 +11,15 @@ namespace Azure.Identity public class AzurePowerShellCredentialOptions : TokenCredentialOptions { /// - /// The Azure Active Directory tenant (directory) Id of the service principal + /// The ID of the tenant to which the credential will authenticate by default. If not specified, the credential will authenticate to any requested tenant, and will default to the tenant provided to the 'Connect-AzAccount' cmdlet. /// public string TenantId { get; set; } + + /// + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for , this option will have no effect, and the credential will acquire tokens for any requested tenant. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredential.cs index 16905d339e07..210c7dc2d02f 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredential.cs @@ -17,9 +17,12 @@ namespace Azure.Identity /// public class ClientAssertionCredential : TokenCredential { + private readonly string[] _additionallyAllowedTenantIds; + internal string TenantId { get; } internal string ClientId { get; } internal MsalConfidentialClient Client { get; } + internal CredentialPipeline Pipeline { get; } internal bool AllowMultiTenantAuthentication { get; } /// @@ -43,6 +46,8 @@ public ClientAssertionCredential(string tenantId, string clientId, Func @@ -60,6 +65,8 @@ public ClientAssertionCredential(string tenantId, string clientId, Func ClientId = clientId; Client = options?.MsalClient ?? new MsalConfidentialClient(options?.Pipeline ?? CredentialPipeline.GetInstance(options), tenantId, clientId, assertionCallback, options); + Pipeline = options?.Pipeline ?? Client.Pipeline; + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -70,11 +77,11 @@ public ClientAssertionCredential(string tenantId, string clientId, Func /// An which can be used to authenticate service client calls. public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { - using CredentialDiagnosticScope scope = Client.Pipeline.StartGetTokenScope("ClientAssertionCredential.GetToken", requestContext); + using CredentialDiagnosticScope scope = Pipeline.StartGetTokenScope("ClientAssertionCredential.GetToken", requestContext); try { - var tenantId = TenantIdResolver.Resolve(TenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = Client.AcquireTokenForClientAsync(requestContext.Scopes, tenantId, false, cancellationToken).EnsureCompleted(); @@ -94,11 +101,11 @@ public override AccessToken GetToken(TokenRequestContext requestContext, Cancell /// An which can be used to authenticate service client calls. public async override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { - using CredentialDiagnosticScope scope = Client.Pipeline.StartGetTokenScope("ClientAssertionCredential.GetToken", requestContext); + using CredentialDiagnosticScope scope = Pipeline.StartGetTokenScope("ClientAssertionCredential.GetToken", requestContext); try { - var tenantId = TenantIdResolver.Resolve(TenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = await Client.AcquireTokenForClientAsync(requestContext.Scopes, tenantId, true, cancellationToken).ConfigureAwait(false); diff --git a/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredentialOptions.cs index 075d0a1a58cf..359b7fd08296 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ClientAssertionCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -11,5 +13,10 @@ public class ClientAssertionCredentialOptions : TokenCredentialOptions internal CredentialPipeline Pipeline { get; set; } internal MsalConfidentialClient MsalClient { get; set; } + + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredential.cs index 09a2614eb5dd..40aaa2008ffa 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredential.cs @@ -36,6 +36,8 @@ public class ClientCertificateCredential : TokenCredential private readonly CredentialPipeline _pipeline; + private readonly string[] _additionallyAllowedTenantIds; + /// /// Protected constructor for mocking. /// @@ -160,6 +162,8 @@ internal ClientCertificateCredential( certificateProvider, certCredOptions?.SendCertificateChain ?? false, options); + + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -174,7 +178,7 @@ public override AccessToken GetToken(TokenRequestContext requestContext, Cancell try { - var tenantId = TenantIdResolver.Resolve(TenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = Client.AcquireTokenForClientAsync(requestContext.Scopes, tenantId, false, cancellationToken).EnsureCompleted(); return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn)); @@ -197,7 +201,7 @@ public override async ValueTask GetTokenAsync(TokenRequestContext r try { - var tenantId = TenantIdResolver.Resolve(TenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = await Client .AcquireTokenForClientAsync(requestContext.Scopes, tenantId, true, cancellationToken) .ConfigureAwait(false); diff --git a/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredentialOptions.cs index 3fef89a2c225..bf06b51396d0 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ClientCertificateCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -17,5 +19,10 @@ public class ClientCertificateCredentialOptions : TokenCredentialOptions, IToken /// Will include x5c header in client claims when acquiring a token to enable subject name / issuer based authentication for the . /// public bool SendCertificateChain { get; set; } + + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredential.cs index 4118405de442..91af8b65f38e 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredential.cs @@ -19,6 +19,8 @@ public class ClientSecretCredential : TokenCredential { private readonly CredentialPipeline _pipeline; + private readonly string[] _additionallyAllowedTenantIds; + internal MsalConfidentialClient Client { get; } /// @@ -95,6 +97,8 @@ internal ClientSecretCredential(string tenantId, string clientId, string clientS clientSecret, null, options); + + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -109,7 +113,7 @@ public override async ValueTask GetTokenAsync(TokenRequestContext r try { - var tenantId = TenantIdResolver.Resolve(TenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = await Client.AcquireTokenForClientAsync(requestContext.Scopes, tenantId, true, cancellationToken).ConfigureAwait(false); return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn)); @@ -132,7 +136,7 @@ public override AccessToken GetToken(TokenRequestContext requestContext, Cancell try { - var tenantId = TenantIdResolver.Resolve(TenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = Client.AcquireTokenForClientAsync(requestContext.Scopes, tenantId, false, cancellationToken).EnsureCompleted(); return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn)); diff --git a/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredentialOptions.cs index 48f41b0090b6..65eb91713ce0 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ClientSecretCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -12,5 +14,10 @@ public class ClientSecretCredentialOptions : TokenCredentialOptions, ITokenCache /// Specifies the to be used by the credential. If not options are specified, the token cache will not be persisted to disk. /// public TokenCachePersistenceOptions TokenCachePersistenceOptions { get; set; } + + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredential.cs index 496e44d1a682..f7f02214e90b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredential.cs @@ -50,7 +50,6 @@ public class DefaultAzureCredential : TokenCredential private const string Troubleshooting = "See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/defaultazurecredential/troubleshoot"; private const string DefaultExceptionMessage = "DefaultAzureCredential failed to retrieve a token from the included credentials. " + Troubleshooting; private const string UnhandledExceptionMessage = "DefaultAzureCredential authentication failed due to an unhandled exception: "; - private static readonly TokenCredential[] s_defaultCredentialChain = GetDefaultAzureCredentialChain(new DefaultAzureCredentialFactory(null), new DefaultAzureCredentialOptions()); private readonly CredentialPipeline _pipeline; private readonly AsyncLockWithValue _credentialLock; @@ -75,14 +74,14 @@ public DefaultAzureCredential(bool includeInteractiveCredentials = false) public DefaultAzureCredential(DefaultAzureCredentialOptions options) // we call ValidateAuthoriyHostOption to validate that we have a valid authority host before constructing the DAC chain // if we don't validate this up front it will end up throwing an exception out of a static initializer which obscures the error. - : this(new DefaultAzureCredentialFactory(ValidateAuthorityHostOption(options)), options) + : this(new DefaultAzureCredentialFactory(ValidateAuthorityHostOption(options))) { } - internal DefaultAzureCredential(DefaultAzureCredentialFactory factory, DefaultAzureCredentialOptions options) + internal DefaultAzureCredential(DefaultAzureCredentialFactory factory) { _pipeline = factory.Pipeline; - _sources = GetDefaultAzureCredentialChain(factory, options); + _sources = factory.CreateCredentialChain(); _credentialLock = new AsyncLockWithValue(); } @@ -183,64 +182,6 @@ private static async ValueTask GetTokenFromCredentialAsync(TokenCre throw CredentialUnavailableException.CreateAggregateException(DefaultExceptionMessage, exceptions); } - private static TokenCredential[] GetDefaultAzureCredentialChain(DefaultAzureCredentialFactory factory, DefaultAzureCredentialOptions options) - { - if (options is null) - { - return s_defaultCredentialChain; - } - - int i = 0; - TokenCredential[] chain = new TokenCredential[8]; - - if (!options.ExcludeEnvironmentCredential) - { - chain[i++] = factory.CreateEnvironmentCredential(); - } - - if (!options.ExcludeManagedIdentityCredential) - { - chain[i++] = factory.CreateManagedIdentityCredential(options); - } - - if (!options.ExcludeSharedTokenCacheCredential) - { - chain[i++] = factory.CreateSharedTokenCacheCredential(options.SharedTokenCacheTenantId, options.SharedTokenCacheUsername); - } - - if (!options.ExcludeVisualStudioCredential) - { - chain[i++] = factory.CreateVisualStudioCredential(options.VisualStudioTenantId); - } - - if (!options.ExcludeVisualStudioCodeCredential) - { - chain[i++] = factory.CreateVisualStudioCodeCredential(options.VisualStudioCodeTenantId); - } - - if (!options.ExcludeAzureCliCredential) - { - chain[i++] = factory.CreateAzureCliCredential(); - } - - if (!options.ExcludeAzurePowerShellCredential) - { - chain[i++] = factory.CreateAzurePowerShellCredential(); - } - - if (!options.ExcludeInteractiveBrowserCredential) - { - chain[i++] = factory.CreateInteractiveBrowserCredential(options.InteractiveBrowserTenantId, options.InteractiveBrowserCredentialClientId); - } - - if (i == 0) - { - throw new ArgumentException("At least one credential type must be included in the authentication flow.", nameof(options)); - } - - return chain; - } - private static DefaultAzureCredentialOptions ValidateAuthorityHostOption(DefaultAzureCredentialOptions options) { Validations.ValidateAuthorityHost(options?.AuthorityHost ?? AzureAuthorityHosts.GetDefault()); diff --git a/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs index 01e701042af2..2c6a042fc12b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs @@ -2,6 +2,9 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Net; using Azure.Core; namespace Azure.Identity @@ -11,12 +14,90 @@ namespace Azure.Identity /// public class DefaultAzureCredentialOptions : TokenCredentialOptions { + private struct UpdateTracker + { + private bool _updated; + private T _value; + + public UpdateTracker(T initialValue) + { + _value = initialValue; + _updated = false; + } + + public T Value + { + get => _value; + set + { + _value = value; + _updated = true; + } + } + + public bool Updated => _updated; + } + + private UpdateTracker _tenantId = new UpdateTracker(GetNonEmptyStringOrNull(EnvironmentVariables.TenantId)); + private UpdateTracker _interactiveBrowserTenantId = new UpdateTracker(GetNonEmptyStringOrNull(EnvironmentVariables.TenantId)); + private UpdateTracker _sharedTokenCacheTenantId = new UpdateTracker(GetNonEmptyStringOrNull(EnvironmentVariables.TenantId)); + private UpdateTracker _visualStudioTenantId = new UpdateTracker(GetNonEmptyStringOrNull(EnvironmentVariables.TenantId)); + private UpdateTracker _visualStudioCodeTenantId = new UpdateTracker(GetNonEmptyStringOrNull(EnvironmentVariables.TenantId)); + + /// + /// The ID of the tenant to which the credential will authenticate by default. If not specified, the credential will authenticate to any requested tenant, and will default to the tenant to which the chosen authentication method was originally authenticated. + /// + public string TenantId + { + get => _tenantId.Value; + set + { + if (_interactiveBrowserTenantId.Updated && value != _interactiveBrowserTenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and InteractiveBrowserTenantId. TenantId is preferred, and is functionally equivalent. InteractiveBrowserTenantId exists only to provide backwards compatibility."); + } + + if (_sharedTokenCacheTenantId.Updated && value != _sharedTokenCacheTenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and SharedTokenCacheTenantId. TenantId is preferred, and is functionally equivalent. SharedTokenCacheTenantId exists only to provide backwards compatibility."); + } + + if (_visualStudioTenantId.Updated && value != _visualStudioTenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and VisualStudioTenantId. TenantId is preferred, and is functionally equivalent. VisualStudioTenantId exists only to provide backwards compatibility."); + } + + if (_visualStudioCodeTenantId.Updated && value != _visualStudioCodeTenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and VisualStudioCodeTenantId. TenantId is preferred, and is functionally equivalent. VisualStudioCodeTenantId exists only to provide backwards compatibility."); + } + _tenantId.Value = value; + _interactiveBrowserTenantId.Value = value; + _sharedTokenCacheTenantId.Value = value; + _visualStudioCodeTenantId.Value = value; + _visualStudioTenantId.Value = value; + } + } + /// /// The tenant id of the user to authenticate, in the case the authenticates through, the /// . The default is null and will authenticate users to their default tenant. /// The value can also be set by setting the environment variable AZURE_TENANT_ID. /// - public string InteractiveBrowserTenantId { get; set; } = GetNonEmptyStringOrNull(EnvironmentVariables.TenantId); + [EditorBrowsable(EditorBrowsableState.Never)] + public string InteractiveBrowserTenantId + { + get => _interactiveBrowserTenantId.Value; + set + { + if (_tenantId.Updated && value != _tenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and InteractiveBrowserTenantId. TenantId is preferred, and is functionally equivalent. InteractiveBrowserTenantId exists only to provide backwards compatibility."); + } + + _interactiveBrowserTenantId.Value = value; + } + } /// /// Specifies the tenant id of the preferred authentication account, to be retrieved from the shared token cache for single sign on authentication with @@ -26,21 +107,68 @@ public class DefaultAzureCredentialOptions : TokenCredentialOptions /// If multiple accounts are found in the shared token cache and no value is specified, or the specified value matches no accounts in /// the cache the SharedTokenCacheCredential will not be used for authentication. /// - public string SharedTokenCacheTenantId { get; set; } = GetNonEmptyStringOrNull(EnvironmentVariables.TenantId); + [EditorBrowsable(EditorBrowsableState.Never)] + public string SharedTokenCacheTenantId + { + get => _sharedTokenCacheTenantId.Value; + set + { + if (_tenantId.Updated && value != _tenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and SharedTokenCacheTenantId. TenantId is preferred, and is functionally equivalent. SharedTokenCacheTenantId exists only to provide backwards compatibility."); + } + + _sharedTokenCacheTenantId.Value = value; + } + } /// /// The tenant id of the user to authenticate, in the case the authenticates through, the /// . The default is null and will authenticate users to their default tenant. /// The value can also be set by setting the environment variable AZURE_TENANT_ID. /// - public string VisualStudioTenantId { get; set; } = GetNonEmptyStringOrNull(EnvironmentVariables.TenantId); + [EditorBrowsable(EditorBrowsableState.Never)] + public string VisualStudioTenantId + { + get => _visualStudioTenantId.Value; + set + { + if (_tenantId.Updated && value != _tenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and VisualStudioTenantId. TenantId is preferred, and is functionally equivalent. VisualStudioTenantId exists only to provide backwards compatibility."); + } + + _visualStudioTenantId.Value = value; + } + } /// - /// The tenant id of the user to authenticate, in the case the authenticates through, the + /// The tenant ID of the user to authenticate, in the case the authenticates through, the /// . The default is null and will authenticate users to their default tenant. /// The value can also be set by setting the environment variable AZURE_TENANT_ID. /// - public string VisualStudioCodeTenantId { get; set; } = GetNonEmptyStringOrNull(EnvironmentVariables.TenantId); + [EditorBrowsable(EditorBrowsableState.Never)] + public string VisualStudioCodeTenantId + { + get => _visualStudioCodeTenantId.Value; + set + { + if (_tenantId.Updated && value != _tenantId.Value) + { + throw new InvalidOperationException("Applications should not set both TenantId and VisualStudioCodeTenantId. TenantId is preferred, and is functionally equivalent. VisualStudioCodeTenantId exists only to provide backwards compatibility."); + } + + _visualStudioCodeTenantId.Value = value; + } + } + + /// + /// Specifies tenants in addition to the specified , , , , for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for any of the above tenants, this option will have no effect on that authentication method, and the credential will acquire tokens for any requested tenant when using that method. + /// This value can also be set by setting the environment variable AZURE_ADDITOINAL_ALLOWED_TENANTS. + /// + public IList AdditionallyAllowedTenants { get; private set; } = EnvironmentVariables.AdditionallyAllowedTenants; /// /// Specifies the preferred authentication account to be retrieved from the shared token cache for single sign on authentication with @@ -108,14 +236,45 @@ public class DefaultAzureCredentialOptions : TokenCredentialOptions /// public bool ExcludeVisualStudioCodeCredential { get; set; } - private static string GetNonEmptyStringOrNull(string str) - { - return !string.IsNullOrEmpty(str) ? str : null; - } - /// /// Specifies whether the will be excluded from the authentication flow. /// public bool ExcludeAzurePowerShellCredential { get; set; } + + internal DefaultAzureCredentialOptions ShallowClone() + { + var options = new DefaultAzureCredentialOptions + { + _tenantId = _tenantId, + _interactiveBrowserTenantId = _interactiveBrowserTenantId, + _sharedTokenCacheTenantId = _sharedTokenCacheTenantId, + _visualStudioTenantId = _visualStudioTenantId, + _visualStudioCodeTenantId = _visualStudioCodeTenantId, + SharedTokenCacheUsername = SharedTokenCacheUsername, + InteractiveBrowserCredentialClientId = InteractiveBrowserCredentialClientId, + ManagedIdentityClientId = ManagedIdentityClientId, + ManagedIdentityResourceId = ManagedIdentityResourceId, + ExcludeEnvironmentCredential = ExcludeEnvironmentCredential, + ExcludeManagedIdentityCredential = ExcludeManagedIdentityCredential, + ExcludeSharedTokenCacheCredential = ExcludeSharedTokenCacheCredential, + ExcludeInteractiveBrowserCredential = ExcludeInteractiveBrowserCredential, + ExcludeAzureCliCredential = ExcludeAzureCliCredential, + ExcludeVisualStudioCredential = ExcludeVisualStudioCredential, + ExcludeVisualStudioCodeCredential = ExcludeVisualStudioCodeCredential, + ExcludeAzurePowerShellCredential = ExcludeAzurePowerShellCredential, + AuthorityHost = AuthorityHost + }; + + foreach (var addlTenant in AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + return options; + } + private static string GetNonEmptyStringOrNull(string str) + { + return !string.IsNullOrEmpty(str) ? str : null; + } } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredential.cs index 665ca7169243..bc80bb78c9d1 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredential.cs @@ -18,6 +18,7 @@ namespace Azure.Identity public class DeviceCodeCredential : TokenCredential { private readonly string _tenantId; + private readonly string[] _additionallyAllowedTenantIds; internal MsalPublicClient Client { get; set; } internal string ClientId { get; } internal bool DisableAutomaticAuthentication { get; } @@ -92,6 +93,7 @@ internal DeviceCodeCredential(Func devi ClientId, AzureAuthorityHosts.GetDeviceCodeRedirectUri(Pipeline.AuthorityHost).AbsoluteUri, options); + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -197,11 +199,12 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC { Exception inner = null; + var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _additionallyAllowedTenantIds); + if (Record != null) { try { - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); AuthenticationResult result = await Client .AcquireTokenSilentAsync(requestContext.Scopes, requestContext.Claims, Record, tenantId, async, cancellationToken) .ConfigureAwait(false); diff --git a/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredentialOptions.cs index a47f2b06fd6d..d160bc3f6b53 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/DeviceCodeCredentialOptions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -29,6 +30,13 @@ public string TenantId set { _tenantId = Validations.ValidateTenantId(value, allowNull: true); } } + /// + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for , this option will have no effect, and the credential will acquire tokens for any requested tenant. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; + /// /// The client ID of the application used to authenticate the user. If not specified the user will be authenticated with an Azure development application. /// diff --git a/sdk/identity/Azure.Identity/src/Credentials/EnvironmentCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/EnvironmentCredential.cs index 24cc6cccb273..f3f53d9a4f28 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/EnvironmentCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/EnvironmentCredential.cs @@ -7,6 +7,8 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; +using System.Linq; +using System.Collections.Generic; namespace Azure.Identity { @@ -65,6 +67,15 @@ internal EnvironmentCredential(CredentialPipeline pipeline, TokenCredentialOptio string username = EnvironmentVariables.Username; string password = EnvironmentVariables.Password; + // Since the AdditionallyAllowedTenantsCore is internal it cannot be set by the application. + // Currently this is only set by the DefaultAzureCredential where it will default to the value + // of EnvironmentVariables.AdditionallyAllowedTenants, but can also be altered by the application. + // In either case we don't want to alter it. + if (_options.AdditionallyAllowedTenantsCore.Count == 0) + { + _options.AdditionallyAllowedTenantsCore = EnvironmentVariables.AdditionallyAllowedTenants; + } + if (!string.IsNullOrEmpty(tenantId) && !string.IsNullOrEmpty(clientId)) { if (!string.IsNullOrEmpty(clientSecret)) @@ -85,6 +96,7 @@ internal EnvironmentCredential(CredentialPipeline pipeline, TokenCredentialOptio AuthorityHost = _options.AuthorityHost, IsLoggingPIIEnabled = _options.IsLoggingPIIEnabled, Transport = _options.Transport, + AdditionallyAllowedTenantsCore = new List(_options.AdditionallyAllowedTenantsCore), SendCertificateChain = sendCertificateChain }; Credential = new ClientCertificateCredential(tenantId, clientId, clientCertificatePath, clientCertificateCredentialOptions, _pipeline, null); diff --git a/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredential.cs index 66b7c8bb2a0a..e5519b85f45a 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredential.cs @@ -17,7 +17,8 @@ namespace Azure.Identity /// public class InteractiveBrowserCredential : TokenCredential { - private readonly string _tenantId; + internal string TenantId { get; } + internal string[] AdditionallyAllowedTenantIds { get; } internal string ClientId { get; } internal string LoginHint { get; } internal MsalPublicClient Client { get; } @@ -76,11 +77,12 @@ internal InteractiveBrowserCredential(string tenantId, string clientId, TokenCre Argument.AssertNotNull(clientId, nameof(clientId)); ClientId = clientId; - _tenantId = tenantId; + TenantId = tenantId; Pipeline = pipeline ?? CredentialPipeline.GetInstance(options); LoginHint = (options as InteractiveBrowserCredentialOptions)?.LoginHint; var redirectUrl = (options as InteractiveBrowserCredentialOptions)?.RedirectUri?.AbsoluteUri ?? Constants.DefaultRedirectUrl; Client = client ?? new MsalPublicClient(Pipeline, tenantId, clientId, redirectUrl, options); + AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -177,11 +179,12 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC { Exception inner = null; + var tenantId = TenantIdResolver.Resolve(TenantId ?? Record?.TenantId, requestContext, AdditionallyAllowedTenantIds); + if (Record != null) { try { - var tenantId = TenantIdResolver.Resolve(_tenantId ?? Record.TenantId, requestContext); AuthenticationResult result = await Client .AcquireTokenSilentAsync(requestContext.Scopes, requestContext.Claims, Record, tenantId, async, cancellationToken) .ConfigureAwait(false); @@ -215,7 +218,7 @@ private async Task GetTokenViaBrowserLoginAsync(TokenRequestContext _ => Prompt.NoPrompt }; - var tenantId = TenantIdResolver.Resolve(_tenantId ?? Record?.TenantId, context); + var tenantId = TenantIdResolver.Resolve(TenantId ?? Record?.TenantId, context, AdditionallyAllowedTenantIds); AuthenticationResult result = await Client .AcquireTokenInteractiveAsync(context.Scopes, context.Claims, prompt, LoginHint, tenantId, async, cancellationToken) .ConfigureAwait(false); diff --git a/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredentialOptions.cs index 4c6b59531022..74798f6e8d83 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/InteractiveBrowserCredentialOptions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading; namespace Azure.Identity @@ -28,6 +29,13 @@ public string TenantId set { _tenantId = Validations.ValidateTenantId(value, allowNull: true); } } + /// + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for , this option will have no effect, and the credential will acquire tokens for any requested tenant. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; + /// /// The client ID of the application used to authenticate the user. If not specified the user will be authenticated with an Azure development application. /// diff --git a/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs index 2ef15ee10302..94b32042fc2d 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/ManagedIdentityCredential.cs @@ -24,7 +24,7 @@ public class ManagedIdentityCredential : TokenCredential internal const string MsiUnavailableError = "No managed identity endpoint found."; private readonly CredentialPipeline _pipeline; - private readonly ManagedIdentityClient _client; + internal ManagedIdentityClient Client { get; } private readonly string _clientId; private readonly bool _logAccountDetails; @@ -82,7 +82,7 @@ internal ManagedIdentityCredential(ResourceIdentifier resourceId, CredentialPipe internal ManagedIdentityCredential(ManagedIdentityClient client) { _pipeline = client.Pipeline; - _client = client; + Client = client; } /// @@ -113,7 +113,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC try { - AccessToken result = await _client.AuthenticateAsync(async, requestContext, cancellationToken).ConfigureAwait(false); + AccessToken result = await Client.AuthenticateAsync(async, requestContext, cancellationToken).ConfigureAwait(false); if (_logAccountDetails) { var accountDetails = TokenHelper.ParseAccountInfoFromToken(result.Token); diff --git a/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredential.cs index 2d585b7e732d..69b8f255b74b 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredential.cs @@ -22,6 +22,7 @@ public class OnBehalfOfCredential : TokenCredential private readonly string _clientId; private readonly string _clientSecret; private readonly UserAssertion _userAssertion; + private readonly string[] _additionallyAllowedTenantIds; /// /// Protected constructor for mocking. @@ -125,6 +126,8 @@ internal OnBehalfOfCredential( certificateProvider, options.SendCertificateChain, options); + + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } internal OnBehalfOfCredential( @@ -146,6 +149,8 @@ internal OnBehalfOfCredential( _clientSecret = clientSecret; _userAssertion = new UserAssertion(userAssertion); _client = client ?? new MsalConfidentialClient(_pipeline, _tenantId, _clientId, _clientSecret, null, options); + + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenants); } /// @@ -162,7 +167,7 @@ internal async ValueTask GetTokenInternalAsync(TokenRequestContext try { - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = await _client .AcquireTokenOnBehalfOfAsync(requestContext.Scopes, tenantId, _userAssertion, async, cancellationToken) diff --git a/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredentialOptions.cs index cd179c191498..26470e5d788c 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/OnBehalfOfCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -17,5 +19,10 @@ public class OnBehalfOfCredentialOptions : TokenCredentialOptions, ITokenCacheOp /// Will include x5c header in client claims when acquiring a token to enable subject name / issuer based authentication for the . /// public bool SendCertificateChain { get; set; } + + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/SharedTokenCacheCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/SharedTokenCacheCredential.cs index 83301a0c0219..abe9e1eb00f0 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/SharedTokenCacheCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/SharedTokenCacheCredential.cs @@ -25,12 +25,13 @@ public class SharedTokenCacheCredential : TokenCredential internal const string MultipleMatchingAccountsInCacheMessage = "SharedTokenCacheCredential authentication unavailable. Multiple accounts matching the specified{0}{1} were found in the cache."; private static readonly SharedTokenCacheCredentialOptions s_DefaultCacheOptions = new SharedTokenCacheCredentialOptions(); private readonly CredentialPipeline _pipeline; - private readonly string _tenantId; - private readonly string _username; private readonly bool _skipTenantValidation; private readonly AuthenticationRecord _record; private readonly AsyncLockWithValue _accountAsyncLock; + internal string TenantId { get; } + internal string Username { get; } + internal MsalPublicClient Client { get; } /// @@ -68,8 +69,8 @@ internal SharedTokenCacheCredential(string tenantId, string username, TokenCrede internal SharedTokenCacheCredential(string tenantId, string username, TokenCredentialOptions options, CredentialPipeline pipeline, MsalPublicClient client) { - _tenantId = tenantId; - _username = username; + TenantId = tenantId; + Username = username; var sharedTokenCredentialOptions = options as SharedTokenCacheCredentialOptions; _skipTenantValidation = sharedTokenCredentialOptions?.EnableGuestTenantAuthentication ?? false; _record = sharedTokenCredentialOptions?.AuthenticationRecord; @@ -111,7 +112,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC try { - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, TenantIdResolver.AllTenants); IAccount account = await GetAccountAsync(tenantId, async, cancellationToken).ConfigureAwait(false); AuthenticationResult result = await Client.AcquireTokenSilentAsync(requestContext.Scopes, requestContext.Claims, account, tenantId, async, cancellationToken).ConfigureAwait(false); return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn)); @@ -120,7 +121,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC { throw scope.FailWrapAndThrow( new CredentialUnavailableException( - $"{nameof(SharedTokenCacheCredential)} authentication unavailable. Token acquisition failed for user {_username}. Ensure that you have authenticated with a developer tool that supports Azure single sign on.", + $"{nameof(SharedTokenCacheCredential)} authentication unavailable. Token acquisition failed for user {Username}. Ensure that you have authenticated with a developer tool that supports Azure single sign on.", ex)); } catch (Exception e) @@ -155,7 +156,7 @@ private async ValueTask GetAccountAsync(string tenantId, bool async, C // filter the accounts to those matching the specified user and tenant List filteredAccounts = accounts.Where(a => // if _username is specified it must match the account - (string.IsNullOrEmpty(_username) || string.Compare(a.Username, _username, StringComparison.OrdinalIgnoreCase) == 0) + (string.IsNullOrEmpty(Username) || string.Compare(a.Username, Username, StringComparison.OrdinalIgnoreCase) == 0) && // if _skipTenantValidation is false and _tenantId is specified it must match the account (_skipTenantValidation || string.IsNullOrEmpty(tenantId) || string.Compare(a.HomeAccountId?.TenantId, tenantId, StringComparison.OrdinalIgnoreCase) == 0) @@ -181,13 +182,13 @@ private async ValueTask GetAccountAsync(string tenantId, bool async, C private string GetCredentialUnavailableMessage(List filteredAccounts) { - if (string.IsNullOrEmpty(_username) && string.IsNullOrEmpty(_tenantId)) + if (string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(TenantId)) { return string.Format(CultureInfo.InvariantCulture, MultipleAccountsInCacheMessage); } - var usernameStr = string.IsNullOrEmpty(_username) ? string.Empty : $" username: {_username}"; - var tenantIdStr = string.IsNullOrEmpty(_tenantId) ? string.Empty : $" tenantId: {_tenantId}"; + var usernameStr = string.IsNullOrEmpty(Username) ? string.Empty : $" username: {Username}"; + var tenantIdStr = string.IsNullOrEmpty(TenantId) ? string.Empty : $" tenantId: {TenantId}"; if (filteredAccounts.Count == 0) { diff --git a/sdk/identity/Azure.Identity/src/Credentials/TokenCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/TokenCredentialOptions.cs index 00727e83fd69..272e633d22da 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/TokenCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/TokenCredentialOptions.cs @@ -2,7 +2,11 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; using Azure.Core; +using Azure.Core.Pipeline; namespace Azure.Identity { @@ -37,6 +41,11 @@ public Uri AuthorityHost /// internal bool IsLoggingPIIEnabled { get; set; } + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + internal List AdditionallyAllowedTenantsCore { get; set; } = new List(); + /// /// Gets the credential diagnostic options. /// diff --git a/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredential.cs index 703535def508..6c79ac502518 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredential.cs @@ -27,6 +27,7 @@ public class UsernamePasswordCredential : TokenCredential private readonly SecureString _password; private AuthenticationRecord _record; private readonly string _tenantId; + private readonly string[] _additionallyAllowedTenantIds; internal MsalPublicClient Client { get; } /// @@ -92,6 +93,8 @@ internal UsernamePasswordCredential( _clientId = clientId; _pipeline = pipeline ?? CredentialPipeline.GetInstance(options); Client = client ?? new MsalPublicClient(_pipeline, tenantId, clientId, null, options); + + _additionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -175,7 +178,7 @@ private async Task AuthenticateImplAsync(bool async, Token using CredentialDiagnosticScope scope = _pipeline.StartGetTokenScope($"{nameof(UsernamePasswordCredential)}.{nameof(Authenticate)}", requestContext); try { - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _additionallyAllowedTenantIds); AuthenticationResult result = await Client .AcquireTokenByUsernamePasswordAsync(requestContext.Scopes, requestContext.Claims, _username, _password, tenantId, async, cancellationToken) @@ -198,7 +201,7 @@ private async Task GetTokenImplAsync(bool async, TokenRequestContex AuthenticationResult result; if (_record != null) { - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext, _additionallyAllowedTenantIds); try { result = await Client.AcquireTokenSilentAsync(requestContext.Scopes, requestContext.Claims, _record, tenantId, async, cancellationToken) diff --git a/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredentialOptions.cs index 4479d8edbd10..3a4dd2752d31 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/UsernamePasswordCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -12,5 +14,10 @@ public class UsernamePasswordCredentialOptions : TokenCredentialOptions, ITokenC /// Specifies the to be used by the credential. If not options are specified, the token cache will not be persisted to disk. /// public TokenCachePersistenceOptions TokenCachePersistenceOptions { get; set; } + + /// + /// For multi-tenant applications, specifies additional tenants for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the application is installed. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredential.cs index 5eabd014e7a4..4cf75ebee270 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredential.cs @@ -24,7 +24,8 @@ public class VisualStudioCodeCredential : TokenCredential private readonly IVisualStudioCodeAdapter _vscAdapter; private readonly IFileSystemService _fileSystem; private readonly CredentialPipeline _pipeline; - private readonly string _tenantId; + internal string TenantId { get; } + internal string[] AdditionallyAllowedTenantIds; private const string _commonTenant = "common"; private const string Troubleshooting = "See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/vscodecredential/troubleshoot"; internal MsalPublicClient Client { get; } @@ -43,11 +44,12 @@ public VisualStudioCodeCredential(VisualStudioCodeCredentialOptions options) : t internal VisualStudioCodeCredential(VisualStudioCodeCredentialOptions options, CredentialPipeline pipeline, MsalPublicClient client, IFileSystemService fileSystem, IVisualStudioCodeAdapter vscAdapter) { - _tenantId = options?.TenantId ?? _commonTenant; + TenantId = options?.TenantId; _pipeline = pipeline ?? CredentialPipeline.GetInstance(options); - Client = client ?? new MsalPublicClient(_pipeline, options?.TenantId, ClientId, null, options); + Client = client ?? new MsalPublicClient(_pipeline, TenantId, ClientId, null, options); _fileSystem = fileSystem ?? FileSystemService.Default; _vscAdapter = vscAdapter ?? GetVscAdapter(); + AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -65,7 +67,8 @@ private async ValueTask GetTokenImplAsync(TokenRequestContext reque try { GetUserSettings(out var tenant, out var environmentName); - var tenantId = TenantIdResolver.Resolve(tenant, requestContext); + + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, AdditionallyAllowedTenantIds) ?? tenant; if (string.Equals(tenantId, Constants.AdfsTenantId, StringComparison.Ordinal)) { @@ -128,7 +131,7 @@ private static bool IsRefreshTokenString(string str) private void GetUserSettings(out string tenant, out string environmentName) { var path = _vscAdapter.GetUserSettingsPath(); - tenant = _tenantId; + tenant = TenantId ?? _commonTenant; environmentName = "AzureCloud"; try diff --git a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredentialOptions.cs index 863ac65b7757..52f7e9b3f7ac 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredentialOptions.cs @@ -15,12 +15,19 @@ public class VisualStudioCodeCredentialOptions : TokenCredentialOptions private string _tenantId; /// - /// The tenant ID the user will be authenticated to. If not specified the user will be authenticated to the tenant the user originally authenticated to via the Visual Studio Code Azure Account plugin. + /// The tenant ID the user will be authenticated to. If not specified, the user will be authenticated to any requested tenant, and by default to the tenant the user originally authenticated to via the Visual Studio Code Azure Account extension. /// public string TenantId { get { return _tenantId; } set { _tenantId = Validations.ValidateTenantId(value, allowNull: true); } } + + /// + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for , this option will have no effect, and the credential will acquire tokens for any requested tenant. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs index e9c82145fa1d..63e4ba0430ac 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredential.cs @@ -27,7 +27,8 @@ public class VisualStudioCredential : TokenCredential private const string TenantArgumentName = "--tenant"; private readonly CredentialPipeline _pipeline; - private readonly string _tenantId; + internal string TenantId { get; } + internal string[] AdditionallyAllowedTenantIds { get; } private readonly IFileSystemService _fileSystem; private readonly IProcessService _processService; private readonly bool _logPII; @@ -42,7 +43,7 @@ public VisualStudioCredential() : this(null) { } /// Creates a new instance of the with the specified options. /// /// Options for configuring the credential. - public VisualStudioCredential(VisualStudioCredentialOptions options) : this(options?.TenantId, CredentialPipeline.GetInstance(options), default, default) + public VisualStudioCredential(VisualStudioCredentialOptions options) : this(options?.TenantId, CredentialPipeline.GetInstance(options), default, default, options) { } @@ -50,10 +51,11 @@ internal VisualStudioCredential(string tenantId, CredentialPipeline pipeline, IF { _logPII = options?.IsLoggingPIIEnabled ?? false; _logAccountDetails = options?.Diagnostics?.IsAccountIdentifierLoggingEnabled ?? false; - _tenantId = tenantId; + TenantId = tenantId; _pipeline = pipeline ?? CredentialPipeline.GetInstance(null); _fileSystem = fileSystem ?? FileSystemService.Default; _processService = processService ?? ProcessService.Default; + AdditionallyAllowedTenantIds = TenantIdResolver.ResolveAddionallyAllowedTenantIds(options?.AdditionallyAllowedTenantsCore); } /// @@ -70,7 +72,7 @@ private async ValueTask GetTokenImplAsync(TokenRequestContext reque try { - if (string.Equals(_tenantId, Constants.AdfsTenantId, StringComparison.Ordinal)) + if (string.Equals(TenantId, Constants.AdfsTenantId, StringComparison.Ordinal)) { throw new CredentialUnavailableException("VisualStudioCredential authentication unavailable. ADFS tenant/authorities are not supported."); } @@ -91,7 +93,7 @@ private async ValueTask GetTokenImplAsync(TokenRequestContext reque if (_logAccountDetails) { var accountDetails = TokenHelper.ParseAccountInfoFromToken(accessToken.Token); - AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(accountDetails.ClientId, accountDetails.TenantId ?? _tenantId, accountDetails.Upn, accountDetails.ObjectId); + AzureIdentityEventSource.Singleton.AuthenticatedAccountDetails(accountDetails.ClientId, accountDetails.TenantId ?? TenantId, accountDetails.Upn, accountDetails.ObjectId); } return scope.Succeeded(accessToken); @@ -174,7 +176,7 @@ private List GetProcessStartInfos(VisualStudioTokenProvider[] arguments.Clear(); arguments.Append(ResourceArgumentName).Append(' ').Append(resource); - var tenantId = TenantIdResolver.Resolve(_tenantId, requestContext); + var tenantId = TenantIdResolver.Resolve(TenantId, requestContext, AdditionallyAllowedTenantIds); if (tenantId != default) { arguments.Append(' ').Append(TenantArgumentName).Append(' ').Append(tenantId); diff --git a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredentialOptions.cs index 1cd789bb3d33..aaf5b93b56df 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCredentialOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; + namespace Azure.Identity { /// @@ -11,12 +13,19 @@ public class VisualStudioCredentialOptions : TokenCredentialOptions private string _tenantId; /// - /// The tenant ID the user will be authenticated to. If not specified the user will be authenticated to their home tenant. + /// The tenant ID the credential will be authenticated to by default. If not specified, the credential will authenticate to any requested tenant, and will default to the tenant the user originally authenticated to via the Visual Studio Azure Service Account dialog. /// public string TenantId { get { return _tenantId; } set { _tenantId = Validations.ValidateTenantId(value, allowNull: true); } } + + /// + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. + /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. + /// If no value is specified for , this option will have no effect, and the credential will acquire tokens for any requested tenant. + /// + public IList AdditionallyAllowedTenants => AdditionallyAllowedTenantsCore; } } diff --git a/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs index 26d002a3c573..b186cba1e903 100644 --- a/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs @@ -2,74 +2,195 @@ // Licensed under the MIT License. using System; +using System.Linq; using Azure.Core; namespace Azure.Identity { internal class DefaultAzureCredentialFactory { - public DefaultAzureCredentialFactory(TokenCredentialOptions options) - : this(CredentialPipeline.GetInstance(options)) + private static readonly TokenCredential[] s_defaultCredentialChain = new DefaultAzureCredentialFactory(new DefaultAzureCredentialOptions()).CreateCredentialChain(); + private bool _useDefaultCredentialChain; + + public DefaultAzureCredentialFactory(DefaultAzureCredentialOptions options) + : this(options, CredentialPipeline.GetInstance(options)) { } - protected DefaultAzureCredentialFactory(CredentialPipeline pipeline) + protected DefaultAzureCredentialFactory(DefaultAzureCredentialOptions options, CredentialPipeline pipeline) { Pipeline = pipeline; + + _useDefaultCredentialChain = options == null; + + Options = options?.ShallowClone() ?? new DefaultAzureCredentialOptions(); + + Options.AdditionallyAllowedTenantsCore = Options.AdditionallyAllowedTenants.ToList(); } + public DefaultAzureCredentialOptions Options { get; } public CredentialPipeline Pipeline { get; } + public TokenCredential[] CreateCredentialChain() + { + if (_useDefaultCredentialChain) + { + return s_defaultCredentialChain; + } + + int i = 0; + TokenCredential[] chain = new TokenCredential[8]; + + if (!Options.ExcludeEnvironmentCredential) + { + chain[i++] = CreateEnvironmentCredential(); + } + + if (!Options.ExcludeManagedIdentityCredential) + { + chain[i++] = CreateManagedIdentityCredential(); + } + + if (!Options.ExcludeSharedTokenCacheCredential) + { + chain[i++] = CreateSharedTokenCacheCredential(); + } + + if (!Options.ExcludeVisualStudioCredential) + { + chain[i++] = CreateVisualStudioCredential(); + } + + if (!Options.ExcludeVisualStudioCodeCredential) + { + chain[i++] = CreateVisualStudioCodeCredential(); + } + + if (!Options.ExcludeAzureCliCredential) + { + chain[i++] = CreateAzureCliCredential(); + } + + if (!Options.ExcludeAzurePowerShellCredential) + { + chain[i++] = CreateAzurePowerShellCredential(); + } + + if (!Options.ExcludeInteractiveBrowserCredential) + { + chain[i++] = CreateInteractiveBrowserCredential(); + } + + if (i == 0) + { + throw new ArgumentException("At least one credential type must be included in the authentication flow.", "options"); + } + + return chain; + } + public virtual TokenCredential CreateEnvironmentCredential() { - return new EnvironmentCredential(Pipeline); + return new EnvironmentCredential(Pipeline, Options); } - public virtual TokenCredential CreateManagedIdentityCredential(DefaultAzureCredentialOptions options) + public virtual TokenCredential CreateManagedIdentityCredential() { return new ManagedIdentityCredential(new ManagedIdentityClient( new ManagedIdentityClientOptions { - ResourceIdentifier = options.ManagedIdentityResourceId, - ClientId = options.ManagedIdentityClientId, + ResourceIdentifier = Options.ManagedIdentityResourceId, + ClientId = Options.ManagedIdentityClientId, Pipeline = Pipeline, - Options = options, + Options = Options, InitialImdsConnectionTimeout = TimeSpan.FromSeconds(1) }) ); } - public virtual TokenCredential CreateSharedTokenCacheCredential(string tenantId, string username) + public virtual TokenCredential CreateSharedTokenCacheCredential() { - return new SharedTokenCacheCredential(tenantId, username, null, Pipeline); + return new SharedTokenCacheCredential(Options.SharedTokenCacheTenantId, Options.SharedTokenCacheUsername, Options, Pipeline); } - public virtual TokenCredential CreateInteractiveBrowserCredential(string tenantId, string clientId) + public virtual TokenCredential CreateInteractiveBrowserCredential() { + var options = new InteractiveBrowserCredentialOptions + { + TokenCachePersistenceOptions = new TokenCachePersistenceOptions(), + AuthorityHost = Options.AuthorityHost, + TenantId = Options.InteractiveBrowserTenantId + }; + + foreach (var addlTenant in Options.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + return new InteractiveBrowserCredential( - tenantId, - clientId ?? Constants.DeveloperSignOnClientId, - new InteractiveBrowserCredentialOptions { TokenCachePersistenceOptions = new TokenCachePersistenceOptions() }, + Options.InteractiveBrowserTenantId, + Options.InteractiveBrowserCredentialClientId ?? Constants.DeveloperSignOnClientId, + options, Pipeline); } public virtual TokenCredential CreateAzureCliCredential() { - return new AzureCliCredential(Pipeline, default); + var options = new AzureCliCredentialOptions + { + TenantId = Options.TenantId, + }; + + foreach (var addlTenant in Options.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + return new AzureCliCredential(Pipeline, default, options); } - public virtual TokenCredential CreateVisualStudioCredential(string tenantId) + public virtual TokenCredential CreateVisualStudioCredential() { - return new VisualStudioCredential(tenantId, Pipeline, default, default); + var options = new VisualStudioCredentialOptions + { + TenantId = Options.VisualStudioTenantId, + }; + + foreach (var addlTenant in Options.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + return new VisualStudioCredential(Options.VisualStudioTenantId, Pipeline, default, default, options); } - public virtual TokenCredential CreateVisualStudioCodeCredential(string tenantId) + public virtual TokenCredential CreateVisualStudioCodeCredential() { - return new VisualStudioCodeCredential(new VisualStudioCodeCredentialOptions { TenantId = tenantId }, Pipeline, default, default, default); + var options = new VisualStudioCodeCredentialOptions + { + TenantId = Options.VisualStudioCodeTenantId, + }; + + foreach (var addlTenant in Options.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + return new VisualStudioCodeCredential(options, Pipeline, default, default, default); } public virtual TokenCredential CreateAzurePowerShellCredential() { - return new AzurePowerShellCredential(new AzurePowerShellCredentialOptions(), Pipeline, default); + var options = new AzurePowerShellCredentialOptions + { + TenantId = Options.VisualStudioCodeTenantId, + }; + + foreach (var addlTenant in Options.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + return new AzurePowerShellCredential(options, Pipeline, default); } } } diff --git a/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs b/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs index 9f38354b6498..4d6900eb53ed 100644 --- a/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs +++ b/sdk/identity/Azure.Identity/src/EnvironmentVariables.cs @@ -2,6 +2,9 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; namespace Azure.Identity { @@ -10,6 +13,7 @@ internal class EnvironmentVariables public static string Username => Environment.GetEnvironmentVariable("AZURE_USERNAME"); public static string Password => Environment.GetEnvironmentVariable("AZURE_PASSWORD"); public static string TenantId => Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); + public static List AdditionallyAllowedTenants => (Environment.GetEnvironmentVariable("AZURE_ADDITIONALLY_ALLOWED_TENANTS") ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).ToList(); public static string ClientId => Environment.GetEnvironmentVariable("AZURE_CLIENT_ID"); public static string ClientSecret => Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET"); public static string ClientCertificatePath => Environment.GetEnvironmentVariable("AZURE_CLIENT_CERTIFICATE_PATH"); diff --git a/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs b/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs index f98f86fefdc6..9a1c1b382b0f 100644 --- a/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs +++ b/sdk/identity/Azure.Identity/src/ManagedIdentityClient.cs @@ -38,13 +38,16 @@ public ManagedIdentityClient(ManagedIdentityClientOptions options) } ClientId = options.ClientId; + ResourceIdentifier = options.ResourceIdentifier; Pipeline = options.Pipeline; _identitySource = new Lazy(() => SelectManagedIdentitySource(options)); } internal CredentialPipeline Pipeline { get; } - protected string ClientId { get; } + internal protected string ClientId { get; } + + internal ResourceIdentifier ResourceIdentifier { get; } public virtual async ValueTask AuthenticateAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken) diff --git a/sdk/identity/Azure.Identity/src/TenantIdResolver.cs b/sdk/identity/Azure.Identity/src/TenantIdResolver.cs index 0e8fab8de64c..b8e9263149f4 100644 --- a/sdk/identity/Azure.Identity/src/TenantIdResolver.cs +++ b/sdk/identity/Azure.Identity/src/TenantIdResolver.cs @@ -1,19 +1,25 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Linq; using Azure.Core; namespace Azure.Identity { internal static class TenantIdResolver { + public static readonly string[] AllTenants = new string[] { "*" }; + /// /// Resolves the tenantId based on the supplied configuration values. /// /// The tenantId passed to the ctor of the Credential. /// The . + /// Additional tenants the credential is configured to acquire tokens for. /// The tenantId to be used for authorization. - public static string Resolve(string explicitTenantId, TokenRequestContext context) + public static string Resolve(string explicitTenantId, TokenRequestContext context, string[] additionallyAllowedTenantIds) { bool disableMultiTenantAuth = IdentityCompatSwitches.DisableTenantDiscovery; @@ -29,12 +35,34 @@ public static string Resolve(string explicitTenantId, TokenRequestContext contex } } - return disableMultiTenantAuth switch + string resolvedTenantId = disableMultiTenantAuth switch { true => explicitTenantId, false when explicitTenantId == Constants.AdfsTenantId => explicitTenantId, _ => context.TenantId ?? explicitTenantId }; + + if (explicitTenantId != null && resolvedTenantId != explicitTenantId && additionallyAllowedTenantIds != AllTenants && Array.BinarySearch(additionallyAllowedTenantIds, resolvedTenantId, StringComparer.OrdinalIgnoreCase) < 0) + { + throw new AuthenticationFailedException($"The current credential is not configured to acquire tokens for tenant {resolvedTenantId}. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add \"*\" to AdditionallyAllowedTenants to allow acquiring tokens for any tenant. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/multitenant/troubleshoot"); + } + + return resolvedTenantId; + } + + public static string[] ResolveAddionallyAllowedTenantIds(IList additionallyAllowedTenants) + { + if (additionallyAllowedTenants == null || additionallyAllowedTenants.Count == 0) + { + return Array.Empty(); + } + + if (additionallyAllowedTenants.Contains("*")) + { + return AllTenants; + } + + return additionallyAllowedTenants.OrderBy(s => s).ToArray(); } } } diff --git a/sdk/identity/Azure.Identity/tests/AuthorizationCodeCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AuthorizationCodeCredentialTests.cs index 05d07ca17262..5a92879404c6 100644 --- a/sdk/identity/Azure.Identity/tests/AuthorizationCodeCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AuthorizationCodeCredentialTests.cs @@ -9,6 +9,8 @@ using Azure.Core; using Azure.Core.Diagnostics; using Azure.Core.TestFramework; +using Azure.Identity.Tests.Mock; +using Microsoft.Identity.Client; using NUnit.Framework; namespace Azure.Identity.Tests @@ -57,8 +59,9 @@ public async Task AuthenticateWithAuthCodeHonorsReplyUrl([Values(null, ReplyUrl) public async Task AuthenticateWithAuthCodeHonorsTenantId([Values(null, TenantIdHint)] string tenantId, [Values(true)] bool allowMultiTenantAuthentication) { var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); + var options = new AuthorizationCodeCredentialOptions { AdditionallyAllowedTenants = { TenantIdHint } }; AuthorizationCodeCredential cred = InstrumentClient( new AuthorizationCodeCredential(TenantId, ClientId, clientSecret, authCode, options, mockConfidentialMsalClient)); @@ -71,6 +74,30 @@ public async Task AuthenticateWithAuthCodeHonorsTenantId([Values(null, TenantIdH Assert.AreEqual(token2.Token, expectedToken, "Should be the expected token value"); } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + // no need to test with null TenantId since we can't construct this credential without it + if (parameters.TenantId == null) + { + Assert.Ignore("Null TenantId test does not apply to this credential"); + } + + var options = new AuthorizationCodeCredentialOptions(); + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var msalClientMock = new MockMsalConfidentialClient(AuthenticationResultFactory.Create()); + + var cred = InstrumentClient(new AuthorizationCodeCredential(parameters.TenantId, ClientId, "secret", "authcode", options, msalClientMock)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public async Task AuthenticateWithAutCodeHonorsRedirectUri([Values(null, redirectUriString)] string redirectUri) { diff --git a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs index e6850030e2cc..76e8f18fe4c7 100644 --- a/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzureCliCredentialTests.cs @@ -33,8 +33,8 @@ public async Task AuthenticateWithCliCredential( [Values(null, TenantId)] string explicitTenantId) { var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - var options = new AzureCliCredentialOptions { TenantId = explicitTenantId }; - string expectedTenantId = TenantIdResolver.Resolve(explicitTenantId, context); + var options = new AzureCliCredentialOptions { TenantId = explicitTenantId, AdditionallyAllowedTenants = { TenantIdHint } }; + string expectedTenantId = TenantIdResolver.Resolve(explicitTenantId, context, TenantIdResolver.AllTenants); var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzureCli(); var testProcess = new TestProcess { Output = processOutput }; @@ -56,6 +56,25 @@ public async Task AuthenticateWithCliCredential( } } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + var options = new AzureCliCredentialOptions { TenantId = parameters.TenantId}; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzureCli(); + var testProcess = new TestProcess { Output = processOutput }; + AzureCliCredential credential = + InstrumentClient(new AzureCliCredential(CredentialPipeline.GetInstance(null), new TestProcessService(testProcess, true), options)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, credential); + } + [Test] public async Task AuthenticateWithCliCredential_ExpiresIn() { diff --git a/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs b/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs index 1dd51b45b2c6..0f9f1416610e 100644 --- a/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs +++ b/sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs @@ -42,8 +42,8 @@ public async Task AuthenticateWithAzurePowerShellCredential( [Values(null, TenantId)] string explicitTenantId) { var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - var options = new AzurePowerShellCredentialOptions { TenantId = explicitTenantId }; - string expectedTenantId = TenantIdResolver.Resolve(explicitTenantId, context); + var options = new AzurePowerShellCredentialOptions { TenantId = explicitTenantId, AdditionallyAllowedTenants = { TenantIdHint } }; + string expectedTenantId = TenantIdResolver.Resolve(explicitTenantId, context, TenantIdResolver.AllTenants); var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzurePowerShell(TimeSpan.FromSeconds(30)); var testProcess = new TestProcess { Output = processOutput }; @@ -72,6 +72,25 @@ public async Task AuthenticateWithAzurePowerShellCredential( } } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + var options = new AzurePowerShellCredentialOptions { TenantId = parameters.TenantId }; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForAzurePowerShell(TimeSpan.FromSeconds(30)); + var testProcess = new TestProcess { Output = processOutput }; + AzurePowerShellCredential credential = InstrumentClient( + new AzurePowerShellCredential(options, CredentialPipeline.GetInstance(null), new TestProcessService(testProcess, true))); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, credential); + } + private static IEnumerable ErrorScenarios() { yield return new object[] { "Run Connect-AzAccount to login", AzurePowerShellCredential.AzurePowerShellNotLogInError }; diff --git a/sdk/identity/Azure.Identity/tests/ClientAssertionCredentialTests.cs b/sdk/identity/Azure.Identity/tests/ClientAssertionCredentialTests.cs new file mode 100644 index 000000000000..08d548f42890 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/ClientAssertionCredentialTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Identity.Tests.Mock; +using NUnit.Framework; + +namespace Azure.Identity.Tests +{ + public class ClientAssertionCredentialTests : CredentialTestBase + { + public ClientAssertionCredentialTests(bool isAsync) : base(isAsync) + { } + + public override TokenCredential GetTokenCredential(TokenCredentialOptions options) + { + var clientAssertionOptions = new ClientAssertionCredentialOptions { Diagnostics = { IsAccountIdentifierLoggingEnabled = options.Diagnostics.IsAccountIdentifierLoggingEnabled }, MsalClient = mockConfidentialMsalClient, Pipeline = CredentialPipeline.GetInstance(null) }; + + return InstrumentClient(new ClientAssertionCredential(expectedTenantId, ClientId, () => "assertion", clientAssertionOptions)); + } + + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + // no need to test with null TenantId since we can't construct this credential without it + if (parameters.TenantId == null) + { + Assert.Ignore("Null TenantId test does not apply to this credential"); + } + + var msalClientMock = new MockMsalConfidentialClient(AuthenticationResultFactory.Create()); + + var options = new ClientAssertionCredentialOptions() { MsalClient = msalClientMock, Pipeline = CredentialPipeline.GetInstance(null) }; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var cred = InstrumentClient(new ClientAssertionCredential(parameters.TenantId, ClientId, () => "assertion", options)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + } +} diff --git a/sdk/identity/Azure.Identity/tests/ClientCertificateCredentialTests.cs b/sdk/identity/Azure.Identity/tests/ClientCertificateCredentialTests.cs index 203821c10452..3dbf6fe0f36d 100644 --- a/sdk/identity/Azure.Identity/tests/ClientCertificateCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/ClientCertificateCredentialTests.cs @@ -176,7 +176,8 @@ public async Task UsesTenantIdHint( { TestSetup(); var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + var options = new ClientCertificateCredentialOptions { AdditionallyAllowedTenants = { TenantIdHint } }; + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); var certificatePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pfx"); var certificatePathPem = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pem"); var mockCert = new X509Certificate2(certificatePath); @@ -192,6 +193,31 @@ public async Task UsesTenantIdHint( Assert.AreEqual(token.Token, expectedToken, "Should be the expected token value"); } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + // no need to test with null TenantId since we can't construct this credential without it + if (parameters.TenantId == null) + { + Assert.Ignore("Null TenantId test does not apply to this credential"); + } + + var options = new ClientCertificateCredentialOptions(); + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var mockCert = new X509Certificate2(Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pfx")); + var msalClientMock = new MockMsalConfidentialClient(AuthenticationResultFactory.Create()); + + var cred = InstrumentClient(new ClientCertificateCredential(parameters.TenantId, ClientId, mockCert, options, null, msalClientMock)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public async Task SendCertificateChain([Values(true, false)] bool usePemFile, [Values(true)] bool sendCertChain) { @@ -199,7 +225,7 @@ public async Task SendCertificateChain([Values(true, false)] bool usePemFile, [V var _transport = Createx5cValidatingTransport(sendCertChain); var _pipeline = new HttpPipeline(_transport, new[] {new BearerTokenAuthenticationPolicy(new MockCredential(), "scope")}); var context = new TokenRequestContext(new[] { Scope }, tenantId: TenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); var certificatePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pfx"); var certificatePathPem = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pem"); var mockCert = new X509Certificate2(certificatePath); diff --git a/sdk/identity/Azure.Identity/tests/ClientSecretCredentialTests.cs b/sdk/identity/Azure.Identity/tests/ClientSecretCredentialTests.cs index 15b25859d2bd..378e28e95bf6 100644 --- a/sdk/identity/Azure.Identity/tests/ClientSecretCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/ClientSecretCredentialTests.cs @@ -19,6 +19,30 @@ public ClientSecretCredentialTests(bool isAsync) : base(isAsync) public override TokenCredential GetTokenCredential(TokenCredentialOptions options) => InstrumentClient( new ClientSecretCredential(expectedTenantId, ClientId, "secret", options, null, mockConfidentialMsalClient)); + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + // no need to test with null TenantId since we can't construct this credential without it + if (parameters.TenantId == null) + { + Assert.Ignore("Null TenantId test does not apply to this credential"); + } + + var options = new ClientSecretCredentialOptions(); + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var msalClientMock = new MockMsalConfidentialClient(AuthenticationResultFactory.Create()); + + var cred = InstrumentClient(new ClientSecretCredential(parameters.TenantId, ClientId, "secret", options, null, msalClientMock)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public void VerifyCtorParametersValidation() { @@ -38,7 +62,8 @@ public async Task UsesTenantIdHint( { TestSetup(); var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); + var options = new ClientSecretCredentialOptions { AdditionallyAllowedTenants = { TenantIdHint } }; ClientSecretCredential client = InstrumentClient(new ClientSecretCredential(expectedTenantId, ClientId, "secret", options, null, mockConfidentialMsalClient)); diff --git a/sdk/identity/Azure.Identity/tests/CredentialTestBase.cs b/sdk/identity/Azure.Identity/tests/CredentialTestBase.cs index b8850b6ebfb9..2312dca2c269 100644 --- a/sdk/identity/Azure.Identity/tests/CredentialTestBase.cs +++ b/sdk/identity/Azure.Identity/tests/CredentialTestBase.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Globalization; using System.IO; @@ -71,6 +73,79 @@ public async Task IsAccountIdentifierLoggingEnabled([Values(true, false)] bool i } } + public class AllowedTenantsTestParameters + { + public string TenantId { get; set; } + public List AdditionallyAllowedTenants { get; set; } + public TokenRequestContext TokenRequestContext { get; set; } + public string ToDebugString() + { + return $"TenantId:{TenantId??"null"}, AddlTenants:[{string.Join(",", AdditionallyAllowedTenants)}], RequestedTenantId:{TokenRequestContext.TenantId??"null"}"; + } + } + + public static IEnumerable GetAllowedTenantsTestCases() + { + string tenant = Guid.NewGuid().ToString(); + string addlTenantA = Guid.NewGuid().ToString(); + string addlTenantB = Guid.NewGuid().ToString(); + + List tenantValues = new List() { tenant, null }; + + List> additionalAllowedTenantsValues = new List>() + { + new List(), + new List { addlTenantA, addlTenantB }, + new List { "*" }, + new List { addlTenantA, "*", addlTenantB } + }; + + List tokenRequestContextValues = new List() + { + new TokenRequestContext(MockScopes.Default), + new TokenRequestContext(MockScopes.Default, tenantId: tenant), + new TokenRequestContext(MockScopes.Default, tenantId: addlTenantA), + new TokenRequestContext(MockScopes.Default, tenantId: addlTenantB), + new TokenRequestContext(MockScopes.Default, tenantId: Guid.NewGuid().ToString()), + }; + + foreach (var mainTenant in tenantValues) + { + foreach (var additoinallyAllowedTenants in additionalAllowedTenantsValues) + { + foreach (var tokenRequestContext in tokenRequestContextValues) + { + yield return new AllowedTenantsTestParameters { TenantId = mainTenant, AdditionallyAllowedTenants = additoinallyAllowedTenants, TokenRequestContext = tokenRequestContext }; + } + } + } + } + + [TestCaseSource(nameof(GetAllowedTenantsTestCases))] + public abstract Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters); + + public static async Task AssertAllowedTenantIdsEnforcedAsync(AllowedTenantsTestParameters parameters, TokenCredential credential) + { + bool expAllowed = parameters.TenantId == null + || parameters.TokenRequestContext.TenantId == null + || parameters.TenantId == parameters.TokenRequestContext.TenantId + || parameters.AdditionallyAllowedTenants.Contains(parameters.TokenRequestContext.TenantId) + || parameters.AdditionallyAllowedTenants.Contains("*"); + + if (expAllowed) + { + var accessToken = await credential.GetTokenAsync(parameters.TokenRequestContext, default); + + Assert.IsNotNull(accessToken.Token); + } + else + { + var ex = Assert.ThrowsAsync(async () => { await credential.GetTokenAsync(parameters.TokenRequestContext, default); }); + + StringAssert.Contains($"The current credential is not configured to acquire tokens for tenant {parameters.TokenRequestContext.TenantId}", ex.Message); + } + } + public void TestSetup(TokenCredentialOptions options = null) { expectedTenantId = null; diff --git a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs new file mode 100644 index 000000000000..ae047b9cc526 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialFactoryTests.cs @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using Azure.Core; +using Azure.Core.TestFramework; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Options; +using NUnit.Framework; + +namespace Azure.Identity.Tests +{ + internal class DefaultAzureCredentialFactoryTests + { + [Test] + public void ValidateManagedIdentityCtorOptionsHonored([Values] bool setClientId, [Values] bool setResourceId) + { + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null } + })) + { + ResourceIdentifier expResourceId = setResourceId ? new ResourceIdentifier($"/subscriptions/{Guid.NewGuid().ToString()}/locations/MyLocation") : null; + string expClientId = setClientId ? Guid.NewGuid().ToString() : null; + + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions() + { + ManagedIdentityClientId = expClientId, + ManagedIdentityResourceId = expResourceId + }; + + var factory = new DefaultAzureCredentialFactory(options); + + if (setClientId && setResourceId) + { + Assert.Throws(() => factory.CreateManagedIdentityCredential()); + } + else + { + ManagedIdentityCredential cred = (ManagedIdentityCredential)factory.CreateManagedIdentityCredential(); + + Assert.AreEqual(expResourceId?.ToString(), cred.Client.ResourceIdentifier?.ToString()); + Assert.AreEqual(expClientId, cred.Client.ClientId); + } + } + } + + [Test] + public void ValidateSharedTokenCacheOptionsHonored([Values] bool setTenantId, [Values] bool setSharedTokenCacheTenantId, [Values] bool setSharedTokenCacheUsername) + { + // ignore when both setTenantId and setSharedTokenCacheTenantId are true since we cannot set both + if (setTenantId && setSharedTokenCacheTenantId) + { + Assert.Ignore("Test variation ignored since TenantId and SharedTokenCacheTenantId cannot both be set"); + } + + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null } + })) + { + string expTenantId = setTenantId ? Guid.NewGuid().ToString() : null; + string expSharedTokenCacheTenantId = setSharedTokenCacheTenantId ? Guid.NewGuid().ToString() : null; + string expSharedTokenCacheUsername = setSharedTokenCacheUsername ? Guid.NewGuid().ToString() : null; + + var options = new DefaultAzureCredentialOptions() + { + SharedTokenCacheUsername = expSharedTokenCacheUsername + }; + + if (setTenantId) + { + options.TenantId = expTenantId; + } + + if (setSharedTokenCacheTenantId) + { + options.SharedTokenCacheTenantId = expSharedTokenCacheTenantId; + } + + var factory = new DefaultAzureCredentialFactory(options); + + SharedTokenCacheCredential cred = (SharedTokenCacheCredential)factory.CreateSharedTokenCacheCredential(); + + Assert.AreEqual(expSharedTokenCacheTenantId ?? expTenantId, cred.TenantId); + Assert.AreEqual(expSharedTokenCacheUsername, cred.Username); + } + } + + [Test] + public void ValidateVisualStudioOptionsHonored([Values] bool setTenantId, [Values] bool setVisualStudioTenantId, [Values] bool setAdditionallyAllowedTenants) + { + // ignore when both setTenantId and setVisualStudioTenantId are true since we cannot set both + if (setTenantId && setVisualStudioTenantId) + { + Assert.Ignore("Test variation ignored since TenantId and VisualStudioTenantId cannot both be set"); + } + + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null }, + { "AZURE_ADDITIONALLY_ALLOWED_TENANTS", null } + })) + { + string expTenantId = setTenantId ? Guid.NewGuid().ToString() : null; + string expVisualStudioTenantId = setVisualStudioTenantId ? Guid.NewGuid().ToString() : null; + string[] expAdditionallyAllowedTenants = setAdditionallyAllowedTenants ? new string[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } : Array.Empty(); + + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions(); + + if (setTenantId) + { + options.TenantId = expTenantId; + } + + if (setVisualStudioTenantId) + { + options.VisualStudioTenantId = expVisualStudioTenantId; + } + + foreach (var tenantId in expAdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(tenantId); + } + + var factory = new DefaultAzureCredentialFactory(options); + + VisualStudioCredential cred = (VisualStudioCredential)factory.CreateVisualStudioCredential(); + + Assert.AreEqual(expVisualStudioTenantId ?? expTenantId, cred.TenantId); + CollectionAssert.AreEquivalent(expAdditionallyAllowedTenants, cred.AdditionallyAllowedTenantIds); + } + } + + [Test] + public void ValidateVisualStudioCodeOptionsHonored([Values] bool setTenantId, [Values] bool setVisualStudioCodeTenantId, [Values] bool setAdditionallyAllowedTenants) + { + // ignore when both setTenantId and setVisualStudioCodeTenantId are true since we cannot set both + if (setTenantId && setVisualStudioCodeTenantId) + { + Assert.Ignore("Test variation ignored since TenantId and VisualStudioCodeTenantId cannot both be set"); + } + + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null }, + { "AZURE_ADDITIONALLY_ALLOWED_TENANTS", null } + })) + { + string expTenantId = setTenantId ? Guid.NewGuid().ToString() : null; + string expVisualStudioCodeTenantId = setVisualStudioCodeTenantId ? Guid.NewGuid().ToString() : null; + string[] expAdditionallyAllowedTenants = setAdditionallyAllowedTenants ? new string[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } : Array.Empty(); + + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions(); + + if (setTenantId) + { + options.TenantId = expTenantId; + } + + if (setVisualStudioCodeTenantId) + { + options.VisualStudioCodeTenantId = expVisualStudioCodeTenantId; + } + + foreach (var tenantId in expAdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(tenantId); + } + + var factory = new DefaultAzureCredentialFactory(options); + + VisualStudioCodeCredential cred = (VisualStudioCodeCredential)factory.CreateVisualStudioCodeCredential(); + + Assert.AreEqual(expVisualStudioCodeTenantId ?? expTenantId, cred.TenantId); + CollectionAssert.AreEquivalent(expAdditionallyAllowedTenants, cred.AdditionallyAllowedTenantIds); + } + } + + [Test] + public void ValidateCliOptionsHonored([Values] bool setTenantId, [Values] bool setAdditionallyAllowedTenants) + { + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null }, + { "AZURE_ADDITIONALLY_ALLOWED_TENANTS", null } + })) + { + string expTenantId = setTenantId ? Guid.NewGuid().ToString() : null; + string[] expAdditionallyAllowedTenants = setAdditionallyAllowedTenants ? new string[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } : Array.Empty(); + + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions() + { + TenantId = expTenantId, + }; + + foreach (var tenantId in expAdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(tenantId); + } + + var factory = new DefaultAzureCredentialFactory(options); + + AzureCliCredential cred = (AzureCliCredential)factory.CreateAzureCliCredential(); + + Assert.AreEqual(expTenantId, cred.TenantId); + CollectionAssert.AreEquivalent(expAdditionallyAllowedTenants, cred.AdditionallyAllowedTenantIds); + } + } + + [Test] + public void ValidatePowerShellOptionsHonored([Values] bool setTenantId, [Values] bool setAdditionallyAllowedTenants) + { + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null }, + { "AZURE_ADDITIONALLY_ALLOWED_TENANTS", null } + })) + { + string expTenantId = setTenantId ? Guid.NewGuid().ToString() : null; + string[] expAdditionallyAllowedTenants = setAdditionallyAllowedTenants ? new string[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } : Array.Empty(); + + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions() + { + TenantId = expTenantId, + }; + + foreach (var tenantId in expAdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(tenantId); + } + + var factory = new DefaultAzureCredentialFactory(options); + + AzurePowerShellCredential cred = (AzurePowerShellCredential)factory.CreateAzurePowerShellCredential(); + + Assert.AreEqual(expTenantId, cred.TenantId); + CollectionAssert.AreEquivalent(expAdditionallyAllowedTenants, cred.AdditionallyAllowedTenantIds); + } + } + + [Test] + public void ValidateInteractiveBrowserOptionsHonored([Values] bool setTenantId, [Values] bool setClientId, [Values] bool setInteractiveBrowserTenantId, [Values] bool setAdditionallyAllowedTenants) + { + // ignore when both setTenantId and setInteractiveBrowserTenantId are true since we cannot set both + if (setTenantId && setInteractiveBrowserTenantId) + { + Assert.Ignore("Test variation ignored since TenantId and InteractiveBrowserTenantId cannot both be set"); + } + + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null }, + { "AZURE_ADDITIONALLY_ALLOWED_TENANTS", null } + })) + { + string expClientId = setClientId ? Guid.NewGuid().ToString() : Constants.DeveloperSignOnClientId; + string expTenantId = setTenantId ? Guid.NewGuid().ToString() : null; + string expInteractiveBrowserTenantId = setInteractiveBrowserTenantId ? Guid.NewGuid().ToString() : null; + string[] expAdditionallyAllowedTenants = setAdditionallyAllowedTenants ? new string[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() } : Array.Empty(); + + DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions(); + + if (setTenantId) + { + options.TenantId = expTenantId; + } + + if (setClientId) + { + options.InteractiveBrowserCredentialClientId = expClientId; + } + + if (setInteractiveBrowserTenantId) + { + options.InteractiveBrowserTenantId = expInteractiveBrowserTenantId; + } + + foreach (var tenantId in expAdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(tenantId); + } + + var factory = new DefaultAzureCredentialFactory(options); + + InteractiveBrowserCredential cred = (InteractiveBrowserCredential)factory.CreateInteractiveBrowserCredential(); + + Assert.AreEqual(expInteractiveBrowserTenantId ?? expTenantId, cred.TenantId); + Assert.AreEqual(expClientId, cred.ClientId); + CollectionAssert.AreEquivalent(expAdditionallyAllowedTenants, cred.AdditionallyAllowedTenantIds); + } + } + + [Test] + public void ValidateExcludeOptionsHonored([Values(true, false)] bool excludeEnvironmentCredential, + [Values(true, false)] bool excludeManagedIdentityCredential, + [Values(true, false)] bool excludeSharedTokenCacheCredential, + [Values(true, false)] bool excludeVisualStudioCredential, + [Values(true, false)] bool excludeVisualStudioCodeCredential, + [Values(true, false)] bool excludeCliCredential, + [Values(true, false)] bool excludeAzurePowerShellCredential, + [Values(true, false)] bool excludeInteractiveBrowserCredential) + { + using (new TestEnvVar(new Dictionary + { + { "AZURE_CLIENT_ID", null }, + { "AZURE_USERNAME", null }, + { "AZURE_TENANT_ID", null }, + { "AZURE_ADDITIONALLY_ALLOWED_TENANTS", null } + })) + { + var expCredentialTypes = new List(); + expCredentialTypes.ConditionalAdd(!excludeEnvironmentCredential, typeof(EnvironmentCredential)); + expCredentialTypes.ConditionalAdd(!excludeManagedIdentityCredential, typeof(ManagedIdentityCredential)); + expCredentialTypes.ConditionalAdd(!excludeSharedTokenCacheCredential, typeof(SharedTokenCacheCredential)); + expCredentialTypes.ConditionalAdd(!excludeVisualStudioCredential, typeof(VisualStudioCredential)); + expCredentialTypes.ConditionalAdd(!excludeVisualStudioCodeCredential, typeof(VisualStudioCodeCredential)); + expCredentialTypes.ConditionalAdd(!excludeCliCredential, typeof(AzureCliCredential)); + expCredentialTypes.ConditionalAdd(!excludeAzurePowerShellCredential, typeof(AzurePowerShellCredential)); + expCredentialTypes.ConditionalAdd(!excludeInteractiveBrowserCredential, typeof(InteractiveBrowserCredential)); + + var options = new DefaultAzureCredentialOptions + { + ExcludeEnvironmentCredential = excludeEnvironmentCredential, + ExcludeManagedIdentityCredential = excludeManagedIdentityCredential, + ExcludeSharedTokenCacheCredential = excludeSharedTokenCacheCredential, + ExcludeAzureCliCredential = excludeCliCredential, + ExcludeInteractiveBrowserCredential = excludeInteractiveBrowserCredential, + ExcludeVisualStudioCredential = excludeVisualStudioCredential, + ExcludeVisualStudioCodeCredential = excludeVisualStudioCodeCredential, + ExcludeAzurePowerShellCredential = excludeAzurePowerShellCredential + }; + + var factory = new DefaultAzureCredentialFactory(options); + + if (expCredentialTypes.Count == 0) + { + Assert.Throws(() => factory.CreateCredentialChain()); + + Assert.Pass(); + } + + TokenCredential[] chain = factory.CreateCredentialChain(); + + for (int i = 0; i < expCredentialTypes.Count; i++) + { + Assert.IsInstanceOf(expCredentialTypes[i], chain[i]); + } + + for (int i = expCredentialTypes.Count; i < chain.Length; i++) + { + Assert.IsNull(chain[i]); + } + } + } + } +} diff --git a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs index a25c3082b19b..36f0be49f9ab 100644 --- a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialLiveTests.cs @@ -40,7 +40,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCredential() var testProcess = new TestProcess { Output = processOutput }; var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(testProcess), default); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); AccessToken token; List scopes; @@ -79,7 +79,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCodeCredential() var process = new TestProcess { Error = "Error" }; var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(process), default); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); AccessToken token; List scopes; @@ -117,7 +117,7 @@ public async Task DefaultAzureCredential_UseVisualStudioCodeCredential_ParallelC var processService = new TestProcessService { CreateHandler = psi => new TestProcess { Error = "Error" }}; var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, processService, default); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); var tasks = new List>(); using (await CredentialTestHelpers.CreateRefreshTokenFixtureAsync(TestEnvironment, Mode, ExpectedServiceName, cloudName)) @@ -154,7 +154,7 @@ public async Task DefaultAzureCredential_UseAzureCliCredential() var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment); var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, new TestProcessService(testProcess), vscAdapter); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); AccessToken token; List scopes; @@ -192,7 +192,7 @@ public async Task DefaultAzureCredential_UseAzureCliCredential_ParallelCalls() var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment); var factory = new TestDefaultAzureCredentialFactory(options, fileSystem, processService, vscAdapter); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); var tasks = new List>(); for (int i = 0; i < 10; i++) @@ -222,7 +222,7 @@ public void DefaultAzureCredential_AllCredentialsHaveFailed_CredentialUnavailabl var vscAdapter = new TestVscAdapter(ExpectedServiceName, "AzureCloud", "{}"); var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "'az' is not recognized" }, new TestProcess{Error = "'PowerShell' is not recognized"}, new TestProcess{Error = "'PowerShell' is not recognized"}), vscAdapter); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); List scopes; @@ -253,7 +253,7 @@ public void DefaultAzureCredential_AllCredentialsHaveFailed_FirstAuthenticationF var vscAdapter = new TestVscAdapter(ExpectedServiceName, "AzureCloud", null); var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "Error" }), vscAdapter); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); List scopes; @@ -283,7 +283,7 @@ public void DefaultAzureCredential_AllCredentialsHaveFailed_LastAuthenticationFa var vscAdapter = new TestVscAdapter(ExpectedServiceName, "AzureCloud", null); var factory = new TestDefaultAzureCredentialFactory(options, new TestFileSystemService(), new TestProcessService(new TestProcess { Error = "Error" }), vscAdapter); - var credential = InstrumentClient(new DefaultAzureCredential(factory, options)); + var credential = InstrumentClient(new DefaultAzureCredential(factory)); List scopes; diff --git a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialOptionsTests.cs b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialOptionsTests.cs new file mode 100644 index 000000000000..003c2afb2c19 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialOptionsTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Azure.Core; +using Azure.Core.TestFramework; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Framework; + +namespace Azure.Identity.Tests +{ + public class DefaultAzureCredentialOptionsTests + { + public static IEnumerable IdTestValues() + { + yield return Guid.NewGuid().ToString(); + yield return String.Empty; + yield return null; + } + + [Test] + [TestCaseSource(nameof(IdTestValues))] + public void ValidateAzureTenantIdEnvVarDefaultHonored(string envVarValue) + { + var expValue = string.IsNullOrEmpty(envVarValue) ? null : envVarValue; + + using (new TestEnvVar("AZURE_TENANT_ID", envVarValue)) + { + var options = new DefaultAzureCredentialOptions(); + + Assert.AreEqual(options.TenantId, expValue); + Assert.AreEqual(options.InteractiveBrowserTenantId, expValue); + Assert.AreEqual(options.SharedTokenCacheTenantId, expValue); + Assert.AreEqual(options.VisualStudioTenantId, expValue); + Assert.AreEqual(options.VisualStudioCodeTenantId, expValue); + } + } + + [Test] + [TestCaseSource(nameof(IdTestValues))] + public void ValidateAzureUsernameEnvVarDefaultHonored(string envVarValue) + { + var expValue = string.IsNullOrEmpty(envVarValue) ? null : envVarValue; + + using (new TestEnvVar("AZURE_USERNAME", envVarValue)) + { + var options = new DefaultAzureCredentialOptions(); + + Assert.AreEqual(options.SharedTokenCacheUsername, expValue); + } + } + + [Test] + [TestCaseSource(nameof(IdTestValues))] + public void ValidateAzureClientIdEnvVarDefaultHonored(string envVarValue) + { + var expValue = string.IsNullOrEmpty(envVarValue) ? null : envVarValue; + + using (new TestEnvVar("AZURE_CLIENT_ID", envVarValue)) + { + var options = new DefaultAzureCredentialOptions(); + + Assert.AreEqual(options.ManagedIdentityClientId, expValue); + } + } + + public static IEnumerable IdListTestValues() + { + yield return "46A32CE9-7466-43E6-8704-B98F83725592"; + yield return "*"; + yield return "C9280049-5395-42BA-BBCB-7F06676595A5;4BB289FC-2FE5-46BD-ADD7-EF71120A3BDC;7256A98E-BA8C-4E31-980E-AE2E7566FE9E"; + yield return String.Empty; + yield return null; + } + [Test] + [TestCaseSource(nameof(IdListTestValues))] + public void ValidateAzureAdditionallyAllowedTenantsEnvVarDefaultHonored(string envVarValue) + { + var expValue = string.IsNullOrEmpty(envVarValue) ? Array.Empty() : envVarValue.Split(';'); + + using (new TestEnvVar("AZURE_ADDITIONALLY_ALLOWED_TENANTS", envVarValue)) + { + var options = new DefaultAzureCredentialOptions(); + + CollectionAssert.AreEqual(expValue, options.AdditionallyAllowedTenants); + } + } + + [Test] + public void ValidateShallowCloneCopiesAllProperties([Values]bool useTenantId) + { + Random rand = new Random(); + + var orig = new DefaultAzureCredentialOptions(); + + foreach (var propInfo in EnumerateDefaultAzureCredentialOptionsProperties(useTenantId, !useTenantId)) + { + if (propInfo.PropertyType == typeof(string)) + { + propInfo.SetValue(orig, Guid.NewGuid().ToString()); + } + else if (propInfo.PropertyType == typeof(bool)) + { + propInfo.SetValue(orig, rand.NextDouble() > .5); + } + else if (propInfo.PropertyType == typeof(Uri)) + { + propInfo.SetValue(orig, AzureAuthorityHosts.AzureChina); + } + else if (propInfo.PropertyType == typeof(ResourceIdentifier)) + { + propInfo.SetValue(orig, new ResourceIdentifier($"{Guid.NewGuid()}/{Guid.NewGuid()}/{Guid.NewGuid()}")); + } + else if (propInfo.PropertyType == typeof(IList)) + { + IList list = propInfo.GetValue(orig) as IList; + list.Add(Guid.NewGuid().ToString()); + list.Add(Guid.NewGuid().ToString()); + } + else + { + Assert.Fail($"test doesn't support property type {propInfo.PropertyType} for property {propInfo.Name}"); + } + } + + var clone = orig.ShallowClone(); + + foreach (var propInfo in EnumerateDefaultAzureCredentialOptionsProperties(useTenantId, !useTenantId)) + { + if (propInfo.PropertyType == typeof(IList)) + { + CollectionAssert.AreEqual((IList)propInfo.GetValue(orig), (IList)propInfo.GetValue(clone), $"Cloned {propInfo.Name} does not match original"); + } + else + { + Assert.AreEqual(propInfo.GetValue(orig), propInfo.GetValue(clone), $"Cloned {propInfo.Name} does not match original"); + } + } + } + + private IEnumerable EnumerateDefaultAzureCredentialOptionsProperties(bool includeTenantId, bool includeAltTenantIds) + { + foreach (var propInfo in typeof(DefaultAzureCredentialOptions).GetProperties()) + { + // shallow clone only clones properties from Azure.Identity so we exclude base properties + if (propInfo.DeclaringType == typeof(DefaultAzureCredentialOptions) || propInfo.DeclaringType == typeof(TokenCredentialOptions)) + { + switch (propInfo.Name) + { + // diagnostics is also ignored by shallow clone + case "Diagnostics": + break; + case "TenantId": + if (includeTenantId) + { + yield return propInfo; + } + break; + case "InteractiveBrowserTenantId": + case "SharedTokenCacheTenantId": + case "VisualStudioTenantId": + case "VisualStudioCodeTenantId": + if (includeAltTenantIds) + { + yield return propInfo; + } + break; + default: + yield return propInfo; + break; + } + } + } + } + } +} diff --git a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialTests.cs b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialTests.cs index 234171312585..4fd429e28950 100644 --- a/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/DefaultAzureCredentialTests.cs @@ -66,303 +66,6 @@ public void ValidateCtorIncludedInteractiveParam([Values(true, false)] bool incl } } - public enum ManagedIdentityIdType - { - None, - ClientId, - ResourceId - } - - [Test] - public void ValidateCtorOptionsPassedToCredentials([Values(ManagedIdentityIdType.None, ManagedIdentityIdType.ClientId, ManagedIdentityIdType.ResourceId)] ManagedIdentityIdType managedIdentityIdType) - { - string expClientId = Guid.NewGuid().ToString(); - string expUsername = Guid.NewGuid().ToString(); - string expCacheTenantId = Guid.NewGuid().ToString(); - string expBrowserTenantId = Guid.NewGuid().ToString(); - string expVsTenantId = Guid.NewGuid().ToString(); - string expCodeTenantId = Guid.NewGuid().ToString(); - string expResourceId = $"/subscriptions/{Guid.NewGuid().ToString()}/locations/MyLocation"; - string actClientId_ManagedIdentity = null; - string actResiurceId_ManagedIdentity = null; - string actClientId_InteractiveBrowser = null; - string actUsername = null; - string actCacheTenantId = null; - string actBrowserTenantId = null; - string actVsTenantId = null; - string actCodeTenantId = null; - - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); - - credFactory.OnCreateManagedIdentityCredential = (options, _) => - { - actClientId_ManagedIdentity = options.ManagedIdentityClientId; - actResiurceId_ManagedIdentity = options.ManagedIdentityResourceId?.ToString(); - }; - credFactory.OnCreateSharedTokenCacheCredential = (tenantId, username, _) => { actCacheTenantId = tenantId; actUsername = username; }; - credFactory.OnCreateInteractiveBrowserCredential = (tenantId, clientId, _) => { actBrowserTenantId = tenantId; actClientId_InteractiveBrowser = clientId; }; - credFactory.OnCreateVisualStudioCredential = (tenantId, _) => { actVsTenantId = tenantId; }; - credFactory.OnCreateVisualStudioCodeCredential = (tenantId, _) => { actCodeTenantId = tenantId; }; - credFactory.OnCreateAzurePowerShellCredential = _ => {}; - - var options = new DefaultAzureCredentialOptions - { - InteractiveBrowserCredentialClientId = expClientId, - SharedTokenCacheUsername = expUsername, - ExcludeSharedTokenCacheCredential = false, - SharedTokenCacheTenantId = expCacheTenantId, - VisualStudioTenantId = expVsTenantId, - VisualStudioCodeTenantId = expCodeTenantId, - InteractiveBrowserTenantId = expBrowserTenantId, - ExcludeInteractiveBrowserCredential = false, - }; - - switch (managedIdentityIdType) - { - case ManagedIdentityIdType.ClientId: - options.ManagedIdentityClientId = expClientId; - break; - case ManagedIdentityIdType.ResourceId: - options.ManagedIdentityResourceId = new ResourceIdentifier(expResourceId); - break; - } - - new DefaultAzureCredential(credFactory, options); - - Assert.AreEqual(expClientId, actClientId_InteractiveBrowser); - Assert.AreEqual(expUsername, actUsername); - Assert.AreEqual(expCacheTenantId, actCacheTenantId); - Assert.AreEqual(expBrowserTenantId, actBrowserTenantId); - Assert.AreEqual(expVsTenantId, actVsTenantId); - Assert.AreEqual(expCodeTenantId, actCodeTenantId); - switch (managedIdentityIdType) - { - case ManagedIdentityIdType.ClientId: - Assert.AreEqual(expClientId, actClientId_ManagedIdentity); - break; - case ManagedIdentityIdType.ResourceId: - Assert.AreEqual(expResourceId, actResiurceId_ManagedIdentity); - break; - case ManagedIdentityIdType.None: - Assert.IsNull(actClientId_ManagedIdentity); - Assert.IsNull(actResiurceId_ManagedIdentity); - break; - } - } - - [Test] - [NonParallelizable] - public void ValidateEnvironmentBasedOptionsPassedToCredentials([Values] bool clientIdSpecified, [Values] bool usernameSpecified, [Values] bool tenantIdSpecified) - { - var expClientId = clientIdSpecified ? Guid.NewGuid().ToString() : null; - var expUsername = usernameSpecified ? Guid.NewGuid().ToString() : null; - var expTenantId = tenantIdSpecified ? Guid.NewGuid().ToString() : null; - bool onCreateSharedCalled = false; - bool onCreatedManagedCalled = false; - bool onCreateInteractiveCalled = false; - bool onCreateVsCalled = false; - bool onCreateVsCodeCalled = false; - - using (new TestEnvVar(new Dictionary - { - { "AZURE_CLIENT_ID", expClientId }, - { "AZURE_USERNAME", expUsername }, - { "AZURE_TENANT_ID", expTenantId } - })) - { - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); - - credFactory.OnCreateManagedIdentityCredential = (options, _) => - { - onCreatedManagedCalled = true; - Assert.AreEqual(expClientId, options.ManagedIdentityClientId); - }; - - credFactory.OnCreateSharedTokenCacheCredential = (tenantId, username, _) => - { - onCreateSharedCalled = true; - Assert.AreEqual(expTenantId, tenantId); - Assert.AreEqual(expUsername, username); - }; - - credFactory.OnCreateInteractiveBrowserCredential = (tenantId, clientId, _) => - { - onCreateInteractiveCalled = true; - Assert.AreEqual(expTenantId, tenantId); - }; - - credFactory.OnCreateVisualStudioCredential = (tenantId, _) => - { - onCreateVsCalled = true; - Assert.AreEqual(expTenantId, tenantId); - }; - - credFactory.OnCreateVisualStudioCodeCredential = (tenantId, _) => - { - onCreateVsCodeCalled = true; - Assert.AreEqual(expTenantId, tenantId); - }; - var options = new DefaultAzureCredentialOptions - { - ExcludeEnvironmentCredential = true, - ExcludeManagedIdentityCredential = false, - ExcludeSharedTokenCacheCredential = false, - ExcludeVisualStudioCredential = false, - ExcludeVisualStudioCodeCredential = false, - ExcludeAzureCliCredential = true, - ExcludeInteractiveBrowserCredential = false - }; - - new DefaultAzureCredential(credFactory, options); - - Assert.IsTrue(onCreateSharedCalled); - Assert.IsTrue(onCreatedManagedCalled); - Assert.IsTrue(onCreateInteractiveCalled); - Assert.IsTrue(onCreateVsCalled); - Assert.IsTrue(onCreateVsCodeCalled); - } - } - - [Test] - [NonParallelizable] - public void ValidateEmptyEnvironmentBasedOptionsNotPassedToCredentials([Values] bool clientIdSpecified, [Values] bool usernameSpecified, [Values] bool tenantIdSpecified) - { - var expClientId = clientIdSpecified ? string.Empty : null; - var expUsername = usernameSpecified ? string.Empty : null; - var expTenantId = tenantIdSpecified ? string.Empty : null; - bool onCreateSharedCalled = false; - bool onCreatedManagedCalled = false; - bool onCreateInteractiveCalled = false; - bool onCreateVsCalled = false; - bool onCreateVsCodeCalled = false; - - using (new TestEnvVar(new Dictionary - { - { "AZURE_CLIENT_ID", expClientId }, - { "AZURE_USERNAME", expUsername }, - { "AZURE_TENANT_ID", expTenantId } - })) - { - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); - - credFactory.OnCreateManagedIdentityCredential = (options, _) => - { - onCreatedManagedCalled = true; - Assert.IsNull(options.ManagedIdentityClientId); - }; - - credFactory.OnCreateSharedTokenCacheCredential = (tenantId, username, _) => - { - onCreateSharedCalled = true; - Assert.IsNull(tenantId); - Assert.IsNull(username); - }; - - credFactory.OnCreateInteractiveBrowserCredential = (tenantId, _, _) => - { - onCreateInteractiveCalled = true; - Assert.IsNull(tenantId); - }; - - credFactory.OnCreateVisualStudioCredential = (tenantId, _) => - { - onCreateVsCalled = true; - Assert.IsNull(tenantId); - }; - - credFactory.OnCreateVisualStudioCodeCredential = (tenantId, _) => - { - onCreateVsCodeCalled = true; - Assert.IsNull(tenantId); - }; - var options = new DefaultAzureCredentialOptions - { - ExcludeEnvironmentCredential = true, - ExcludeManagedIdentityCredential = false, - ExcludeSharedTokenCacheCredential = false, - ExcludeVisualStudioCredential = false, - ExcludeVisualStudioCodeCredential = false, - ExcludeAzureCliCredential = true, - ExcludeInteractiveBrowserCredential = false - }; - - new DefaultAzureCredential(credFactory, options); - - Assert.IsTrue(onCreateSharedCalled); - Assert.IsTrue(onCreatedManagedCalled); - Assert.IsTrue(onCreateInteractiveCalled); - Assert.IsTrue(onCreateVsCalled); - Assert.IsTrue(onCreateVsCodeCalled); - } - } - - [Test] - public void ValidateCtorWithExcludeOptions([Values(true, false)]bool excludeEnvironmentCredential, - [Values(true, false)]bool excludeManagedIdentityCredential, - [Values(true, false)]bool excludeSharedTokenCacheCredential, - [Values(true, false)]bool excludeVisualStudioCredential, - [Values(true, false)]bool excludeVisualStudioCodeCredential, - [Values(true, false)]bool excludeCliCredential, - [Values(true, false)]bool excludeAzurePowerShellCredential, - [Values(true, false)]bool excludeInteractiveBrowserCredential) - { - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); - - bool environmentCredentialIncluded = false; - bool managedIdentityCredentialIncluded = false; - bool sharedTokenCacheCredentialIncluded = false; - bool cliCredentialIncluded = false; - bool interactiveBrowserCredentialIncluded = false; - bool visualStudioCredentialIncluded = false; - bool visualStudioCodeCredentialIncluded = false; - bool powerShellCredentialsIncluded = false; - - credFactory.OnCreateEnvironmentCredential = _ => environmentCredentialIncluded = true; - credFactory.OnCreateAzureCliCredential = _ => cliCredentialIncluded = true; - credFactory.OnCreateInteractiveBrowserCredential = (tenantId, _, _) => interactiveBrowserCredentialIncluded = true; - credFactory.OnCreateVisualStudioCredential = (tenantId, _) => visualStudioCredentialIncluded = true; - credFactory.OnCreateVisualStudioCodeCredential = (tenantId, _) => visualStudioCodeCredentialIncluded = true; - credFactory.OnCreateAzurePowerShellCredential = _ => powerShellCredentialsIncluded = true; - credFactory.OnCreateManagedIdentityCredential = (clientId, _) => - { - managedIdentityCredentialIncluded = true; - }; - credFactory.OnCreateSharedTokenCacheCredential = (tenantId, username, _) => - { - sharedTokenCacheCredentialIncluded = true; - }; - - var options = new DefaultAzureCredentialOptions - { - ExcludeEnvironmentCredential = excludeEnvironmentCredential, - ExcludeManagedIdentityCredential = excludeManagedIdentityCredential, - ExcludeSharedTokenCacheCredential = excludeSharedTokenCacheCredential, - ExcludeAzureCliCredential = excludeCliCredential, - ExcludeInteractiveBrowserCredential = excludeInteractiveBrowserCredential, - ExcludeVisualStudioCredential = excludeVisualStudioCredential, - ExcludeVisualStudioCodeCredential = excludeVisualStudioCodeCredential, - ExcludeAzurePowerShellCredential = excludeAzurePowerShellCredential - }; - - if (excludeEnvironmentCredential && excludeManagedIdentityCredential && excludeSharedTokenCacheCredential && excludeVisualStudioCredential && excludeVisualStudioCodeCredential && excludeCliCredential && excludeAzurePowerShellCredential && excludeInteractiveBrowserCredential) - { - Assert.Throws(() => new DefaultAzureCredential(options)); - } - else - { - new DefaultAzureCredential(credFactory, options); - - Assert.AreEqual(!excludeEnvironmentCredential, environmentCredentialIncluded); - Assert.AreEqual(!excludeManagedIdentityCredential, managedIdentityCredentialIncluded); - Assert.AreEqual(!excludeSharedTokenCacheCredential, sharedTokenCacheCredentialIncluded); - Assert.AreEqual(!excludeCliCredential, cliCredentialIncluded); - Assert.AreEqual(!excludeAzurePowerShellCredential, powerShellCredentialsIncluded); - Assert.AreEqual(!excludeInteractiveBrowserCredential, interactiveBrowserCredentialIncluded); - Assert.AreEqual(!excludeVisualStudioCredential, visualStudioCredentialIncluded); - Assert.AreEqual(!excludeVisualStudioCodeCredential, visualStudioCodeCredentialIncluded); - } - } - [Test] public void ValidateAllUnavailable([Values(true, false)]bool excludeEnvironmentCredential, [Values(true, false)]bool excludeManagedIdentityCredential, @@ -378,7 +81,19 @@ public void ValidateAllUnavailable([Values(true, false)]bool excludeEnvironmentC Assert.Pass(); } - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); + var options = new DefaultAzureCredentialOptions + { + ExcludeEnvironmentCredential = excludeEnvironmentCredential, + ExcludeManagedIdentityCredential = excludeManagedIdentityCredential, + ExcludeSharedTokenCacheCredential = excludeSharedTokenCacheCredential, + ExcludeVisualStudioCredential = excludeVisualStudioCredential, + ExcludeVisualStudioCodeCredential = excludeVisualStudioCodeCredential, + ExcludeAzureCliCredential = excludeCliCredential, + ExcludeAzurePowerShellCredential = excludePowerShellCredential, + ExcludeInteractiveBrowserCredential = excludeInteractiveBrowserCredential + }; + + var credFactory = new MockDefaultAzureCredentialFactory(options); void SetupMockForException(Mock mock) where T : TokenCredential => mock.Setup(m => m.GetTokenAsync(It.IsAny(), It.IsAny())) @@ -386,34 +101,22 @@ void SetupMockForException(Mock mock) where T : TokenCredential => credFactory.OnCreateEnvironmentCredential = c => SetupMockForException(c); - credFactory.OnCreateInteractiveBrowserCredential = (_, _, c) => + credFactory.OnCreateInteractiveBrowserCredential = c => SetupMockForException(c); - credFactory.OnCreateManagedIdentityCredential = (_, c) => + credFactory.OnCreateManagedIdentityCredential = c => SetupMockForException(c); - credFactory.OnCreateSharedTokenCacheCredential = (_, _, c) => + credFactory.OnCreateSharedTokenCacheCredential = c => SetupMockForException(c); credFactory.OnCreateAzureCliCredential = c => SetupMockForException(c); credFactory.OnCreateAzurePowerShellCredential = c => SetupMockForException(c); - credFactory.OnCreateVisualStudioCredential = (_, c) => + credFactory.OnCreateVisualStudioCredential = c => SetupMockForException(c); - credFactory.OnCreateVisualStudioCodeCredential = (_, c) => + credFactory.OnCreateVisualStudioCodeCredential = c => SetupMockForException(c); - var options = new DefaultAzureCredentialOptions - { - ExcludeEnvironmentCredential = excludeEnvironmentCredential, - ExcludeManagedIdentityCredential = excludeManagedIdentityCredential, - ExcludeSharedTokenCacheCredential = excludeSharedTokenCacheCredential, - ExcludeVisualStudioCredential = excludeVisualStudioCredential, - ExcludeVisualStudioCodeCredential = excludeVisualStudioCodeCredential, - ExcludeAzureCliCredential = excludeCliCredential, - ExcludeAzurePowerShellCredential = excludePowerShellCredential, - ExcludeInteractiveBrowserCredential = excludeInteractiveBrowserCredential - }; - - var cred = new DefaultAzureCredential(credFactory, options); + var cred = new DefaultAzureCredential(credFactory); var ex = Assert.ThrowsAsync(async () => await cred.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); @@ -455,7 +158,17 @@ void SetupMockForException(Mock mock) where T : TokenCredential => [TestCaseSource(nameof(AllCredentialTypes))] public void ValidateUnhandledException(Type credentialType) { - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); + var options = new DefaultAzureCredentialOptions + { + ExcludeEnvironmentCredential = false, + ExcludeManagedIdentityCredential = false, + ExcludeSharedTokenCacheCredential = false, + ExcludeAzureCliCredential = false, + ExcludeAzurePowerShellCredential = false, + ExcludeInteractiveBrowserCredential = false + }; + + var credFactory = new MockDefaultAzureCredentialFactory(options); void SetupMockForException(Mock mock) where T : TokenCredential { @@ -474,36 +187,26 @@ void SetupMockForException(Mock mock) where T : TokenCredential credFactory.OnCreateEnvironmentCredential = c => SetupMockForException(c); - credFactory.OnCreateManagedIdentityCredential = (_, c) => + credFactory.OnCreateManagedIdentityCredential = c => SetupMockForException(c); - credFactory.OnCreateSharedTokenCacheCredential = (_, _, c) => + credFactory.OnCreateSharedTokenCacheCredential = c => SetupMockForException(c); - credFactory.OnCreateVisualStudioCredential = (_, c) => + credFactory.OnCreateVisualStudioCredential = c => SetupMockForException(c); - credFactory.OnCreateVisualStudioCodeCredential = (_, c) => + credFactory.OnCreateVisualStudioCodeCredential = c => SetupMockForException(c); credFactory.OnCreateAzureCliCredential = c => SetupMockForException(c); credFactory.OnCreateAzurePowerShellCredential = c => SetupMockForException(c); - credFactory.OnCreateInteractiveBrowserCredential = (_, _, c) => + credFactory.OnCreateInteractiveBrowserCredential = c => { c.Setup(m => m.GetTokenAsync(It.IsAny(), It.IsAny())) .Throws(new MockClientException("InteractiveBrowserCredential unhandled exception")); }; - var options = new DefaultAzureCredentialOptions - { - ExcludeEnvironmentCredential = false, - ExcludeManagedIdentityCredential = false, - ExcludeSharedTokenCacheCredential = false, - ExcludeAzureCliCredential = false, - ExcludeAzurePowerShellCredential = false, - ExcludeInteractiveBrowserCredential = false - }; - - var cred = new DefaultAzureCredential(credFactory, options); + var cred = new DefaultAzureCredential(credFactory); var ex = Assert.ThrowsAsync(async () => await cred.GetTokenAsync(new TokenRequestContext(MockScopes.Default))); var unhandledException = ex.InnerException is AggregateException ae ? ae.InnerExceptions.Last() : ex.InnerException; @@ -528,7 +231,6 @@ public async Task ValidateSelectedCredentialCaching(Type availableCredential) { var expToken = new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.MaxValue); List calledCredentials = new(); - var credFactory = GetMockDefaultAzureCredentialFactory(availableCredential, expToken, calledCredentials); var options = new DefaultAzureCredentialOptions { @@ -540,7 +242,9 @@ public async Task ValidateSelectedCredentialCaching(Type availableCredential) ExcludeInteractiveBrowserCredential = false }; - var cred = new DefaultAzureCredential(credFactory, options); + var credFactory = GetMockDefaultAzureCredentialFactory(options, availableCredential, expToken, calledCredentials); + + var cred = new DefaultAzureCredential(credFactory); AccessToken actToken = await cred.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); @@ -572,7 +276,6 @@ public async Task CredentialTypeLogged(Type availableCredential) var expToken = new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.MaxValue); List calledCredentials = new(); - var credFactory = GetMockDefaultAzureCredentialFactory(availableCredential, expToken, calledCredentials); var options = new DefaultAzureCredentialOptions { @@ -583,16 +286,18 @@ public async Task CredentialTypeLogged(Type availableCredential) ExcludeInteractiveBrowserCredential = false, }; - var cred = new DefaultAzureCredential(credFactory, options); + var credFactory = GetMockDefaultAzureCredentialFactory(options, availableCredential, expToken, calledCredentials); + + var cred = new DefaultAzureCredential(credFactory); await cred.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); Assert.That(messages, Has.Some.Match(availableCredential.Name).And.Some.Match("DefaultAzureCredential credential selected")); } - internal MockDefaultAzureCredentialFactory GetMockDefaultAzureCredentialFactory(Type availableCredential, AccessToken expToken, List calledCredentials) + internal MockDefaultAzureCredentialFactory GetMockDefaultAzureCredentialFactory(DefaultAzureCredentialOptions options, Type availableCredential, AccessToken expToken, List calledCredentials) { - var credFactory = new MockDefaultAzureCredentialFactory(CredentialPipeline.GetInstance(null)); + var credFactory = new MockDefaultAzureCredentialFactory(options); void SetupMockForException(Mock mock) where T : TokenCredential => mock.Setup(m => m.GetTokenAsync(It.IsAny(), It.IsAny())) @@ -604,17 +309,17 @@ void SetupMockForException(Mock mock) where T : TokenCredential => credFactory.OnCreateEnvironmentCredential = c => SetupMockForException(c); - credFactory.OnCreateManagedIdentityCredential = (clientId, c) => + credFactory.OnCreateManagedIdentityCredential = c => SetupMockForException(c); - credFactory.OnCreateSharedTokenCacheCredential = (tenantId, username, c) => + credFactory.OnCreateSharedTokenCacheCredential = c => SetupMockForException(c); credFactory.OnCreateAzureCliCredential = c => SetupMockForException(c); - credFactory.OnCreateInteractiveBrowserCredential = (_, _, c) => + credFactory.OnCreateInteractiveBrowserCredential = c => SetupMockForException(c); - credFactory.OnCreateVisualStudioCredential = (_, c) => + credFactory.OnCreateVisualStudioCredential = c => SetupMockForException(c); - credFactory.OnCreateVisualStudioCodeCredential = (_, c) => + credFactory.OnCreateVisualStudioCodeCredential = c => SetupMockForException(c); credFactory.OnCreateAzurePowerShellCredential = c => SetupMockForException(c); diff --git a/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs b/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs index 8b0e930ea5ea..559d676f3273 100644 --- a/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs @@ -66,9 +66,9 @@ public void Setup() [Test] public async Task AuthenticateWithDeviceCodeMockAsync([Values(null, TenantIdHint)] string tenantId, [Values(true)] bool allowMultiTenantAuthentication) { - options = new TokenCredentialOptions(); + var options = new DeviceCodeCredentialOptions { AdditionallyAllowedTenants = { TenantIdHint } }; var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context) ; + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants) ; var cred = InstrumentClient( new DeviceCodeCredential((code, _) => VerifyDeviceCode(code, expectedCode), TenantId, ClientId, options, null, mockPublicMsalClient)); @@ -81,6 +81,25 @@ public async Task AuthenticateWithDeviceCodeMockAsync([Values(null, TenantIdHint Assert.AreEqual(token.Token, expectedToken); } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + var options = new DeviceCodeCredentialOptions { TenantId = parameters.TenantId }; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + var authResult = AuthenticationResultFactory.Create(); + var msalClientMock = new MockMsalPublicClient(authResult); + + var cred = InstrumentClient( + new DeviceCodeCredential((code, _) => Task.CompletedTask, parameters.TenantId, ClientId, options, null, msalClientMock)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public void RespectsIsPIILoggingEnabled([Values(true, false)] bool isLoggingPIIEnabled) { diff --git a/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialTests.cs b/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialTests.cs index 89c4626265ac..23e649f7ce15 100644 --- a/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialTests.cs @@ -224,9 +224,9 @@ public void DisableAutomaticAuthenticationException() public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId, [Values(true)] bool allowMultiTenantAuthentication) { TestSetup(); - var options = new InteractiveBrowserCredentialOptions(); + var options = new InteractiveBrowserCredentialOptions() { AdditionallyAllowedTenants = { TenantIdHint }}; var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); var credential = InstrumentClient( new InteractiveBrowserCredential( @@ -242,6 +242,23 @@ public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId, Assert.AreEqual(expiresOn, actualToken.ExpiresOn, "expiresOn should match"); } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + var options = new InteractiveBrowserCredentialOptions { TenantId = parameters.TenantId }; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + var mockMsalClient = new MockMsalPublicClient(AuthenticationResultFactory.Create()); + var cred = InstrumentClient( + new InteractiveBrowserCredential(parameters.TenantId, Constants.DeveloperSignOnClientId, options, null, mockMsalClient)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + public class ExtendedInteractiveBrowserCredentialOptions : InteractiveBrowserCredentialOptions, IMsalPublicClientInitializerOptions { private Action _beforeBuildClient; diff --git a/sdk/identity/Azure.Identity/tests/Mock/MockDefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/tests/Mock/MockDefaultAzureCredentialFactory.cs index aa270e8aac89..806d7d55cfec 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/MockDefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/MockDefaultAzureCredentialFactory.cs @@ -9,21 +9,22 @@ namespace Azure.Identity.Tests.Mock { internal class MockDefaultAzureCredentialFactory : DefaultAzureCredentialFactory { - public MockDefaultAzureCredentialFactory(CredentialPipeline pipeline) : base(pipeline) { } + public MockDefaultAzureCredentialFactory(DefaultAzureCredentialOptions options) : base(options) { } + public MockDefaultAzureCredentialFactory(DefaultAzureCredentialOptions options, CredentialPipeline pipeline) : base(options, pipeline) { } public Action> OnCreateEnvironmentCredential { get; set; } private Mock mockEnvironmentCredential = new(); public Action> OnCreateAzureCliCredential { get; set; } private Mock mockAzureCliCredential = new(); - public Action> OnCreateManagedIdentityCredential { get; set; } + public Action> OnCreateManagedIdentityCredential { get; set; } private Mock mockManagedIdentityCredential = new(); - public Action> OnCreateSharedTokenCacheCredential { get; set; } + public Action> OnCreateSharedTokenCacheCredential { get; set; } private Mock mockSharedTokenCacheCredential = new(); - public Action> OnCreateInteractiveBrowserCredential { get; set; } + public Action> OnCreateInteractiveBrowserCredential { get; set; } private Mock mockInteractiveBrowserCredential = new(); - public Action> OnCreateVisualStudioCredential { get; set; } + public Action> OnCreateVisualStudioCredential { get; set; } private Mock mockVisualStudioCredential = new(); - public Action> OnCreateVisualStudioCodeCredential { get; set; } + public Action> OnCreateVisualStudioCodeCredential { get; set; } private Mock mockVisualStudioCodeCredential = new(); public Action> OnCreateAzurePowerShellCredential { get; set; } private Mock mockAzurePowershellCredential = new(); @@ -34,15 +35,15 @@ public override TokenCredential CreateEnvironmentCredential() return mockEnvironmentCredential.Object; } - public override TokenCredential CreateManagedIdentityCredential(DefaultAzureCredentialOptions options) + public override TokenCredential CreateManagedIdentityCredential() { - OnCreateManagedIdentityCredential?.Invoke(options, mockManagedIdentityCredential); + OnCreateManagedIdentityCredential?.Invoke(mockManagedIdentityCredential); return mockManagedIdentityCredential.Object; } - public override TokenCredential CreateSharedTokenCacheCredential(string tenantId, string username) + public override TokenCredential CreateSharedTokenCacheCredential() { - OnCreateSharedTokenCacheCredential?.Invoke(tenantId, username, mockSharedTokenCacheCredential); + OnCreateSharedTokenCacheCredential?.Invoke(mockSharedTokenCacheCredential); return mockSharedTokenCacheCredential.Object; } @@ -58,21 +59,21 @@ public override TokenCredential CreateAzurePowerShellCredential() return mockAzurePowershellCredential.Object; } - public override TokenCredential CreateInteractiveBrowserCredential(string tenantId, string clientId) + public override TokenCredential CreateInteractiveBrowserCredential() { - OnCreateInteractiveBrowserCredential?.Invoke(tenantId, clientId, mockInteractiveBrowserCredential); + OnCreateInteractiveBrowserCredential?.Invoke(mockInteractiveBrowserCredential); return mockInteractiveBrowserCredential.Object; } - public override TokenCredential CreateVisualStudioCredential(string tenantId) + public override TokenCredential CreateVisualStudioCredential() { - OnCreateVisualStudioCredential?.Invoke(tenantId, mockVisualStudioCredential); + OnCreateVisualStudioCredential?.Invoke(mockVisualStudioCredential); return mockVisualStudioCredential.Object; } - public override TokenCredential CreateVisualStudioCodeCredential(string tenantId) + public override TokenCredential CreateVisualStudioCodeCredential() { - OnCreateVisualStudioCodeCredential?.Invoke(tenantId, mockVisualStudioCodeCredential); + OnCreateVisualStudioCodeCredential?.Invoke(mockVisualStudioCodeCredential); return mockVisualStudioCodeCredential.Object; } } diff --git a/sdk/identity/Azure.Identity/tests/Mock/MockMsalPublicClient.cs b/sdk/identity/Azure.Identity/tests/Mock/MockMsalPublicClient.cs index e6c90150d000..382e798c305b 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/MockMsalPublicClient.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/MockMsalPublicClient.cs @@ -41,6 +41,17 @@ internal class MockMsalPublicClient : MsalPublicClient public MockMsalPublicClient() { } + public MockMsalPublicClient(AuthenticationResult result) + { + AuthFactory = (_,_) => result; + UserPassAuthFactory = (_,_) => result; + InteractiveAuthFactory = (_,_,_,_,_,_,_) => result; + SilentAuthFactory = (_,_) => result; + ExtendedSilentAuthFactory = (_,_,_,_,_,_) => result; + DeviceCodeAuthFactory = (_,_) => result; + RefreshTokenFactory = (_,_,_,_,_,_,_) => result; + } + public MockMsalPublicClient(CredentialPipeline pipeline, string tenantId, string clientId, string redirectUrl, TokenCredentialOptions options) : base(pipeline, tenantId, clientId, redirectUrl, options) { } diff --git a/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs index 3652568ab673..d670e02a9a80 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using Azure.Core; namespace Azure.Identity.Tests.Mock @@ -12,7 +13,7 @@ internal class TestDefaultAzureCredentialFactory : DefaultAzureCredentialFactory private readonly IProcessService _processService; private readonly IVisualStudioCodeAdapter _vscAdapter; - public TestDefaultAzureCredentialFactory(TokenCredentialOptions options, IFileSystemService fileSystem, IProcessService processService, IVisualStudioCodeAdapter vscAdapter) + public TestDefaultAzureCredentialFactory(DefaultAzureCredentialOptions options, IFileSystemService fileSystem, IProcessService processService, IVisualStudioCodeAdapter vscAdapter) : base(options) { _fileSystem = fileSystem; @@ -21,27 +22,59 @@ public TestDefaultAzureCredentialFactory(TokenCredentialOptions options, IFileSy } public override TokenCredential CreateEnvironmentCredential() - => new EnvironmentCredential(Pipeline); + => new EnvironmentCredential(Pipeline, Options); - public override TokenCredential CreateManagedIdentityCredential(DefaultAzureCredentialOptions options) - => new ManagedIdentityCredential(new ManagedIdentityClient(Pipeline, options.ManagedIdentityClientId)); + public override TokenCredential CreateManagedIdentityCredential() + => new ManagedIdentityCredential(new ManagedIdentityClient(Pipeline, Options.ManagedIdentityClientId)); - public override TokenCredential CreateSharedTokenCacheCredential(string tenantId, string username) - => new SharedTokenCacheCredential(tenantId, username, null, Pipeline); + public override TokenCredential CreateSharedTokenCacheCredential() + => new SharedTokenCacheCredential(Options.SharedTokenCacheTenantId, Options.SharedTokenCacheUsername, Options, Pipeline); - public override TokenCredential CreateInteractiveBrowserCredential(string tenantId, string clientId) - => new InteractiveBrowserCredential(tenantId, clientId ?? Constants.DeveloperSignOnClientId, new InteractiveBrowserCredentialOptions(), Pipeline); + public override TokenCredential CreateInteractiveBrowserCredential() + => new InteractiveBrowserCredential(Options.InteractiveBrowserTenantId, Options.InteractiveBrowserCredentialClientId ?? Constants.DeveloperSignOnClientId, new InteractiveBrowserCredentialOptions() { AdditionallyAllowedTenantsCore = Options.AdditionallyAllowedTenantsCore, AuthorityHost = Options.AuthorityHost }, Pipeline); public override TokenCredential CreateAzureCliCredential() - => new AzureCliCredential(Pipeline, _processService); + { + var options = new AzureCliCredentialOptions + { + TenantId = Options.TenantId, + AdditionallyAllowedTenantsCore = Options.AdditionallyAllowedTenants.ToList() + }; + + return new AzureCliCredential(Pipeline, _processService, options); + } + + public override TokenCredential CreateVisualStudioCredential() + { + var options = new VisualStudioCredentialOptions + { + TenantId = Options.VisualStudioTenantId, + AdditionallyAllowedTenantsCore = Options.AdditionallyAllowedTenants.ToList() + }; + + return new VisualStudioCredential(Options.VisualStudioTenantId, Pipeline, _fileSystem, _processService, options); + } - public override TokenCredential CreateVisualStudioCredential(string tenantId) - => new VisualStudioCredential(tenantId, Pipeline, _fileSystem, _processService); + public override TokenCredential CreateVisualStudioCodeCredential() + { + var options = new VisualStudioCodeCredentialOptions + { + TenantId = Options.VisualStudioCodeTenantId, + AdditionallyAllowedTenantsCore = Options.AdditionallyAllowedTenants.ToList() + }; - public override TokenCredential CreateVisualStudioCodeCredential(string tenantId) - => new VisualStudioCodeCredential(new VisualStudioCodeCredentialOptions { TenantId = tenantId }, Pipeline, default, _fileSystem, _vscAdapter); + return new VisualStudioCodeCredential(options, Pipeline, default, _fileSystem, _vscAdapter); + } public override TokenCredential CreateAzurePowerShellCredential() - => new AzurePowerShellCredential(new AzurePowerShellCredentialOptions(), Pipeline, _processService); + { + var options = new AzurePowerShellCredentialOptions + { + TenantId = Options.VisualStudioCodeTenantId, + AdditionallyAllowedTenantsCore = Options.AdditionallyAllowedTenants.ToList() + }; + + return new AzurePowerShellCredential(options, Pipeline, _processService); + } } } diff --git a/sdk/identity/Azure.Identity/tests/OnBehalfOfCredentialTests.cs b/sdk/identity/Azure.Identity/tests/OnBehalfOfCredentialTests.cs index fea8f990bd3c..c24de758351d 100644 --- a/sdk/identity/Azure.Identity/tests/OnBehalfOfCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/OnBehalfOfCredentialTests.cs @@ -89,9 +89,9 @@ public async Task UsesTenantIdHint( [Values(null, TenantId)] string explicitTenantId) { TestSetup(); - options = new OnBehalfOfCredentialOptions(); + options = new OnBehalfOfCredentialOptions() { AdditionallyAllowedTenants = { TenantIdHint } }; var context = new TokenRequestContext(new[] {Scope}, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(explicitTenantId, context); + expectedTenantId = TenantIdResolver.Resolve(explicitTenantId, context, TenantIdResolver.AllTenants); OnBehalfOfCredential client = InstrumentClient( new OnBehalfOfCredential( TenantId, @@ -106,6 +106,30 @@ public async Task UsesTenantIdHint( Assert.AreEqual(token.Token, expectedToken, "Should be the expected token value"); } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + // no need to test with null TenantId since we can't construct this credential without it + if (parameters.TenantId == null) + { + Assert.Ignore("Null TenantId test does not apply to this credential"); + } + + var options = new OnBehalfOfCredentialOptions(); + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var msalClientMock = new MockMsalConfidentialClient(AuthenticationResultFactory.Create()); + + var cred = InstrumentClient(new OnBehalfOfCredential(parameters.TenantId, ClientId, "secret", "userAssertion", options, null, msalClientMock)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public async Task SendCertificateChain([Values(true, false)] bool sendCertChain) { diff --git a/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs b/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs index 59284eaf4a4c..20b92fc4ec43 100644 --- a/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs @@ -601,7 +601,7 @@ public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId, TestSetup(); var options = new SharedTokenCacheCredentialOptions(); var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); mockPublicMsalClient.Accounts = new List { new MockAccount(expectedUsername, expectedTenantId) }; var credential = InstrumentClient(new SharedTokenCacheCredential(TenantId, null, options, null, mockPublicMsalClient)); @@ -611,5 +611,11 @@ public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId, Assert.AreEqual(expectedToken, token.Token); Assert.AreEqual(expiresOn, token.ExpiresOn); } + + public override Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Assert.Ignore("Tenant Enforcement tests do not apply to the SharedTokenCacheCredential."); + return Task.CompletedTask; + } } } diff --git a/sdk/identity/Azure.Identity/tests/TenantIdResolverTests.cs b/sdk/identity/Azure.Identity/tests/TenantIdResolverTests.cs index 9abd6a4df532..5b6d55300711 100644 --- a/sdk/identity/Azure.Identity/tests/TenantIdResolverTests.cs +++ b/sdk/identity/Azure.Identity/tests/TenantIdResolverTests.cs @@ -4,10 +4,14 @@ using System; using System.Collections.Generic; using System.Diagnostics.Tracing; +using System.Linq; +using System.Reflection.Metadata; +using System.Threading.Tasks; using Azure.Core; using Azure.Core.Diagnostics; using Azure.Core.TestFramework; using NUnit.Framework; +using static Azure.Identity.Tests.CredentialTestBase; namespace Azure.Identity.Tests { @@ -19,18 +23,18 @@ public class TenantIdResolverTests public static IEnumerable ResolveInputs() { - yield return new object[] {TenantId, Context_Hint, false, Context_Hint.TenantId, new[] {string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndUsedEventMessage, TenantId, Context_Hint.TenantId)}}; - yield return new object[] {TenantId, Context_NoHint, false, TenantId, new string[] { }}; - yield return new object[] {TenantId, Context_Hint, true, TenantId, new[] {string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndNotUsedEventMessage, TenantId, Context_Hint.TenantId)}}; - yield return new object[] {TenantId, Context_NoHint, true, TenantId, new string[] { }}; - yield return new object[] {Constants.AdfsTenantId, Context_Hint, false, Constants.AdfsTenantId, new[] {string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndNotUsedEventMessage, Constants.AdfsTenantId, Context_Hint.TenantId)}}; - yield return new object[] {Constants.AdfsTenantId, Context_NoHint, false, Constants.AdfsTenantId, new string[] { }}; - yield return new object[] {Constants.AdfsTenantId, Context_Hint, true, Constants.AdfsTenantId, new[] {string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndNotUsedEventMessage, Constants.AdfsTenantId, Context_Hint.TenantId)}}; - yield return new object[] {Constants.AdfsTenantId, Context_NoHint, true, Constants.AdfsTenantId, new string[] { }}; - yield return new object[] {null, Context_Hint, false, Context_Hint.TenantId, new string[] { }}; - yield return new object[] {null, Context_NoHint, false, null, new string[] { }}; - yield return new object[] {null, Context_Hint, true, null, new string[] { }}; - yield return new object[] {null, Context_NoHint, true, null, new string[] { }}; + yield return new object[] { TenantId, Context_Hint, false, Context_Hint.TenantId, new[] { string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndUsedEventMessage, TenantId, Context_Hint.TenantId) } }; + yield return new object[] { TenantId, Context_NoHint, false, TenantId, new string[] { } }; + yield return new object[] { TenantId, Context_Hint, true, TenantId, new[] { string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndNotUsedEventMessage, TenantId, Context_Hint.TenantId) } }; + yield return new object[] { TenantId, Context_NoHint, true, TenantId, new string[] { } }; + yield return new object[] { Constants.AdfsTenantId, Context_Hint, false, Constants.AdfsTenantId, new[] { string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndNotUsedEventMessage, Constants.AdfsTenantId, Context_Hint.TenantId) } }; + yield return new object[] { Constants.AdfsTenantId, Context_NoHint, false, Constants.AdfsTenantId, new string[] { } }; + yield return new object[] { Constants.AdfsTenantId, Context_Hint, true, Constants.AdfsTenantId, new[] { string.Format(AzureIdentityEventSource.TenantIdDiscoveredAndNotUsedEventMessage, Constants.AdfsTenantId, Context_Hint.TenantId) } }; + yield return new object[] { Constants.AdfsTenantId, Context_NoHint, true, Constants.AdfsTenantId, new string[] { } }; + yield return new object[] { null, Context_Hint, false, Context_Hint.TenantId, new string[] { } }; + yield return new object[] { null, Context_NoHint, false, null, new string[] { } }; + yield return new object[] { null, Context_Hint, true, null, new string[] { } }; + yield return new object[] { null, Context_NoHint, true, null, new string[] { } }; } [Test] @@ -61,7 +65,7 @@ public void Resolve(string tenantId, TokenRequestContext context, bool? disableM disableMultiTenantAuth.Value.ToString()); } - result = TenantIdResolver.Resolve(tenantId, context); + result = TenantIdResolver.Resolve(tenantId, context, TenantIdResolver.AllTenants); Assert.AreEqual(expectedresult, result); Assert.AreEqual(expectedEvents, messages); } @@ -70,5 +74,40 @@ public void Resolve(string tenantId, TokenRequestContext context, bool? disableM env?.Dispose(); } } + + public static IEnumerable GetAllowedTenantsTestCases() + { + return CredentialTestBase.GetAllowedTenantsTestCases(); + } + + [TestCaseSource(nameof(GetAllowedTenantsTestCases))] + public void VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + var additionallyAllowedTenants = TenantIdResolver.ResolveAddionallyAllowedTenantIds(parameters.AdditionallyAllowedTenants); + + AssertAllowedTenantIdsEnforcedAsync(parameters.TenantId, parameters.TokenRequestContext, additionallyAllowedTenants); + } + + public static void AssertAllowedTenantIdsEnforcedAsync(string tenantId, TokenRequestContext tokenRequestContext, string[] additionallyAllowedTenants) + { + bool expAllowed = tenantId == null + || tokenRequestContext.TenantId == null + || tenantId == tokenRequestContext.TenantId + || additionallyAllowedTenants.Contains(tokenRequestContext.TenantId) + || additionallyAllowedTenants.Contains("*"); + + if (expAllowed) + { + var resolvedTenantId = TenantIdResolver.Resolve(tenantId, tokenRequestContext, additionallyAllowedTenants); + + Assert.AreEqual(tokenRequestContext.TenantId ?? tenantId, resolvedTenantId); + } + else + { + var ex = Assert.Throws(() => TenantIdResolver.Resolve(tenantId, tokenRequestContext, additionallyAllowedTenants)); + + StringAssert.Contains($"The current credential is not configured to acquire tokens for tenant {tokenRequestContext.TenantId}", ex.Message); + } + } } } diff --git a/sdk/identity/Azure.Identity/tests/TestAccessorExtensions.cs b/sdk/identity/Azure.Identity/tests/TestAccessorExtensions.cs index e5797c02a74b..a551c721dd30 100644 --- a/sdk/identity/Azure.Identity/tests/TestAccessorExtensions.cs +++ b/sdk/identity/Azure.Identity/tests/TestAccessorExtensions.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; using System.Reflection; using System.Security; using Azure.Core; +using NUnit.Framework; namespace Azure.Identity.Tests { @@ -36,5 +38,13 @@ public static TokenCredential[] _sources(this DefaultAzureCredential credential) { return typeof(DefaultAzureCredential).GetField("_sources", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(credential) as TokenCredential[]; } + + public static void ConditionalAdd(this List list, bool condition, T item) + { + if (condition) + { + list.Add(item); + } + } } } diff --git a/sdk/identity/Azure.Identity/tests/UsernamePasswordCredentialTests.cs b/sdk/identity/Azure.Identity/tests/UsernamePasswordCredentialTests.cs index 08e6812a9747..f3ec62aaccd4 100644 --- a/sdk/identity/Azure.Identity/tests/UsernamePasswordCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/UsernamePasswordCredentialTests.cs @@ -75,9 +75,9 @@ public void RespectsIsPIILoggingEnabled([Values(true, false)] bool isLoggingPIIE public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId, [Values(true)] bool allowMultiTenantAuthentication) { TestSetup(); - var options = new UsernamePasswordCredentialOptions(); + var options = new UsernamePasswordCredentialOptions() { AdditionallyAllowedTenants = { TenantIdHint }}; var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); var credential = InstrumentClient(new UsernamePasswordCredential("user", "password", TenantId, ClientId, options, null, mockPublicMsalClient)); @@ -86,5 +86,29 @@ public async Task UsesTenantIdHint([Values(null, TenantIdHint)] string tenantId, Assert.AreEqual(expectedToken, token.Token); Assert.AreEqual(expiresOn, token.ExpiresOn); } + + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + // no need to test with null TenantId since we can't construct this credential without it + if (parameters.TenantId == null) + { + Assert.Ignore("Null TenantId test does not apply to this credential"); + } + + var options = new UsernamePasswordCredentialOptions(); + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var msalClientMock = new MockMsalPublicClient(AuthenticationResultFactory.Create()); + + var cred = InstrumentClient(new UsernamePasswordCredential("username", "password",parameters.TenantId, ClientId, options, null, msalClientMock)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } } } diff --git a/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs index a2b4d523e6e6..cb29fc9b7b20 100644 --- a/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialLiveTests.cs @@ -83,7 +83,7 @@ public async Task AuthenticateWithVscCredential_TenantInSettings() var fileSystemService = CredentialTestHelpers.CreateFileSystemForVisualStudioCode(TestEnvironment, cloudName); using IDisposable fixture = await CredentialTestHelpers.CreateRefreshTokenFixtureAsync(TestEnvironment, Mode, ExpectedServiceName, cloudName); - var options = InstrumentClientOptions(new VisualStudioCodeCredentialOptions { TenantId = Guid.NewGuid().ToString() }); + var options = InstrumentClientOptions(new VisualStudioCodeCredentialOptions { TenantId = "e0bd2321-07fa-4cf0-87b8-00aa2a747329" }); VisualStudioCodeCredential credential = InstrumentClient(new VisualStudioCodeCredential(options, default, default, fileSystemService, default)); AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(new[] {TestEnvironment.KeyvaultScope}), CancellationToken.None); diff --git a/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialTests.cs b/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialTests.cs index a7059009e25f..c5f969eed885 100644 --- a/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/VisualStudioCodeCredentialTests.cs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Core.TestFramework; +using Azure.Identity.Tests.Mock; using NUnit.Framework; namespace Azure.Identity.Tests @@ -47,9 +49,9 @@ public async Task AuthenticateWithVsCodeCredential([Values(null, TenantIdHint)] { using var env = new TestEnvVar(new Dictionary { { "TENANT_ID", TenantId } }); var environment = new IdentityTestEnvironment(); - var options = new VisualStudioCodeCredentialOptions { TenantId = environment.TenantId, Transport = new MockTransport() }; + var options = new VisualStudioCodeCredentialOptions { TenantId = environment.TenantId, AdditionallyAllowedTenants = { TenantIdHint }, Transport = new MockTransport() }; var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(environment.TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(environment.TenantId, context, TenantIdResolver.AllTenants); VisualStudioCodeCredential credential = InstrumentClient( new VisualStudioCodeCredential( @@ -65,6 +67,36 @@ public async Task AuthenticateWithVsCodeCredential([Values(null, TenantIdHint)] Assert.AreEqual(expiresOn, actualToken.ExpiresOn, "expiresOn should match"); } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + using var env = new TestEnvVar(new Dictionary { { "TENANT_ID", TenantId } }); + var environment = new IdentityTestEnvironment(); + var options = new VisualStudioCodeCredentialOptions + { + TenantId = parameters.TenantId, + Transport = new MockTransport() + }; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + options.AdditionallyAllowedTenants.Add(addlTenant); + } + + var msalClientMock = new MockMsalPublicClient(AuthenticationResultFactory.Create()); + + var cred = InstrumentClient( + new VisualStudioCodeCredential( + options, + null, + msalClientMock, + CredentialTestHelpers.CreateFileSystemForVisualStudioCode(environment), + new TestVscAdapter("VS Code Azure", "AzureCloud", expectedToken))); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public void RespectsIsPIILoggingEnabled([Values(true, false)] bool isLoggingPIIEnabled) { @@ -79,7 +111,7 @@ public void AdfsTenantThrowsCredentialUnavailable() { var options = new VisualStudioCodeCredentialOptions { TenantId = "adfs", Transport = new MockTransport() }; var context = new TokenRequestContext(new[] { Scope }); - string expectedTenantId = TenantIdResolver.Resolve(null, context); + string expectedTenantId = TenantIdResolver.Resolve(null, context, TenantIdResolver.AllTenants); VisualStudioCodeCredential credential = InstrumentClient(new VisualStudioCodeCredential(options)); diff --git a/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs b/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs index 1de0eb8e2914..8b66a272bca0 100644 --- a/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/VisualStudioCredentialTests.cs @@ -2,12 +2,14 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Core.TestFramework; +using Azure.Identity.Tests.Mock; using NUnit.Framework; namespace Azure.Identity.Tests @@ -35,10 +37,10 @@ public async Task AuthenticateWithVsCredential([Values(null, TenantIdHint)] stri var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudio(); var (expectedToken, expectedExpiresOn, processOutput) = CredentialTestHelpers.CreateTokenForVisualStudio(); var testProcess = new TestProcess { Output = processOutput }; - var options = new VisualStudioCredentialOptions(); + var options = new VisualStudioCredentialOptions() { AdditionallyAllowedTenants = { TenantIdHint }}; var credential = InstrumentClient(new VisualStudioCredential(TenantId, default, fileSystem, new TestProcessService(testProcess, true), options)); var context = new TokenRequestContext(new[] { Scope }, tenantId: tenantId); - expectedTenantId = TenantIdResolver.Resolve(TenantId, context); + expectedTenantId = TenantIdResolver.Resolve(TenantId, context, TenantIdResolver.AllTenants); var token = await credential.GetTokenAsync(context, default); @@ -50,6 +52,28 @@ public async Task AuthenticateWithVsCredential([Values(null, TenantIdHint)] stri } } + public override async Task VerifyAllowedTenantEnforcement(AllowedTenantsTestParameters parameters) + { + Console.WriteLine(parameters.ToDebugString()); + + var fileSystem = CredentialTestHelpers.CreateFileSystemForVisualStudio(); + var (_, _, processOutput) = CredentialTestHelpers.CreateTokenForVisualStudio(); + var testProcess = new TestProcess { Output = processOutput }; + var vsOptions = new VisualStudioCredentialOptions + { + TenantId = parameters.TenantId + }; + + foreach (var addlTenant in parameters.AdditionallyAllowedTenants) + { + vsOptions.AdditionallyAllowedTenants.Add(addlTenant); + } + + var cred = InstrumentClient(new VisualStudioCredential(parameters.TenantId, default, fileSystem, new TestProcessService(testProcess, true), vsOptions)); + + await AssertAllowedTenantIdsEnforcedAsync(parameters, cred); + } + [Test] public async Task AuthenticateWithVsCredential_FirstProcessFail() { diff --git a/sdk/identity/Azure.Identity/tests/samples/BreakingChangesSnippets.cs b/sdk/identity/Azure.Identity/tests/samples/BreakingChangesSnippets.cs index 4583e90d91a2..a849f98ee748 100644 --- a/sdk/identity/Azure.Identity/tests/samples/BreakingChangesSnippets.cs +++ b/sdk/identity/Azure.Identity/tests/samples/BreakingChangesSnippets.cs @@ -14,5 +14,25 @@ public void SetExcludeSharedTokenCacheCredentialToFalse() }); #endregion } + + public void AddExplicitAdditionallyAllowedTenants() + { + #region Snippet:Identity_BreakingChanges_AddExplicitAdditionallyAllowedTenants + var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions + { + AdditionallyAllowedTenants = { "", "" } + }); + #endregion + } + + public void AddAllAdditionallyAllowedTenants() + { + #region Snippet:Identity_BreakingChanges_AddAllAdditionallyAllowedTenants + var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions + { + AdditionallyAllowedTenants = { "*" } + }); + #endregion + } } } diff --git a/sdk/identity/Azure.Identity/tests/samples/CustomCredentialSnippets.cs b/sdk/identity/Azure.Identity/tests/samples/CustomCredentialSnippets.cs index 1769d00f1c98..7bd46ddef354 100644 --- a/sdk/identity/Azure.Identity/tests/samples/CustomCredentialSnippets.cs +++ b/sdk/identity/Azure.Identity/tests/samples/CustomCredentialSnippets.cs @@ -68,7 +68,7 @@ public void ConfidentialClientCredentialUsage() #endregion } - #region Snippet:OnBehalfOfCredential + //#region Snippet:OnBehalfOfCredential public class OnBehalfOfCredential : TokenCredential { private readonly IConfidentialClientApplication _confidentialClient; @@ -93,7 +93,7 @@ public override async ValueTask GetTokenAsync(TokenRequestContext r return new AccessToken(result.AccessToken, result.ExpiresOn); } } - #endregion + //#endregion public void OnBehalfOfCredentialUsage() { @@ -101,11 +101,11 @@ public void OnBehalfOfCredentialUsage() string clientId = "00000000-0000-0000-0000-000000000000"; string userAccessToken = "00000000-0000-0000-0000-000000000000"; - #region Snippet:OnBehalfOfCredentialUsage + //#region Snippet:OnBehalfOfCredentialUsage var oboCredential = new OnBehalfOfCredential(clientId, clientSecret, userAccessToken); var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), oboCredential); - #endregion + //#endregion } } } From 927b214930836bbc2cf6ae60838aa1df8a593f28 Mon Sep 17 00:00:00 2001 From: Scott Schaab Date: Thu, 15 Sep 2022 08:40:51 -0700 Subject: [PATCH 2/3] [Identity] Updating scope validation (#31154) --- .../Azure.Identity/src/ScopeUtilities.cs | 2 +- .../tests/ScopeUtilitiesTests.cs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 sdk/identity/Azure.Identity/tests/ScopeUtilitiesTests.cs diff --git a/sdk/identity/Azure.Identity/src/ScopeUtilities.cs b/sdk/identity/Azure.Identity/src/ScopeUtilities.cs index ffad084ec066..ac7b7f54ee1f 100644 --- a/sdk/identity/Azure.Identity/src/ScopeUtilities.cs +++ b/sdk/identity/Azure.Identity/src/ScopeUtilities.cs @@ -13,7 +13,7 @@ namespace Azure.Identity internal static class ScopeUtilities { private const string DefaultSuffix = "/.default"; - private const string ScopePattern = "^[0-9a-zA-Z-.:/]+$"; + private const string ScopePattern = "^[0-9a-zA-Z-_.:/]+$"; private const string InvalidScopeMessage = "The specified scope is not in expected format. Only alphanumeric characters, '.', '-', ':', and '/' are allowed"; private static readonly Regex scopeRegex = new Regex(ScopePattern); diff --git a/sdk/identity/Azure.Identity/tests/ScopeUtilitiesTests.cs b/sdk/identity/Azure.Identity/tests/ScopeUtilitiesTests.cs new file mode 100644 index 000000000000..9eec6762c6e7 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/ScopeUtilitiesTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using NUnit.Framework; + +namespace Azure.Identity.Tests +{ + public class ScopeUtilitiesTests + { + [TestCase("https://vaults.azure.net/.default")] + [TestCase("https://management.core.windows.net//.default")] + [TestCase("https://graph.microsoft.com/User.Read")] + [TestCase("api://0478121b-afdc-4ecd-91d8-fe015a9e1826/user_impersonation")] + public void ValidateScopesAcceptsValidScopes(string scope) + { + Assert.DoesNotThrow(() => ScopeUtilities.ValidateScope(scope)); + } + + [TestCase("api://0478121b-afdc-4ecd-91d8-fe015a9e1826/invalid scope")] + [TestCase("api://0478121b-afdc-4ecd-91d8-fe015a9e1826/invalid\"scope")] + [TestCase("api://0478121b-afdc-4ecd-91d8-fe015a9e1826/invalid\\scope")] + public void ValidateScopesRejectsInvalidScopes(string scope) + { + Assert.Throws(() => ScopeUtilities.ValidateScope(scope)); + } + } +} From 3627e3cbc75c628f659d033ed9270c9d02ab9038 Mon Sep 17 00:00:00 2001 From: Scott Schaab Date: Mon, 19 Sep 2022 13:20:24 -0700 Subject: [PATCH 3/3] Identity Updating docs for 1.7.0 release (#31251) * Identity Updating docs for 1.7.0 release * update release date --- sdk/identity/Azure.Identity/BREAKING_CHANGES.md | 4 +++- sdk/identity/Azure.Identity/CHANGELOG.md | 6 +++++- .../src/Credentials/DefaultAzureCredentialOptions.cs | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sdk/identity/Azure.Identity/BREAKING_CHANGES.md b/sdk/identity/Azure.Identity/BREAKING_CHANGES.md index aa0523ac4c00..e32e4ea6941e 100644 --- a/sdk/identity/Azure.Identity/BREAKING_CHANGES.md +++ b/sdk/identity/Azure.Identity/BREAKING_CHANGES.md @@ -15,7 +15,7 @@ var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions }); ``` -- Add `*` to enable token acquisition from any tenant. This is the original behavior and is compatible with versions 1.5.0 through 1.6.1. For example: +- Add `*` to enable token acquisition from any tenant. This is the original behavior and is compatible with previous versions supporting multi tenant authentication. For example: ```C# Snippet:Identity_BreakingChanges_AddAllAdditionallyAllowedTenants var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions @@ -26,6 +26,8 @@ var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions Note: Credential types which do not require a `TenantId` on construction will only throw `AuthenticationFailedException` when the application has provided a value for `TenantId` either in the options or via a constructor overload. If no `TenantId` is specified when constructing the credential, the credential will acquire tokens for any requested `TenantId` regardless of the value of `AdditionallyAllowedTenants`. +More information on this change and the consideration behind it can be found [here](https://aka.ms/azsdk/blog/multi-tenant-guidance). + ## 1.4.0 ### Changed `ExcludeSharedTokenCacheCredential` default value from __false__ to __true__ on `DefaultAzureCredentialsOptions` diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 4cde941e8e0f..cbb1720b0dc3 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## 1.7.0 (2022-09-13) +## 1.7.0 (2022-09-19) ### Features Added - Added `AdditionallyAllowedTenants` to the following credential options to force explicit opt-in behavior for multi-tenant authentication: @@ -17,8 +17,12 @@ - `VisualStudioCredentialOptions` - Added `TenantId` to `DefaultAzureCredentialOptions` to avoid having to set `InteractiveBrowserTenantId`, `SharedTokenCacheTenantId`, `VisualStudioCodeTenantId`, and `VisualStudioTenantId` individually. +### Bugs Fixed +- Fixed overly restrictive scope validation to allow the '_' character, for common scopes such as `user_impersonation` [#30647](https://github.com/Azure/azure-sdk-for-net/issues/30647) + ### Breaking Changes - Credential types supporting multi-tenant authentication will now throw `AuthenticationFailedException` if the requested tenant ID doesn't match the credential's tenant ID, and is not included in the `AdditionallyAllowedTenants` option. Applications must now explicitly add additional tenants to the `AdditionallyAllowedTenants` list, or add '*' to list, to enable acquiring tokens from tenants other than the originally specified tenant ID. See [BREAKING_CHANGES.md](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/BREAKING_CHANGES.md#170). +- `ManagedIdentityCredential` token caching added in 1.7.0-beta.1 has been removed from this release and will be added back in 1.8.0-beta.1 ## 1.7.0-beta.1 (2022-08-09) diff --git a/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs b/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs index 2c6a042fc12b..50cbca268b47 100644 --- a/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/Credentials/DefaultAzureCredentialOptions.cs @@ -163,9 +163,9 @@ public string VisualStudioCodeTenantId } /// - /// Specifies tenants in addition to the specified , , , , for which the credential may acquire tokens. + /// Specifies tenants in addition to the specified for which the credential may acquire tokens. /// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the logged in account can access. - /// If no value is specified for any of the above tenants, this option will have no effect on that authentication method, and the credential will acquire tokens for any requested tenant when using that method. + /// If no value is specified for , this option will have no effect on that authentication method, and the credential will acquire tokens for any requested tenant when using that method. /// This value can also be set by setting the environment variable AZURE_ADDITOINAL_ALLOWED_TENANTS. /// public IList AdditionallyAllowedTenants { get; private set; } = EnvironmentVariables.AdditionallyAllowedTenants;