diff --git a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs index ec6817cef2a1..6a1b67085fbf 100644 --- a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs +++ b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs @@ -13,11 +13,9 @@ // ---------------------------------------------------------------------------------- using Azure.Identity; - using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core; -using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces; using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Models; using Microsoft.Azure.Commands.Common.Authentication.Config.Models; using Microsoft.Azure.Commands.Common.Authentication.Factories; @@ -41,7 +39,6 @@ using Microsoft.WindowsAzure.Commands.Common.Sanitizer; using Microsoft.WindowsAzure.Commands.Common.Utilities; using Microsoft.WindowsAzure.Commands.Utilities.Common; - using System; using System.Collections.Concurrent; using System.Linq; @@ -237,6 +234,10 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod [ValidateNotNullOrEmpty] public string FederatedToken { get; set; } + [Parameter(ParameterSetName = UserParameterSet, Mandatory = false, HelpMessage = "Specifies the claims challenge with base64 encoding.")] + [ValidateNotNullOrEmpty] + public string ClaimsChallenge { get; set; } + protected override IAzureContext DefaultContext { get @@ -353,7 +354,6 @@ public override void ExecuteCmdlet() { subscriptionName = Subscription; } - } else if (AzureSession.Instance.TryGetComponent(nameof(IConfigManager), out var configManager)) { @@ -373,6 +373,15 @@ public override void ExecuteCmdlet() } } + string claimsChallenge = null; + if (this.IsParameterBound(c => c.ClaimsChallenge)) + { + if (!ClaimsChallengeUtilities.TryParseClaimsChallenge(ClaimsChallenge, out claimsChallenge)) + { + throw new PSArgumentException(Resources.InvalidClaimsChallenge, nameof(ClaimsChallenge)); + } + } + var azureAccount = new AzureAccount(); switch (ParameterSetName) @@ -548,6 +557,7 @@ public override void ExecuteCmdlet() SkipValidation, new OpenIDConfiguration(Tenant, baseUri: _environment.ActiveDirectoryAuthority, httpClientFactory: httpClientFactory), WriteWarningEvent, //Could not use WriteWarning directly because it may be in worker thread + claimsChallenge, name, shouldPopulateContextList, MaxContextPopulation, diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 24432ad4eaf2..27c98a9aab37 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -19,6 +19,7 @@ --> ## Upcoming Release +* Added new parameter `-ClaimsChallenge` to `Connect-AzAccount` to support claims challenge authentication for MFA. * Refined the error message when a cmdlet fails because of policy violations about Multi-Factor Authentication (MFA) to provide more actionable guidance. ## Version 5.1.1 diff --git a/src/Accounts/Accounts/Models/RMProfileClient.cs b/src/Accounts/Accounts/Models/RMProfileClient.cs index 46f2f0f836c9..a238636a90b8 100644 --- a/src/Accounts/Accounts/Models/RMProfileClient.cs +++ b/src/Accounts/Accounts/Models/RMProfileClient.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // ---------------------------------------------------------------------------------- + using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces; @@ -22,13 +23,11 @@ using Microsoft.Azure.Commands.Profile.Utilities; using Microsoft.Rest.Azure; using Microsoft.WindowsAzure.Commands.Common; - using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Security; - using AuthenticationMessages = Microsoft.Azure.Commands.Common.Authentication.Properties.Resources; using ProfileMessages = Microsoft.Azure.Commands.Profile.Properties.Resources; using ResourceMessages = Microsoft.Azure.Commands.ResourceManager.Common.Properties.Resources; @@ -52,7 +51,7 @@ private IAzureContext DefaultContext { get { - if(_profile == null || _profile.DefaultContext == null || _profile.DefaultContext.Account == null) + if (_profile == null || _profile.DefaultContext == null || _profile.DefaultContext.Account == null) { throw new PSInvalidOperationException(ResourceMessages.RunConnectAccount); } @@ -130,13 +129,14 @@ public AzureRmProfile Login( bool skipValidation, IOpenIDConfiguration openIDConfigDoc, Action promptAction, + string claimsChallenge = null, string name = null, bool shouldPopulateContextList = true, int maxContextPopulation = Profile.ConnectAzureRmAccountCommand.DefaultMaxContextPopulation, string authScope = null, bool IsInteractiveContextSelectionEnabled = true) { - + WriteInteractiveInformationMessage($"{PSStyle.ForegroundColor.BrightYellow}{Resources.PleaseSelectAccount}{PSStyle.Reset}{System.Environment.NewLine}"); IAzureSubscription defaultSubscription = null; @@ -144,7 +144,7 @@ public AzureRmProfile Login( List subscriptions = new List(); List tempSubscriptions = null; string tenantName = null; - + bool selectSubscriptionFromList = AzureAccount.AccountType.User.Equals(account.Type) && IsInteractiveContextSelectionEnabled && string.IsNullOrEmpty(subscriptionId) && @@ -161,9 +161,9 @@ public AzureRmProfile Login( SubscritpionClientCandidates.Reset(); bool needDataPlanAuthFirst = !string.IsNullOrEmpty(authScope); - if(needDataPlanAuthFirst) + if (needDataPlanAuthFirst) { - var token = AcquireAccessToken(account, environment, tenantIdOrName, password, promptBehavior, promptAction, authScope); + var token = AcquireAccessToken(account, environment, tenantIdOrName, password, promptBehavior, promptAction, claimsChallenge, authScope); promptBehavior = ShowDialog.Never; } @@ -202,7 +202,8 @@ public AzureRmProfile Login( tenantIdOrName, password, promptBehavior, - promptAction); + promptAction, + claimsChallenge); if (!Guid.TryParse(tenantIdOrName, out Guid _)) { @@ -229,7 +230,7 @@ public AzureRmProfile Login( } } } - catch(Exception e) + catch (Exception e) { string baseMessage = string.Format(ProfileMessages.TenantDomainNotFound, tenantIdOrName); var typeMessageMap = new Dictionary @@ -293,7 +294,7 @@ public AzureRmProfile Login( try { - token = AcquireAccessToken(account, environment, tenant.Id, password, ShowDialog.Auto, null); + token = AcquireAccessToken(account, environment, tenant.Id, password, ShowDialog.Auto, null, claimsChallenge); if (accountId == null) { accountId = account.Id; @@ -314,7 +315,7 @@ public AzureRmProfile Login( token = null; } } - catch(Exception e) + catch (Exception e) { WriteWarningMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenant.Id, e.Message)); WriteDebugMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenant.Id, e.ToString())); @@ -334,7 +335,7 @@ public AzureRmProfile Login( defaultTenant = tempTenant; } } - if(tempSubscription != null) + if (tempSubscription != null) { subscriptions.AddRange(tempSubscriptions); } @@ -397,7 +398,7 @@ public AzureRmProfile Login( { var defaultContext = _profile.DefaultContext; var populatedSubscriptions = (maxContextPopulation < 0 || selectSubscriptionFromList) ? ListSubscriptions(tenantIdOrName) : ListSubscriptions(tenantIdOrName).Take(maxContextPopulation); - + foreach (var subscription in populatedSubscriptions) { IAzureTenant tempTenant = InteractiveSubscriptionSelectionHelper.GetDetailedTenantFromQueryHistory(_queriedTenants, subscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant() @@ -449,7 +450,7 @@ public IAzureContext SetCurrentContext(string subscriptionNameOrId, string tenan } var tenantFromSubscription = subscription.GetTenant(); - tenant = string.IsNullOrWhiteSpace(tenantId) ? (string.IsNullOrEmpty(tenantFromSubscription) ? context.Tenant : CreateTenant(tenantFromSubscription)): CreateTenant(tenantId); + tenant = string.IsNullOrWhiteSpace(tenantId) ? (string.IsNullOrEmpty(tenantFromSubscription) ? context.Tenant : CreateTenant(tenantFromSubscription)) : CreateTenant(tenantId); } else if (!string.IsNullOrWhiteSpace(tenantId)) { @@ -536,14 +537,14 @@ public bool TryGetSubscriptionListByName(string tenantId, string subscriptionNam HashSet existedSubscriptionIds = new HashSet(); // Consider subscription in Home tenant first, exclude duplicate subscriptions by id. - foreach(IAzureSubscription subscription in subscriptions) + foreach (IAzureSubscription subscription in subscriptions) { - if (subscription is PSAzureSubscription && subscription.GetTenant() != null + if (subscription is PSAzureSubscription && subscription.GetTenant() != null && subscription.GetHomeTenant().Equals(subscription.GetTenant()) && existedSubscriptionIds.Add(subscription.GetId())) { subscriptionList.Add(subscription); } - + } // Consider other subscriptions. foreach (IAzureSubscription subscription in subscriptions) @@ -679,6 +680,7 @@ private IAccessToken AcquireAccessToken( SecureString password, string promptBehavior, Action promptAction, + string claimsChallenge = null, string resourceId = AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId) { if (account.Type == AzureAccount.AccountType.AccessToken) @@ -689,11 +691,13 @@ private IAccessToken AcquireAccessToken( var optionalParameters = new Dictionary() { - {AuthenticationFactory.TokenCacheParameterName, _cache}, - {AuthenticationFactory.ResourceIdParameterName, resourceId }, - {AuthenticationFactory.CmdletContextParameterName, CmdletContext } + { AuthenticationFactory.ResourceIdParameterName, resourceId }, + { AuthenticationFactory.ClaimsChallengeParameterName, claimsChallenge }, + { AuthenticationFactory.TokenCacheParameterName, _cache }, + { AuthenticationFactory.CmdletContextParameterName, CmdletContext } }; + return AzureSession.Instance.AuthenticationFactory.Authenticate( account, environment, @@ -814,7 +818,7 @@ private List ListAccountTenants( result = SubscriptionAndTenantClient?.ListAccountTenants(commonTenantToken, environment); } - catch(Exception e) + catch (Exception e) { WriteWarningMessage(string.Format(ProfileMessages.UnableToAqcuireToken, commonTenant, e.Message)); WriteDebugMessage(string.Format(ProfileMessages.UnableToAqcuireToken, commonTenant, e.ToString())); @@ -861,7 +865,7 @@ private IEnumerable ListAllSubscriptionsForTenant( { accessToken = AcquireAccessToken(account, environment, tenantId, password, promptBehavior, null); } - catch(Exception e) + catch (Exception e) { WriteWarningMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenantId, e.Message)); WriteDebugMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenantId, e.ToString())); @@ -881,7 +885,7 @@ private void WriteWarningMessage(string message) private void WriteDebugMessage(string message) { - if(DebugLog != null) + if (DebugLog != null) { DebugLog(message); } diff --git a/src/Accounts/Accounts/Properties/Resources.Designer.cs b/src/Accounts/Accounts/Properties/Resources.Designer.cs index ff5e428fe1c4..b7fc32718646 100644 --- a/src/Accounts/Accounts/Properties/Resources.Designer.cs +++ b/src/Accounts/Accounts/Properties/Resources.Designer.cs @@ -690,6 +690,15 @@ internal static string InvalidAzureContext { } } + /// + /// Looks up a localized string similar to Invalid claims challenge format. It should be a valid base64 encoded string.. + /// + internal static string InvalidClaimsChallenge { + get { + return ResourceManager.GetString("InvalidClaimsChallenge", resourceCulture); + } + } + /// /// Looks up a localized string similar to Endpoint provided is invalid. Please check the value and retry again with the correct value.. /// diff --git a/src/Accounts/Accounts/Properties/Resources.resx b/src/Accounts/Accounts/Properties/Resources.resx index 249efaac7a4f..e352ab479bc6 100644 --- a/src/Accounts/Accounts/Properties/Resources.resx +++ b/src/Accounts/Accounts/Properties/Resources.resx @@ -637,4 +637,7 @@ Using authentication with username and password in the command line is strongly discouraged. Consider using one of the recommended authentication methods. For more details, see https://go.microsoft.com/fwlink/?linkid=2276971 + + Invalid claims challenge format. It should be a valid base64 encoded string. + \ No newline at end of file diff --git a/src/Accounts/Accounts/help/Connect-AzAccount.md b/src/Accounts/Accounts/help/Connect-AzAccount.md index 80cecef738cb..56602332ee83 100644 --- a/src/Accounts/Accounts/help/Connect-AzAccount.md +++ b/src/Accounts/Accounts/help/Connect-AzAccount.md @@ -16,8 +16,9 @@ Connect to Azure with an authenticated account for use with cmdlets from the Az ``` Connect-AzAccount [-Environment ] [-Tenant ] [-AccountId ] [-Subscription ] [-AuthScope ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] - [-UseDeviceAuthentication] [-Force] [-Scope ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-UseDeviceAuthentication] [-Force] [-ClaimsChallenge ] [-Scope ] + [-DefaultProfile ] [-WhatIf] [-Confirm] + [] ``` ### ServicePrincipalWithSubscriptionId @@ -25,7 +26,8 @@ Connect-AzAccount [-Environment ] [-Tenant ] [-AccountId ] -Credential [-ServicePrincipal] -Tenant [-Subscription ] [-AuthScope ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] [-Scope ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-DefaultProfile ] [-WhatIf] [-Confirm] + [] ``` ### UserWithCredential @@ -33,7 +35,8 @@ Connect-AzAccount [-Environment ] -Credential [-ServicePr Connect-AzAccount [-Environment ] -Credential [-Tenant ] [-Subscription ] [-AuthScope ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] [-Scope ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-DefaultProfile ] [-WhatIf] [-Confirm] + [] ``` ### ServicePrincipalCertificateWithSubscriptionId @@ -41,8 +44,8 @@ Connect-AzAccount [-Environment ] -Credential [-Tenant ] -CertificateThumbprint -ApplicationId [-ServicePrincipal] -Tenant [-Subscription ] [-AuthScope ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] [-SendCertificateChain] - [-Scope ] [-DefaultProfile ] [-WhatIf] [-Confirm] - [] + [-Scope ] [-DefaultProfile ] + [-WhatIf] [-Confirm] [] ``` ### ClientAssertionParameterSet @@ -50,7 +53,8 @@ Connect-AzAccount [-Environment ] -CertificateThumbprint -Appli Connect-AzAccount [-Environment ] -ApplicationId [-ServicePrincipal] -Tenant [-Subscription ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] -FederatedToken [-Scope ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-DefaultProfile ] [-WhatIf] [-Confirm] + [] ``` ### ServicePrincipalCertificateFileWithSubscriptionId @@ -58,8 +62,8 @@ Connect-AzAccount [-Environment ] -ApplicationId [-ServicePrinc Connect-AzAccount [-Environment ] -ApplicationId [-ServicePrincipal] -Tenant [-Subscription ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] [-SendCertificateChain] -CertificatePath [-CertificatePassword ] - [-Scope ] [-DefaultProfile ] [-WhatIf] [-Confirm] - [] + [-Scope ] [-DefaultProfile ] + [-WhatIf] [-Confirm] [] ``` ### AccessTokenWithSubscriptionId @@ -68,7 +72,8 @@ Connect-AzAccount [-Environment ] [-Tenant ] -AccessToken ] [-KeyVaultAccessToken ] -AccountId [-Subscription ] [-ContextName ] [-SkipValidation] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] [-Scope ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-DefaultProfile ] [-WhatIf] [-Confirm] + [] ``` ### ManagedServiceLogin @@ -76,7 +81,8 @@ Connect-AzAccount [-Environment ] [-Tenant ] -AccessToken ] [-Tenant ] [-AccountId ] [-Identity] [-Subscription ] [-AuthScope ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] [-Force] [-Scope ] - [-DefaultProfile ] [-WhatIf] [-Confirm] [] + [-DefaultProfile ] [-WhatIf] [-Confirm] + [] ``` ## DESCRIPTION @@ -292,6 +298,21 @@ Account SubscriptionName TenantId Env xxxxxxxx-xxxx-xxxx-xxxxxxxx Subscription1 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy AzureCloud ``` +### Example 11: Connect with claims challenge + +This example demonstrates how to connect using a claims challenge token. +This is useful when you receive a claims challenge during authentication, typically when additional authentication factors are required due to conditional access policies. + +```powershell +Connect-AzAccount -Tenant yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy -Subscription zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzz -ClaimsChallenge eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlcyI6WyJwMSJdfX19 +``` + +```Output +Account SubscriptionName TenantId Environment +------- ---------------- -------- ----------- +xxxxxxxx-xxxx-xxxx-xxxxxxxx Subscription1 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy AzureCloud +``` + ## PARAMETERS ### -AccessToken @@ -421,6 +442,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -ClaimsChallenge +Specifies the claims challenge with base64 encoding. + +```yaml +Type: System.String +Parameter Sets: UserWithSubscriptionId +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ContextName Name of the default Azure context for this login. For more information about Azure contexts, see diff --git a/src/Accounts/Authentication/Authentication/Parameters/InteractiveParameters.cs b/src/Accounts/Authentication/Authentication/Parameters/InteractiveParameters.cs index 71091a0a00ad..00d8fbdd9e6c 100644 --- a/src/Accounts/Authentication/Authentication/Parameters/InteractiveParameters.cs +++ b/src/Accounts/Authentication/Authentication/Parameters/InteractiveParameters.cs @@ -21,6 +21,8 @@ public class InteractiveParameters : DeviceCodeParameters { public Action PromptAction { get; set; } + public string ClaimsChallenge { get; set; } + public InteractiveParameters( PowerShellTokenCacheProvider tokenCacheProvider, IAzureEnvironment environment, @@ -29,9 +31,11 @@ public InteractiveParameters( string resourceId, string userId, string homeAccountId, - Action promptAction) : base(tokenCacheProvider, environment, tokenCache, tenantId, resourceId, userId, homeAccountId) + Action promptAction, + string claimsChallenge) : base(tokenCacheProvider, environment, tokenCache, tenantId, resourceId, userId, homeAccountId) { PromptAction = promptAction; + ClaimsChallenge = claimsChallenge; } } } diff --git a/src/Accounts/Authentication/Authentication/Parameters/InteractiveWamParameters.cs b/src/Accounts/Authentication/Authentication/Parameters/InteractiveWamParameters.cs index d7f99b85d69a..b9a428d24f2d 100644 --- a/src/Accounts/Authentication/Authentication/Parameters/InteractiveWamParameters.cs +++ b/src/Accounts/Authentication/Authentication/Parameters/InteractiveWamParameters.cs @@ -21,6 +21,8 @@ public class InteractiveWamParameters : DeviceCodeParameters { public Action PromptAction { get; set; } + public string ClaimsChallenge { get; set; } + public InteractiveWamParameters( PowerShellTokenCacheProvider tokenCacheProvider, IAzureEnvironment environment, @@ -29,9 +31,11 @@ public InteractiveWamParameters( string resourceId, string userId, string homeAccountId, - Action promptAction) : base(tokenCacheProvider, environment, tokenCache, tenantId, resourceId, userId, homeAccountId) + Action promptAction, + string claimsChallenge) : base(tokenCacheProvider, environment, tokenCache, tenantId, resourceId, userId, homeAccountId) { PromptAction = promptAction; + ClaimsChallenge = claimsChallenge; } } } diff --git a/src/Accounts/Authentication/Factories/AuthenticationFactory.cs b/src/Accounts/Authentication/Factories/AuthenticationFactory.cs index 6dd2996294f4..c6fdb5f90992 100644 --- a/src/Accounts/Authentication/Factories/AuthenticationFactory.cs +++ b/src/Accounts/Authentication/Factories/AuthenticationFactory.cs @@ -13,7 +13,6 @@ // ---------------------------------------------------------------------------------- using Hyak.Common; - using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces; using Microsoft.Azure.Commands.Common.Authentication.Authentication; @@ -24,7 +23,6 @@ using Microsoft.Identity.Client; using Microsoft.Rest; using Microsoft.WindowsAzure.Commands.Common; - using System; using System.Collections.Generic; using System.Linq; @@ -41,6 +39,11 @@ public class AuthenticationFactory : IAuthenticationFactory DefaultMSILoginUri = "http://169.254.169.254/metadata/identity/oauth2/token", DefaultBackupMSILoginUri = "http://localhost:50342/oauth2/token"; + public const string TokenCacheParameterName = "tokenCache"; + public const string ResourceIdParameterName = "resourceId"; + public const string CmdletContextParameterName = "cmdletContext"; + public const string ClaimsChallengeParameterName = "claimsChallenge"; + public AuthenticationFactory() { _getKeyStore = () => @@ -90,17 +93,47 @@ public AzKeyStore KeyStore public ITokenProvider TokenProvider { get; set; } /// - /// + /// Authenticates an Azure account, service principal, or managed identity against Entra ID and obtains an access token. /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// The Azure account to authenticate. + /// The Azure environment to authenticate against. + /// The tenant ID or name to authenticate with. + /// The password for the account, if applicable. + /// The prompt behavior to use during authentication. + /// Action to execute when a prompt is required. + /// The resource identifier to authenticate for. + /// An access token for the authenticated principal. + public IAccessToken Authenticate( + IAzureAccount account, + IAzureEnvironment environment, + string tenant, + SecureString password, + string promptBehavior, + Action promptAction, + string resourceId = AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId) + { + return Authenticate( + account, + environment, + tenant, password, + promptBehavior, + promptAction, + null, + resourceId); + } + + /// + /// Authenticates an Azure account, service principal, or managed identity against Entra ID and obtains an access token. + /// + /// The Azure account to authenticate. + /// The Azure environment to authenticate against. + /// The tenant ID or name to authenticate with. + /// The password for the account, if applicable. + /// The prompt behavior to use during authentication. + /// Action to execute when a prompt is required. + /// The token cache to use for caching authentication results. + /// The resource identifier to authenticate for. + /// An access token for the authenticated principal. public IAccessToken Authenticate( IAzureAccount account, IAzureEnvironment environment, @@ -119,21 +152,25 @@ public IAccessToken Authenticate( return Authenticate(account, environment, tenant, password, promptBehavior, promptAction, optionalParameters); } - public const string TokenCacheParameterName = "tokenCache"; - public const string ResourceIdParameterName = "resourceId"; - public const string CmdletContextParameterName = "cmdletContext"; - /// - /// + /// Authenticates an Azure account, service principal, or managed identity against Entra ID and obtains an access token. + /// This method supports claims challenge for conditional access scenarios and various authentication flows based on account type. /// - /// - /// - /// - /// - /// - /// - /// - /// + /// The Azure account to authenticate. + /// The Azure environment to authenticate against. + /// The tenant ID or name to authenticate with. + /// The password for the account, if applicable. + /// The prompt behavior to use during authentication. + /// Action to execute when a prompt is required. + /// Dictionary of optional parameters that may include: + /// + /// tokenCache (IAzureTokenCache): The token cache to use for caching authentication results. + /// resourceId (string): The resource identifier to authenticate for. + /// claimsChallenge (string): Claims challenge token from a previous authentication attempt requiring additional claims. + /// cmdletContext (ICmdletContext): Context for the cmdlet executing the authentication. + /// + /// + /// An access token for the authenticated principal. public IAccessToken Authenticate( IAzureAccount account, IAzureEnvironment environment, @@ -144,19 +181,28 @@ public IAccessToken Authenticate( IDictionary optionalParameters) { var resourceId = AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId; + string claimsChallenge = null; IAzureTokenCache tokenCache = null; ICmdletContext cmdletContext = null; AuthenticationTelemetry authenticationTelemetry = null; + if (optionalParameters != null) { if (optionalParameters.ContainsKey(ResourceIdParameterName)) { resourceId = optionalParameters[ResourceIdParameterName] as string; } + + if (optionalParameters.ContainsKey(ClaimsChallengeParameterName)) + { + claimsChallenge = optionalParameters[ClaimsChallengeParameterName] as string; + } + if (optionalParameters.ContainsKey(TokenCacheParameterName)) { tokenCache = optionalParameters[TokenCacheParameterName] as IAzureTokenCache; } + if (AzureSession.Instance.TryGetComponent(AuthenticationTelemetry.Name, out authenticationTelemetry)) { if (optionalParameters.ContainsKey(CmdletContextParameterName)) @@ -175,7 +221,7 @@ public IAccessToken Authenticate( Task authToken; var processAuthenticator = Builder.Authenticator; var retries = 5; - var authParamters = GetAuthenticationParameters(tokenCacheProvider, account, environment, tenant, password, promptBehavior, promptAction, tokenCache, resourceId); + var authParamters = GetAuthenticationParameters(tokenCacheProvider, account, environment, tenant, password, promptBehavior, promptAction, claimsChallenge, tokenCache, resourceId); IAccessToken token = null; while (retries-- > 0) @@ -283,26 +329,7 @@ private static AzPSAuthenticationFailedException AnalyzeMsalException(Exception private static bool NeedTenantArmPermission(IAzureEnvironment environment, string tenantId, string resourceId) { return !string.IsNullOrEmpty(tenantId) && !string.IsNullOrEmpty(resourceId) && - string.Equals(environment.GetEndpoint(resourceId), environment.GetEndpoint(AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId)); - } - - public IAccessToken Authenticate( - IAzureAccount account, - IAzureEnvironment environment, - string tenant, - SecureString password, - string promptBehavior, - Action promptAction, - string resourceId = AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId) - { - return Authenticate( - account, - environment, - tenant, password, - promptBehavior, - promptAction, - null, - resourceId); + string.Equals(environment.GetEndpoint(resourceId), environment.GetEndpoint(AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId)); } public SubscriptionCloudCredentials GetSubscriptionCloudCredentials(IAzureContext context) @@ -503,10 +530,8 @@ public void RemoveUser(IAzureAccount account, IAzureEnvironment environment) case AzureAccount.AccountType.ServicePrincipal: try { - KeyStore.RemoveSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, - account.Id, account.GetTenants().FirstOrDefault())); - KeyStore.RemoveSecureString(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, - account.Id, account.GetTenants().FirstOrDefault())); + KeyStore.RemoveSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, account.GetTenants().FirstOrDefault())); + KeyStore.RemoveSecureString(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, account.Id, account.GetTenants().FirstOrDefault())); } catch { @@ -567,13 +592,11 @@ private void RemoveFromTokenCache(IAzureAccount account) } var publicClient = tokenCacheProvider.CreatePublicClient(); - var accounts = publicClient.GetAccountsAsync() - .ConfigureAwait(false).GetAwaiter().GetResult(); + var accounts = publicClient.GetAccountsAsync().ConfigureAwait(false).GetAwaiter().GetResult(); var tokenAccounts = accounts.Where(a => MatchCacheItem(account, a)); foreach (var tokenAccount in tokenAccounts) - { - publicClient.RemoveAsync(tokenAccount) - .ConfigureAwait(false).GetAwaiter().GetResult(); + { + publicClient.RemoveAsync(tokenAccount).ConfigureAwait(false).GetAwaiter().GetResult(); } } @@ -599,6 +622,20 @@ private bool MatchCacheItem(IAzureAccount account, IAccount tokenAccount) return result; } + /// + /// Creates the appropriate authentication parameters based on the account type and other inputs. + /// + /// Provider for token caching. + /// The Azure account to authenticate. + /// The Azure environment to authenticate against. + /// The tenant ID or name to authenticate with. + /// The password for the account, if applicable. + /// The prompt behavior to use during authentication. + /// Action to execute when a prompt is required. + /// Claims challenge token from a previous authentication attempt. + /// The token cache to use for caching authentication results. + /// The resource identifier to authenticate for. + /// Authentication parameters appropriate for the account type. private AuthenticationParameters GetAuthenticationParameters( PowerShellTokenCacheProvider tokenCacheProvider, IAzureAccount account, @@ -607,6 +644,7 @@ private AuthenticationParameters GetAuthenticationParameters( SecureString password, string promptBehavior, Action promptAction, + string claimsChallenge, IAzureTokenCache tokenCache, string resourceId = AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId) { @@ -617,7 +655,7 @@ private AuthenticationParameters GetAuthenticationParameters( { var homeAccountId = account.GetProperty(AzureAccount.Property.HomeAccountId) ?? ""; - if (!string.IsNullOrEmpty(account.Id)) + if (!string.IsNullOrEmpty(account.Id) && string.IsNullOrEmpty(claimsChallenge)) { return GetSilentParameters(tokenCacheProvider, account, environment, tenant, tokenCache, resourceId, homeAccountId); } @@ -630,7 +668,7 @@ private AuthenticationParameters GetAuthenticationParameters( { return new UsernamePasswordParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.Id, password, homeAccountId); } - return GetInteractiveParameters(tokenCacheProvider, account, environment, tenant, promptAction, tokenCache, resourceId, homeAccountId); + return GetInteractiveParameters(tokenCacheProvider, account, environment, tenant, promptAction, claimsChallenge, tokenCache, resourceId, homeAccountId); } return new UsernamePasswordParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.Id, password, null); @@ -647,8 +685,7 @@ private AuthenticationParameters GetAuthenticationParameters( { try { - password = KeyStore.GetSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret -, account.Id, tenant)); + password = KeyStore.GetSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, tenant)); } catch { @@ -683,11 +720,11 @@ private AuthenticationParameters GetAuthenticationParameters( } } - private static AuthenticationParameters GetInteractiveParameters(PowerShellTokenCacheProvider tokenCacheProvider, IAzureAccount account, IAzureEnvironment environment, string tenant, Action promptAction, IAzureTokenCache tokenCache, string resourceId, string homeAccountId) + private static AuthenticationParameters GetInteractiveParameters(PowerShellTokenCacheProvider tokenCacheProvider, IAzureAccount account, IAzureEnvironment environment, string tenant, Action promptAction, string claimsChallenge, IAzureTokenCache tokenCache, string resourceId, string homeAccountId) { return AzConfigReader.IsWamEnabled(environment.ActiveDirectoryAuthority) - ? new InteractiveWamParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.GetProperty("LoginHint"), homeAccountId, promptAction) as AuthenticationParameters - : new InteractiveParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.GetProperty("LoginHint"), homeAccountId, promptAction); + ? new InteractiveWamParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.GetProperty("LoginHint"), homeAccountId, promptAction, claimsChallenge) as AuthenticationParameters + : new InteractiveParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.GetProperty("LoginHint"), homeAccountId, promptAction, claimsChallenge); } private static AuthenticationParameters GetSilentParameters(PowerShellTokenCacheProvider tokenCacheProvider, IAzureAccount account, IAzureEnvironment environment, string tenant, IAzureTokenCache tokenCache, string resourceId, string homeAccountId) diff --git a/src/Accounts/Authentication/Utilities/ClaimsChallengeUtilities.cs b/src/Accounts/Authentication/Utilities/ClaimsChallengeUtilities.cs index 3f0755d71477..983e0ed8d330 100644 --- a/src/Accounts/Authentication/Utilities/ClaimsChallengeUtilities.cs +++ b/src/Accounts/Authentication/Utilities/ClaimsChallengeUtilities.cs @@ -13,7 +13,6 @@ // ---------------------------------------------------------------------------------- using Microsoft.Azure.Commands.Common.Authentication.Properties; -using Microsoft.WindowsAzure.Commands.Common; using Microsoft.WindowsAzure.Commands.Utilities.Common; using System; using System.Collections.Generic; @@ -119,5 +118,34 @@ private static string TryGetErrorMessageFromOriginalResponse(string content) return content; } } + + /// + /// Attempts to decode a base64 encoded claims challenge string. + /// + /// The base64 encoded claims challenge string. + /// When this method returns, contains the decoded JSON claims challenge string if decoding succeeded; otherwise, null. + /// + /// true if the base64 string was successfully decoded into a valid claims challenge; otherwise, false. + /// + public static bool TryParseClaimsChallenge(string base64Input, out string claimsChallenge) + { + claimsChallenge = null; + + if (string.IsNullOrWhiteSpace(base64Input)) + return false; + + try + { + byte[] data = Convert.FromBase64String(base64Input); + claimsChallenge = Encoding.UTF8.GetString(data); + + return true; + } + catch + { + claimsChallenge = null; + return false; + } + } } } diff --git a/src/Accounts/Authenticators/InteractiveUserAuthenticator.cs b/src/Accounts/Authenticators/InteractiveUserAuthenticator.cs index fd29c3c7ba9e..cc5ba463b6ad 100644 --- a/src/Accounts/Authenticators/InteractiveUserAuthenticator.cs +++ b/src/Accounts/Authenticators/InteractiveUserAuthenticator.cs @@ -14,12 +14,9 @@ using Azure.Core; using Azure.Identity; - using Hyak.Common; - using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; - using System; using System.Net; using System.Net.Sockets; @@ -52,8 +49,9 @@ public override Task Authenticate(AuthenticationParameters paramet var resource = interactiveParameters.Environment.GetEndpoint(interactiveParameters.ResourceId) ?? interactiveParameters.ResourceId; var scopes = AuthenticationHelpers.GetScope(onPremise, resource); var clientId = Constants.PowerShellClientId; + var claimsChallenge = interactiveParameters.ClaimsChallenge; - var requestContext = new TokenRequestContext(scopes, isCaeEnabled: true); + var requestContext = new TokenRequestContext(scopes, claims: claimsChallenge, isCaeEnabled: true); var authority = interactiveParameters.Environment.ActiveDirectoryAuthority; var options = new InteractiveBrowserCredentialOptions() @@ -63,7 +61,7 @@ public override Task Authenticate(AuthenticationParameters paramet TokenCachePersistenceOptions = tokenCacheProvider.GetTokenCachePersistenceOptions(), AuthorityHost = new Uri(authority), RedirectUri = GetReplyUrl(onPremise, interactiveParameters.PromptAction), - LoginHint = interactiveParameters.UserId, + LoginHint = interactiveParameters.UserId }; options.DisableInstanceDiscovery = interactiveParameters.DisableInstanceDiscovery ?? options.DisableInstanceDiscovery; var browserCredential = new InteractiveBrowserCredential(options); diff --git a/src/Accounts/Authenticators/InteractiveWamAuthenticator.cs b/src/Accounts/Authenticators/InteractiveWamAuthenticator.cs index ca3f10a71c8c..f6761990c5a6 100644 --- a/src/Accounts/Authenticators/InteractiveWamAuthenticator.cs +++ b/src/Accounts/Authenticators/InteractiveWamAuthenticator.cs @@ -43,8 +43,9 @@ public override Task Authenticate(AuthenticationParameters paramet var resource = interactiveParameters.Environment.GetEndpoint(interactiveParameters.ResourceId) ?? interactiveParameters.ResourceId; var scopes = AuthenticationHelpers.GetScope(onPremise, resource); var clientId = Constants.PowerShellClientId; + var claimsChallenge = interactiveParameters.ClaimsChallenge; - var requestContext = new TokenRequestContext(scopes, isCaeEnabled: true); + var requestContext = new TokenRequestContext(scopes, claims: claimsChallenge, isCaeEnabled: true); var authority = interactiveParameters.Environment.ActiveDirectoryAuthority; var options = new InteractiveBrowserCredentialBrokerOptions(WindowHandleUtilities.GetConsoleOrTerminalWindow())