diff --git a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs index 6e3bccc77df6..ebc38f70aba8 100644 --- a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs +++ b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Concurrent; +using System.Linq; using System.Management.Automation; using System.Security; using System.Threading; @@ -52,6 +53,7 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod public const string UserWithCredentialParameterSet = "UserWithCredential"; public const string ServicePrincipalParameterSet = "ServicePrincipalWithSubscriptionId"; public const string ServicePrincipalCertificateParameterSet= "ServicePrincipalCertificateWithSubscriptionId"; + public const string ServicePrincipalCertificateFileParameterSet = "ServicePrincipalCertificateFileWithSubscriptionId"; public const string AccessTokenParameterSet = "AccessTokenWithSubscriptionId"; public const string ManagedServiceParameterSet = "ManagedServiceLogin"; public const string MSIEndpointVariable = "MSI_ENDPOINT"; @@ -79,12 +81,16 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = true, HelpMessage = "SPN")] + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, + Mandatory = true, HelpMessage = "SPN")] public string ApplicationId { get; set; } [Parameter(ParameterSetName = ServicePrincipalParameterSet, Mandatory = true)] [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = false)] + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, + Mandatory = false)] public SwitchParameter ServicePrincipal { get; set; } [Parameter(ParameterSetName = UserParameterSet, @@ -97,6 +103,8 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod Mandatory = false, HelpMessage = "Tenant name or ID")] [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = true, HelpMessage = "Tenant name or ID")] + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, + Mandatory = true, HelpMessage = "Tenant name or ID")] [Parameter(ParameterSetName = ManagedServiceParameterSet, Mandatory = false, HelpMessage = "Optional tenant name or ID")] [Alias("Domain", "TenantId")] @@ -138,6 +146,8 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod Mandatory = false, HelpMessage = "Subscription Name or ID", ValueFromPipeline = true)] [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = false, HelpMessage = "Subscription Name or ID", ValueFromPipeline = true)] + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, + Mandatory = false, HelpMessage = "Subscription Name or ID", ValueFromPipeline = true)] [Parameter(ParameterSetName = AccessTokenParameterSet, Mandatory = false, HelpMessage = "Subscription Name or ID", ValueFromPipeline = true)] [Parameter(ParameterSetName = ManagedServiceParameterSet, @@ -187,6 +197,7 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod [Parameter(ParameterSetName = UserWithCredentialParameterSet, Mandatory = false, HelpMessage = "Max subscription number to populate contexts after login. Default is " + DefaultMaxContextPopulationString + ". To populate all subscriptions to contexts, set to -1.")] [Parameter(ParameterSetName = ServicePrincipalParameterSet, Mandatory = false, HelpMessage = "Max subscription number to populate contexts after login. Default is " + DefaultMaxContextPopulationString + ". To populate all subscriptions to contexts, set to -1.")] [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = false, HelpMessage = "Max subscription number to populate contexts after login. Default is " + DefaultMaxContextPopulationString + ". To populate all subscriptions to contexts, set to -1.")] + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, Mandatory = false, HelpMessage = "Max subscription number to populate contexts after login. Default is " + DefaultMaxContextPopulationString + ". To populate all subscriptions to contexts, set to -1.")] [Parameter(ParameterSetName = AccessTokenParameterSet, Mandatory = false, HelpMessage = "Max subscription number to populate contexts after login. Default is " + DefaultMaxContextPopulationString + ". To populate all subscriptions to contexts, set to -1.")] [Parameter(ParameterSetName = ManagedServiceParameterSet, Mandatory = false, HelpMessage = "Max subscription number to populate contexts after login. Default is " + DefaultMaxContextPopulationString + ". To populate all subscriptions to contexts, set to -1.")] [PSDefaultValue(Help = DefaultMaxContextPopulationString, Value = DefaultMaxContextPopulation)] @@ -201,9 +212,17 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod [Parameter(Mandatory = false, HelpMessage = "Overwrite the existing context with the same name, if any.")] public SwitchParameter Force { get; set; } - [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, Mandatory = false, HelpMessage = "Specifies if the x5c claim (public key of the certificate) should be sent to the STS to achieve easy certificate rollover in Azure AD.")] + [Parameter(ParameterSetName = ServicePrincipalCertificateParameterSet, HelpMessage = "Specifies if the x5c claim (public key of the certificate) should be sent to the STS to achieve easy certificate rollover in Azure AD.")] + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, HelpMessage = "Specifies if the x5c claim (public key of the certificate) should be sent to the STS to achieve easy certificate rollover in Azure AD.")] public SwitchParameter SendCertificateChain { get; set; } + + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, Mandatory = true, HelpMessage = "The path of certficate file in pkcs#12 format.")] + public String CertificatePath { get; set; } + + [Parameter(ParameterSetName = ServicePrincipalCertificateFileParameterSet, HelpMessage = "The password required to access the pkcs#12 certificate file.")] + public SecureString CertificatePassword { get; set; } + protected override IAzureContext DefaultContext { get @@ -305,6 +324,7 @@ public override void ExecuteCmdlet() azureAccount.SetProperty(AzureAccount.Property.KeyVaultAccessToken, KeyVaultAccessToken); break; case ServicePrincipalCertificateParameterSet: + case ServicePrincipalCertificateFileParameterSet: case ServicePrincipalParameterSet: azureAccount.Type = AzureAccount.AccountType.ServicePrincipal; break; @@ -345,7 +365,23 @@ public override void ExecuteCmdlet() azureAccount.SetThumbprint(CertificateThumbprint); } - if (ParameterSetName == ServicePrincipalCertificateParameterSet && SendCertificateChain) + if( !string.IsNullOrWhiteSpace(CertificatePath)) + { + var resolvedPath = this.SessionState.Path.GetResolvedPSPathFromPSPath(CertificatePath).FirstOrDefault()?.Path; + if (string.IsNullOrEmpty(resolvedPath)) + { + var parametersLog = $"- Invalid certificate path :'{CertificatePath}'."; + throw new InvalidOperationException(parametersLog); + } + azureAccount.SetProperty(AzureAccount.Property.CertificatePath, resolvedPath); + if (CertificatePassword != null) + { + azureAccount.SetProperty(AzureAccount.Property.CertificatePassword, CertificatePassword.ConvertToString()); + } + } + + if ((ParameterSetName == ServicePrincipalCertificateParameterSet || ParameterSetName == ServicePrincipalCertificateFileParameterSet) + && SendCertificateChain) { azureAccount.SetProperty(AzureAccount.Property.SendCertificateChain, SendCertificateChain.ToString()); bool supressWarningOrError = false; @@ -368,7 +404,7 @@ public override void ExecuteCmdlet() azureAccount.SetProperty(AzureAccount.Property.Tenants, Tenant); } - if (azureAccount.Type == AzureAccount.AccountType.ServicePrincipal && string.IsNullOrEmpty(CertificateThumbprint)) + if (azureAccount.Type == AzureAccount.AccountType.ServicePrincipal && password != null) { azureAccount.SetProperty(AzureAccount.Property.ServicePrincipalSecret, password.ConvertToString()); if (GetContextModificationScope() == ContextModificationScope.CurrentUser) diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index f793c7174f15..a0168756551b 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -19,11 +19,12 @@ --> ## Upcoming Release +* Supported certificate file as input parameter of Connect-AzAccount ## Version 2.3.0 * Upgraded Azure.Identity to 1.4 and MSAL to 4.30.1 * Removed obsolete parameters `ManagedServiceHostName`, `ManagedServicePort` and `ManagedServiceSecret` of cmdlet `Connect-AzAccount`, environment variables `MSI_ENDPOINT` and `MSI_SECRET` could be used instead -* Customize display format of PSAzureRmAccount to hide secret of service principal [#14208] +* Customized display format of PSAzureRmAccount to hide secret of service principal [#14208] * Added optional parameter `AuthScope` to `Connect-AzAccount` to support enhanced authentication of data plane features * Set retry times by environment variable [#14748] * Supported subject name issuer authentication diff --git a/src/Accounts/Accounts/help/Connect-AzAccount.md b/src/Accounts/Accounts/help/Connect-AzAccount.md index a3df41da7422..60110311b00f 100644 --- a/src/Accounts/Accounts/help/Connect-AzAccount.md +++ b/src/Accounts/Accounts/help/Connect-AzAccount.md @@ -45,6 +45,15 @@ Connect-AzAccount [-Environment ] -CertificateThumbprint -Appli [] ``` +### ServicePrincipalCertificateFileWithSubscriptionId +``` +Connect-AzAccount [-Environment ] -ApplicationId [-ServicePrincipal] -Tenant + [-Subscription ] [-ContextName ] [-SkipContextPopulation] [-MaxContextPopulation ] + [-Force] [-SendCertificateChain] -CertificatePath [-CertificatePassword ] + [-Scope ] [-DefaultProfile ] [-WhatIf] [-Confirm] + [] +``` + ### AccessTokenWithSubscriptionId ``` Connect-AzAccount [-Environment ] [-Tenant ] -AccessToken [-GraphAccessToken ] @@ -184,21 +193,21 @@ more information on creating a self-signed certificates and assigning them permi [Use Azure PowerShell to create a service principal with a certificate](/azure/active-directory/develop/howto-authenticate-service-principal-powershell) ```powershell -$Thumbprint = '0SZTNJ34TCCMUJ5MJZGR8XQD3S0RVHJBA33Z8ZXV' -$TenantId = '4cd76576-b611-43d0-8f2b-adcb139531bf' -$ApplicationId = '3794a65a-e4e4-493d-ac1d-f04308d712dd' +$Thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' +$TenantId = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy' +$ApplicationId = '00000000-0000-0000-0000-00000000' Connect-AzAccount -CertificateThumbprint $Thumbprint -ApplicationId $ApplicationId -Tenant $TenantId -ServicePrincipal ``` ```Output -Account SubscriptionName TenantId Environment -------- ---------------- -------- ----------- -xxxx-xxxx-xxxx-xxxx Subscription1 xxxx-xxxx-xxxx-xxxx AzureCloud +Account SubscriptionName TenantId Environment +------- ---------------- -------- ----------- +xxxxxxxx-xxxx-xxxx-xxxxxxxxx Subscription1 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy AzureCloud -Account : 3794a65a-e4e4-493d-ac1d-f04308d712dd +Account : xxxxxxxx-xxxx-xxxx-xxxxxxxx SubscriptionName : MyTestSubscription -SubscriptionId : 85f0f653-1f86-4d2c-a9f1-042efc00085c -TenantId : 4cd76576-b611-43d0-8f2b-adcb139531bf +SubscriptionId : zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzz +TenantId : yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy Environment : AzureCloud ``` @@ -216,6 +225,24 @@ Account SubscriptionName TenantId Environment yyyy-yyyy-yyyy-yyyy Subscription1 xxxx-xxxx-xxxx-xxxx AzureCloud ``` +### Example 9: Connect using certificate file + +This example connects to an Azure account using certificate-based service principal authentication. +The certificate file, which is specified by `CertficatePath`, should contains both certificate and private key as the input. + +```powershell +$securePassword = $plainPassword | ConvertTo-SecureString -AsPlainText -Force +$TenantId = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy' +$ApplicationId = 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzz' +Connect-AzAccount -ServicePrincipal -ApplicationId $ApplicationId -TenantId $TenantId -CertificatePath './certificatefortest.pfx' -CertificatePassword $securePassword +``` + +```Output +Account SubscriptionName TenantId Environment +------- ---------------- -------- ----------- +xxxxxxxx-xxxx-xxxx-xxxxxxxx Subscription1 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyy AzureCloud +``` + ## PARAMETERS ### -AccessToken @@ -275,7 +302,7 @@ Application ID of the service principal. ```yaml Type: System.String -Parameter Sets: ServicePrincipalCertificateWithSubscriptionId +Parameter Sets: ServicePrincipalCertificateWithSubscriptionId, ServicePrincipalCertificateFileWithSubscriptionId Aliases: Required: True @@ -300,6 +327,36 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -CertificatePassword +The password required to access the pkcs#12 certificate file. + +```yaml +Type: System.Security.SecureString +Parameter Sets: ServicePrincipalCertificateFileWithSubscriptionId +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -CertificatePath +The path of certficate file in pkcs#12 format. + +```yaml +Type: System.String +Parameter Sets: ServicePrincipalCertificateFileWithSubscriptionId +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -CertificateThumbprint Certificate Hash or Thumbprint. @@ -486,7 +543,7 @@ Specifies if the x5c claim (public key of the certificate) should be sent to the ```yaml Type: System.Management.Automation.SwitchParameter -Parameter Sets: ServicePrincipalCertificateWithSubscriptionId +Parameter Sets: ServicePrincipalCertificateWithSubscriptionId, ServicePrincipalCertificateFileWithSubscriptionId Aliases: Required: False @@ -514,7 +571,7 @@ Accept wildcard characters: False ```yaml Type: System.Management.Automation.SwitchParameter -Parameter Sets: ServicePrincipalCertificateWithSubscriptionId +Parameter Sets: ServicePrincipalCertificateWithSubscriptionId, ServicePrincipalCertificateFileWithSubscriptionId Aliases: Required: False @@ -594,7 +651,7 @@ Accept wildcard characters: False ```yaml Type: System.String -Parameter Sets: ServicePrincipalWithSubscriptionId, ServicePrincipalCertificateWithSubscriptionId +Parameter Sets: ServicePrincipalWithSubscriptionId, ServicePrincipalCertificateWithSubscriptionId, ServicePrincipalCertificateFileWithSubscriptionId Aliases: Domain, TenantId Required: True diff --git a/src/Accounts/Authentication.Test/AuthenticatorsTest/ServicePrincipalAuthenticatorTests.cs b/src/Accounts/Authentication.Test/AuthenticatorsTest/ServicePrincipalAuthenticatorTests.cs new file mode 100644 index 000000000000..ef39b6bf0cd2 --- /dev/null +++ b/src/Accounts/Authentication.Test/AuthenticatorsTest/ServicePrincipalAuthenticatorTests.cs @@ -0,0 +1,266 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Azure.Core; +using Azure.Identity; +using Microsoft.Azure.Commands.Common.Authentication; +using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.PowerShell.Authenticators; +using Microsoft.Azure.PowerShell.Authenticators.Factories; +using Microsoft.Azure.Test.HttpRecorder; +using Microsoft.WindowsAzure.Commands.Common.Test.Mocks; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using Moq; +using Xunit; +using Xunit.Abstractions; +using System; +using System.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + + + +namespace Common.Authenticators.Test +{ + public class ServicePrincipalAuthenticatorTests + { + private const string TestTenantId = "123"; + private const string TestResourceId = "ActiveDirectoryServiceEndpointResourceId"; + + private const string fakeToken = "faketoken"; + + private ITestOutputHelper Output { get; set; } + + class TokenCredentialMock : TokenCredential + { + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(new AccessToken(fakeToken, DateTimeOffset.Now)); + } + } + + public ServicePrincipalAuthenticatorTests(ITestOutputHelper output) + { + AzureSessionInitializer.InitializeAzureSession(); + Output = output; + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public async Task ServicePrincipalSecretAuthenticationTest() + { + var accountId = "testuser"; + var securePassword = new SecureString(); + "pa88w0rd!".ToCharArray().ForEach(c => securePassword.AppendChar(c)); + + //Setup + var mockAzureCredentialFactory = new Mock(); + mockAzureCredentialFactory.Setup(f => f.CreateClientSecretCredential( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(() => new TokenCredentialMock()); + + AzureSession.Instance.RegisterComponent(nameof(AzureCredentialFactory), () => mockAzureCredentialFactory.Object, true); + InMemoryTokenCacheProvider cacheProvider = new InMemoryTokenCacheProvider(); + + var account = new AzureAccount + { + Id = accountId, + Type = AzureAccount.AccountType.User, + }; + account.SetTenants(TestTenantId); + + var parameter = new ServicePrincipalParameters( + cacheProvider, + AzureEnvironment.PublicEnvironments["AzureCloud"], + null, + TestTenantId, + TestResourceId, + account.Id, + null, + null, + null, + securePassword, + null); + + //Run + var authenticator = new ServicePrincipalAuthenticator(); + var token = await authenticator.Authenticate(parameter); + + //Verify + mockAzureCredentialFactory.Verify(f => f.CreateClientSecretCredential(TestTenantId, accountId, securePassword, It.IsAny()), Times.Once()); + Assert.Equal(fakeToken, token.AccessToken); + Assert.Equal(TestTenantId, token.TenantId); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public async Task ServicePrincipalThumbprintAuthenticationTest() + { + var accountId = "testuser"; + var thumbprint = Guid.NewGuid().ToString(); + + IDataStore prevDataStore = AzureSession.Instance.DataStore; + AzureSession.Instance.DataStore = new MockDataStore(); + var certificate = AzureSession.Instance.DataStore.GetCertificate(thumbprint); + + //Setup + var mockAzureCredentialFactory = new Mock(); + mockAzureCredentialFactory.Setup(f => f.CreateClientCertificateCredential( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(() => new TokenCredentialMock()); + + AzureSession.Instance.RegisterComponent(nameof(AzureCredentialFactory), () => mockAzureCredentialFactory.Object, true); + InMemoryTokenCacheProvider cacheProvider = new InMemoryTokenCacheProvider(); + + var account = new AzureAccount + { + Id = accountId, + Type = AzureAccount.AccountType.User, + }; + account.SetTenants(TestTenantId); + + var parameter = new ServicePrincipalParameters( + cacheProvider, + AzureEnvironment.PublicEnvironments["AzureCloud"], + null, + TestTenantId, + TestResourceId, + account.Id, + thumbprint, + null, + null, + null, + null); + + //Run + var authenticator = new ServicePrincipalAuthenticator(); + var token = await authenticator.Authenticate(parameter); + + //Verify + mockAzureCredentialFactory.Verify(f => f.CreateClientCertificateCredential(TestTenantId, accountId, certificate, It.IsAny()), Times.Once()); + Assert.Equal(fakeToken, token.AccessToken); + Assert.Equal(TestTenantId, token.TenantId); + + AzureSession.Instance.DataStore = prevDataStore; + } + + [Fact] + [Trait(Category.AcceptanceType, Category.CheckIn)] + public async Task ServicePrincipalCertificateFileAuthenticationTest() + { + var accountId = "testuser"; + var certificateFile = "d:/certficatefortest.pfx"; + + IDataStore prevDataStore = AzureSession.Instance.DataStore; + AzureSession.Instance.DataStore = new MockDataStore(); + AzureSession.Instance.DataStore.WriteFile(certificateFile, "dummyfile"); + + //Setup + var mockAzureCredentialFactory = new Mock(); + mockAzureCredentialFactory.Setup(f => f.CreateClientCertificateCredential( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(() => new TokenCredentialMock()); + + AzureSession.Instance.RegisterComponent(nameof(AzureCredentialFactory), () => mockAzureCredentialFactory.Object, true); + InMemoryTokenCacheProvider cacheProvider = new InMemoryTokenCacheProvider(); + + var account = new AzureAccount + { + Id = accountId, + Type = AzureAccount.AccountType.User, + }; + account.SetTenants(TestTenantId); + + var parameter = new ServicePrincipalParameters( + cacheProvider, + AzureEnvironment.PublicEnvironments["AzureCloud"], + null, + TestTenantId, + TestResourceId, + account.Id, + null, + certificateFile, + null, + null, + null); + + //Run + var authenticator = new ServicePrincipalAuthenticator(); + var token = await authenticator.Authenticate(parameter); + + //Verify + mockAzureCredentialFactory.Verify(f => f.CreateClientCertificateCredential(TestTenantId, accountId, certificateFile, It.IsAny()), Times.Once()); + Assert.Equal(fakeToken, token.AccessToken); + Assert.Equal(TestTenantId, token.TenantId); + + AzureSession.Instance.DataStore = prevDataStore; + } + + [Fact] + [Trait(Category.RunType, Category.LiveOnly)] + public async Task ServicePrincipalCertificateFileWithSecretAuthenticationTest() + { + var accountId = "testuser"; + var certificateFile = "d:/certficatefortest.pfx"; + var thumbprint = Guid.NewGuid().ToString(); + var securePassword = new SecureString(); + "pa88w0rd!".ToCharArray().ForEach(c => securePassword.AppendChar(c)); + + IDataStore prevDataStore = AzureSession.Instance.DataStore; + AzureSession.Instance.DataStore = new DiskDataStore(); + + //Setup + var mockAzureCredentialFactory = new Mock(); + mockAzureCredentialFactory.Setup(f => f.CreateClientCertificateCredential( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(() => new TokenCredentialMock()); + + AzureSession.Instance.RegisterComponent(nameof(AzureCredentialFactory), () => mockAzureCredentialFactory.Object, true); + InMemoryTokenCacheProvider cacheProvider = new InMemoryTokenCacheProvider(); + + var account = new AzureAccount + { + Id = accountId, + Type = AzureAccount.AccountType.User, + }; + account.SetTenants(TestTenantId); + + var parameter = new ServicePrincipalParameters( + cacheProvider, + AzureEnvironment.PublicEnvironments["AzureCloud"], + null, + TestTenantId, + TestResourceId, + account.Id, + null, + certificateFile, + securePassword, + null, + null); + + //Run + var authenticator = new ServicePrincipalAuthenticator(); + var token = await authenticator.Authenticate(parameter); + + //Verify + mockAzureCredentialFactory.Verify(f => f.CreateClientCertificateCredential(TestTenantId, accountId, It.IsAny(), It.IsAny()), Times.Once()); + Assert.Equal(fakeToken, token.AccessToken); + Assert.Equal(TestTenantId, token.TenantId); + + AzureSession.Instance.DataStore = prevDataStore; + } + } +} diff --git a/src/Accounts/Authentication/Authentication/Parameters/ServicePrincipalParameters.cs b/src/Accounts/Authentication/Authentication/Parameters/ServicePrincipalParameters.cs index 5a7fa8a77438..ef019fa76c4e 100644 --- a/src/Accounts/Authentication/Authentication/Parameters/ServicePrincipalParameters.cs +++ b/src/Accounts/Authentication/Authentication/Parameters/ServicePrincipalParameters.cs @@ -28,6 +28,10 @@ public class ServicePrincipalParameters : AuthenticationParameters public bool? SendCertificateChain { get; set; } = null; + public string CertificatePath { get; set; } + + public SecureString CertificateSecret { get; set; } + public ServicePrincipalParameters( PowerShellTokenCacheProvider tokenCacheProvider, IAzureEnvironment environment, @@ -36,6 +40,8 @@ public ServicePrincipalParameters( string resourceId, string applicationId, string thumbprint, + string certificatePath, + SecureString certificateSecret, SecureString secret, bool? sendCertificateChain) : base(tokenCacheProvider, environment, tokenCache, tenantId, resourceId) { @@ -43,6 +49,8 @@ public ServicePrincipalParameters( Thumbprint = thumbprint; Secret = secret; SendCertificateChain = sendCertificateChain; + CertificatePath = certificatePath; + CertificateSecret = certificateSecret; } } } diff --git a/src/Accounts/Authentication/Factories/AuthenticationFactory.cs b/src/Accounts/Authentication/Factories/AuthenticationFactory.cs index a7bff3024f15..fbfba60fd5f7 100644 --- a/src/Accounts/Authentication/Factories/AuthenticationFactory.cs +++ b/src/Accounts/Authentication/Factories/AuthenticationFactory.cs @@ -558,7 +558,9 @@ private AuthenticationParameters GetAuthenticationParameters( sendCertificateChain = Boolean.Parse(sendCertificateChainStr); } password = password ?? ConvertToSecureString(account.GetProperty(AzureAccount.Property.ServicePrincipalSecret)); - return new ServicePrincipalParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.Id, account.GetProperty(AzureAccount.Property.CertificateThumbprint), password, sendCertificateChain); + var certificatePassword = ConvertToSecureString(account.GetProperty(AzureAccount.Property.CertificatePassword)); + return new ServicePrincipalParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account.Id, account.GetProperty(AzureAccount.Property.CertificateThumbprint), account.GetProperty(AzureAccount.Property.CertificatePath), + certificatePassword, password, sendCertificateChain); case AzureAccount.AccountType.ManagedService: return new ManagedServiceIdentityParameters(tokenCacheProvider, environment, tokenCache, tenant, resourceId, account); case AzureAccount.AccountType.AccessToken: diff --git a/src/Accounts/Authenticators/Factories/AzureCredentialFactory.cs b/src/Accounts/Authenticators/Factories/AzureCredentialFactory.cs index d4ced8b57ce2..e813f10fd7a6 100644 --- a/src/Accounts/Authenticators/Factories/AzureCredentialFactory.cs +++ b/src/Accounts/Authenticators/Factories/AzureCredentialFactory.cs @@ -14,6 +14,9 @@ using Azure.Core; using Azure.Identity; +using Microsoft.WindowsAzure.Commands.Common; +using System.Security; +using System.Security.Cryptography.X509Certificates; namespace Microsoft.Azure.PowerShell.Authenticators.Factories { @@ -23,5 +26,20 @@ public virtual TokenCredential CreateManagedIdentityCredential(string clientId) { return new ManagedIdentityCredential(clientId); } + + public virtual TokenCredential CreateClientSecretCredential(string tenantId, string clientId, SecureString secret, ClientCertificateCredentialOptions options) + { + return new ClientSecretCredential(tenantId, clientId, secret.ConvertToString(), options); + } + + public virtual TokenCredential CreateClientCertificateCredential(string tenantId, string clientId, X509Certificate2 certifiate, ClientCertificateCredentialOptions options) + { + return new ClientCertificateCredential(tenantId, clientId, certifiate, options); + } + + public virtual TokenCredential CreateClientCertificateCredential(string tenantId, string clientId, string certificatePath, ClientCertificateCredentialOptions options) + { + return new ClientCertificateCredential(tenantId, clientId, certificatePath, options); + } } } diff --git a/src/Accounts/Authenticators/ServicePrincipalAuthenticator.cs b/src/Accounts/Authenticators/ServicePrincipalAuthenticator.cs index 92d92a6ce2ef..7f198282a322 100644 --- a/src/Accounts/Authenticators/ServicePrincipalAuthenticator.cs +++ b/src/Accounts/Authenticators/ServicePrincipalAuthenticator.cs @@ -13,19 +13,16 @@ // ---------------------------------------------------------------------------------- using System; -using System.Collections.Concurrent; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Azure.Core; using Azure.Identity; - -using Hyak.Common; - using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.PowerShell.Authenticators.Factories; using Microsoft.Identity.Client; -using Microsoft.WindowsAzure.Commands.Common; namespace Microsoft.Azure.PowerShell.Authenticators { @@ -46,6 +43,7 @@ public override Task Authenticate(AuthenticationParameters paramet var authority = spParameters.Environment.ActiveDirectoryAuthority; var requestContext = new TokenRequestContext(scopes); + AzureSession.Instance.TryGetComponent(nameof(AzureCredentialFactory), out AzureCredentialFactory azureCredentialFactory); var options = new ClientCertificateCredentialOptions() { @@ -53,39 +51,49 @@ public override Task Authenticate(AuthenticationParameters paramet SendCertificateChain = spParameters.SendCertificateChain ?? default(bool) }; + TokenCredential tokenCredential = null; + string parametersLog = null; if (!string.IsNullOrEmpty(spParameters.Thumbprint)) { - //Service Principal with Certificate - var certificate = AzureSession.Instance.DataStore.GetCertificate(spParameters.Thumbprint); - ClientCertificateCredential certCredential = new ClientCertificateCredential(tenantId, spParameters.ApplicationId, certificate, options); - var parametersLog = $"- Thumbprint:'{spParameters.Thumbprint}', ApplicationId:'{spParameters.ApplicationId}', TenantId:'{tenantId}', Scopes:'{string.Join(",", scopes)}', AuthorityHost:'{options.AuthorityHost}'"; - return MsalAccessToken.GetAccessTokenAsync( - nameof(ServicePrincipalAuthenticator), - parametersLog, - certCredential, - requestContext, - cancellationToken, - spParameters.TenantId, - spParameters.ApplicationId); + //Service Principal with Certificate thumbprint + var certCertificate = AzureSession.Instance.DataStore.GetCertificate(spParameters.Thumbprint); + tokenCredential = azureCredentialFactory.CreateClientCertificateCredential(tenantId, spParameters.ApplicationId, certCertificate, options); + parametersLog = $"- Thumbprint:'{spParameters.Thumbprint}', ApplicationId:'{spParameters.ApplicationId}', TenantId:'{tenantId}', Scopes:'{string.Join(",", scopes)}', AuthorityHost:'{options.AuthorityHost}'"; } else if (spParameters.Secret != null) { - // service principal with secret - var secretCredential = new ClientSecretCredential(tenantId, spParameters.ApplicationId, spParameters.Secret.ConvertToString(), options); - var parametersLog = $"- ApplicationId:'{spParameters.ApplicationId}', TenantId:'{tenantId}', Scopes:'{string.Join(",", scopes)}', AuthorityHost:'{options.AuthorityHost}'"; - return MsalAccessToken.GetAccessTokenAsync( - nameof(ServicePrincipalAuthenticator), - parametersLog, - secretCredential, - requestContext, - cancellationToken, - spParameters.TenantId, - spParameters.ApplicationId); + //Service principal with secret + tokenCredential = azureCredentialFactory.CreateClientSecretCredential(tenantId, spParameters.ApplicationId, spParameters.Secret, options); + parametersLog = $"- ApplicationId:'{spParameters.ApplicationId}', TenantId:'{tenantId}', Scopes:'{string.Join(",", scopes)}', AuthorityHost:'{options.AuthorityHost}'"; + } + else if(!string.IsNullOrEmpty(spParameters.CertificatePath)) + { + if (spParameters.CertificateSecret != null) + { + //Service Principal with Certificate file and password + var certCertificate = new X509Certificate2(spParameters.CertificatePath, spParameters.CertificateSecret); + tokenCredential = azureCredentialFactory.CreateClientCertificateCredential(tenantId, spParameters.ApplicationId, certCertificate, options); + parametersLog = $"- CertificatePath(with password):'{spParameters.CertificatePath}', ApplicationId:'{spParameters.ApplicationId}', TenantId:'{tenantId}', Scopes:'{string.Join(",", scopes)}', AuthorityHost:'{options.AuthorityHost}'"; + } + else + { + //Service Principal with Certificate file without password + tokenCredential = azureCredentialFactory.CreateClientCertificateCredential(tenantId, spParameters.ApplicationId, spParameters.CertificatePath, options); + parametersLog = $"- CertificatePath:'{spParameters.CertificatePath}', ApplicationId:'{spParameters.ApplicationId}', TenantId:'{tenantId}', Scopes:'{string.Join(",", scopes)}', AuthorityHost:'{options.AuthorityHost}'"; + } } else { throw new MsalException(MsalError.AuthenticationFailed, string.Format(AuthenticationFailedMessage, clientId)); } + return MsalAccessToken.GetAccessTokenAsync( + nameof(ServicePrincipalAuthenticator), + parametersLog, + tokenCredential, + requestContext, + cancellationToken, + spParameters.TenantId, + spParameters.ApplicationId); } public override bool CanAuthenticate(AuthenticationParameters parameters)