diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 64e60047fc2c..01ba6179a063 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -19,6 +19,9 @@ --> ## Upcoming Release +* Added Service Principal support for SSH certificate generation in 'SshCredentialFactory' + - Service Principals authenticated with client secret or certificate can now obtain SSH certificates for Entra ID login + - Enables CI/CD pipelines and automation scenarios that use Service Principal authentication with Az.SSH and Az.SFTP modules ## Version 5.4.0 * Updated the `System.Memory` dependency to v4.6.3 to support the Storage SDK update. diff --git a/src/Accounts/Authentication/Authentication/TokenCache/PowerShellTokenCacheProvider.cs b/src/Accounts/Authentication/Authentication/TokenCache/PowerShellTokenCacheProvider.cs index e5a81f289635..ee011e9244cd 100644 --- a/src/Accounts/Authentication/Authentication/TokenCache/PowerShellTokenCacheProvider.cs +++ b/src/Accounts/Authentication/Authentication/TokenCache/PowerShellTokenCacheProvider.cs @@ -15,6 +15,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security; +using System.Security.Cryptography.X509Certificates; using Azure.Identity; @@ -214,5 +216,37 @@ public virtual IPublicClientApplication CreatePublicClient(string authority = nu public abstract TokenCachePersistenceOptions GetTokenCachePersistenceOptions(); + /// + /// Creates a confidential client app with a client secret. + /// Used for Service Principal SSH certificate authentication. + /// + public virtual IConfidentialClientApplication CreateConfidentialClient(string authority, string tenantId, string clientId, string clientSecret) + { + var builder = ConfidentialClientApplicationBuilder.Create(clientId) + .WithClientSecret(clientSecret) + .WithExperimentalFeatures(); + if (!string.IsNullOrEmpty(authority)) + { + builder.WithAuthority(authority, tenantId ?? organizationTenant); + } + return builder.Build(); + } + + /// + /// Creates a confidential client app with a certificate. + /// Used for Service Principal SSH certificate authentication. + /// + public virtual IConfidentialClientApplication CreateConfidentialClient(string authority, string tenantId, string clientId, X509Certificate2 certificate) + { + var builder = ConfidentialClientApplicationBuilder.Create(clientId) + .WithCertificate(certificate) + .WithExperimentalFeatures(); + if (!string.IsNullOrEmpty(authority)) + { + builder.WithAuthority(authority, tenantId ?? organizationTenant); + } + return builder.Build(); + } + } } diff --git a/src/Accounts/Authentication/Factories/SshCredentialFactory.cs b/src/Accounts/Authentication/Factories/SshCredentialFactory.cs index 8905172e8f07..23ea36a8b138 100644 --- a/src/Accounts/Authentication/Factories/SshCredentialFactory.cs +++ b/src/Accounts/Authentication/Factories/SshCredentialFactory.cs @@ -15,6 +15,10 @@ using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Models; using Microsoft.Azure.Commands.Common.Authentication.Properties; +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AuthScheme; +using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.SSHCertificates; using Microsoft.WindowsAzure.Commands.Utilities.Common; @@ -22,7 +26,9 @@ using System; using System.Collections.Generic; +using System.Security; using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Text; namespace Microsoft.Azure.Commands.Common.Authentication.Factories @@ -62,11 +68,25 @@ public SshCredential GetSshCredential(IAzureContext context, RSAParameters rsaKe throw new NullReferenceException(Resources.AuthenticationClientFactoryNotRegistered); } - var publicClient = tokenCacheProvider.CreatePublicClient(context.Environment.ActiveDirectoryAuthority, context.Tenant.Id); string scope = GetAuthScope(); List scopes = new List() { scope }; var jwk = CreateJwk(rsaKeyInfo, out string keyId); + switch (context.Account.Type) + { + case AzureAccount.AccountType.User: + return AcquireTokenForUser(tokenCacheProvider, context, scopes, jwk, keyId); + case AzureAccount.AccountType.ServicePrincipal: + return AcquireTokenForServicePrincipal(tokenCacheProvider, context, scopes, jwk, keyId); + default: + throw new InvalidOperationException(string.Format(Resources.UnsupportedAccountTypeForSshCertificate, context.Account.Type)); + } + } + + private SshCredential AcquireTokenForUser(PowerShellTokenCacheProvider tokenCacheProvider, IAzureContext context, List scopes, string jwk, string keyId) + { + var publicClient = tokenCacheProvider.CreatePublicClient(context.Environment.ActiveDirectoryAuthority, context.Tenant.Id); + var account = publicClient.GetAccountAsync(context.Account.ExtendedProperties["HomeAccountId"]) .ConfigureAwait(false).GetAwaiter().GetResult(); var result = publicClient.AcquireTokenSilent(scopes, account) @@ -74,17 +94,158 @@ public SshCredential GetSshCredential(IAzureContext context, RSAParameters rsaKe .ExecuteAsync(); var accessToken = result.ConfigureAwait(false).GetAwaiter().GetResult(); - var resultToken = new SshCredential() + return new SshCredential() { Credential = accessToken.AccessToken, ExpiresOn = accessToken.ExpiresOn, }; - return resultToken; + } + + private SshCredential AcquireTokenForServicePrincipal(PowerShellTokenCacheProvider tokenCacheProvider, IAzureContext context, List scopes, string jwk, string keyId) + { + string authority = context.Environment.ActiveDirectoryAuthority; + string tenantId = context.Tenant.Id; + string clientId = context.Account.Id; + + var confidentialClient = CreateConfidentialClientForServicePrincipal(tokenCacheProvider, authority, tenantId, clientId, context); + + var authExtension = new MsalAuthenticationExtension + { + AuthenticationOperation = new SshCertAuthOperation(keyId, jwk) + }; + + var result = confidentialClient.AcquireTokenForClient(scopes) + .WithForceRefresh(true) + .WithAuthenticationExtension(authExtension) + .ExecuteAsync() + .ConfigureAwait(false).GetAwaiter().GetResult(); + + return new SshCredential() + { + Credential = result.AccessToken, + ExpiresOn = result.ExpiresOn, + }; + } + + private IConfidentialClientApplication CreateConfidentialClientForServicePrincipal(PowerShellTokenCacheProvider tokenCacheProvider, string authority, string tenantId, string clientId, IAzureContext context) + { + // Try certificate thumbprint first + string thumbprint = context.Account.GetProperty(AzureAccount.Property.CertificateThumbprint); + if (!string.IsNullOrEmpty(thumbprint)) + { + var certificate = AzureSession.Instance.DataStore.GetCertificate(thumbprint); + if (certificate != null) + { + return tokenCacheProvider.CreateConfidentialClient(authority, tenantId, clientId, certificate); + } + } + + // Try certificate path + string certificatePath = context.Account.GetProperty(AzureAccount.Property.CertificatePath); + if (!string.IsNullOrEmpty(certificatePath)) + { + SecureString certificatePassword = GetServicePrincipalSecureString(context, AzureAccount.Property.CertificatePassword); + X509Certificate2 certificate = certificatePassword != null + ? new X509Certificate2(certificatePath, certificatePassword) + : new X509Certificate2(certificatePath); + return tokenCacheProvider.CreateConfidentialClient(authority, tenantId, clientId, certificate); + } + + // Try client secret + string secret = context.Account.GetProperty(AzureAccount.Property.ServicePrincipalSecret); + if (string.IsNullOrEmpty(secret)) + { + SecureString secureSecret = GetServicePrincipalSecureString(context, AzureAccount.Property.ServicePrincipalSecret); + if (secureSecret != null) + { + secret = ConvertToPlainText(secureSecret); + } + } + + if (!string.IsNullOrEmpty(secret)) + { + return tokenCacheProvider.CreateConfidentialClient(authority, tenantId, clientId, secret); + } + + throw new InvalidOperationException(Resources.ServicePrincipalCredentialNotFound); + } + + private SecureString GetServicePrincipalSecureString(IAzureContext context, string propertyName) + { + try + { + if (AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out AzKeyStore keyStore)) + { + return keyStore.GetSecureString(new ServicePrincipalKey(propertyName, context.Account.Id, context.Tenant.Id)); + } + } + catch + { + // Key not found in store, return null + } + return null; + } + + private static string ConvertToPlainText(SecureString secureString) + { + if (secureString == null) + { + return null; + } + var ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(secureString); + try + { + return System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr); + } + finally + { + System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr); + } } private string GetAuthScope() { return $"{AadSshLoginForLinuxServerAppId}/.default"; } + + /// + /// Custom IAuthenticationOperation that instructs MSAL to request and accept + /// SSH certificate token type instead of bearer tokens. + /// This is the equivalent of WithSSHCertificateAuthenticationScheme for confidential client flows. + /// + private class SshCertAuthOperation : IAuthenticationOperation + { + private const string SshCertTokenType = "ssh-cert"; + private readonly string _jwk; + + public SshCertAuthOperation(string keyId, string jwk) + { + KeyId = keyId; + _jwk = jwk; + } + + public int TelemetryTokenType => 3; + + public string AuthorizationHeaderPrefix => + throw new InvalidOperationException("SSH certificates cannot be used as HTTP authorization headers."); + + public string AccessTokenType => SshCertTokenType; + + public string KeyId { get; } + + public IReadOnlyDictionary GetTokenRequestParams() + { + return new Dictionary + { + { "token_type", SshCertTokenType }, + { "req_cnf", _jwk } + }; + } + + public void FormatResult(AuthenticationResult authenticationResult) + { + // no-op + } + } } } diff --git a/src/Accounts/Authentication/Properties/Resources.Designer.cs b/src/Accounts/Authentication/Properties/Resources.Designer.cs index 53b43fdb5a3c..de15293c7c8b 100644 --- a/src/Accounts/Authentication/Properties/Resources.Designer.cs +++ b/src/Accounts/Authentication/Properties/Resources.Designer.cs @@ -963,5 +963,23 @@ public static string x86InProgramFiles { return ResourceManager.GetString("x86InProgramFiles", resourceCulture); } } + + /// + /// Looks up a localized string similar to Account type '{0}' is not supported for SSH certificate generation. Supported types are: User, ServicePrincipal.. + /// + public static string UnsupportedAccountTypeForSshCertificate { + get { + return ResourceManager.GetString("UnsupportedAccountTypeForSshCertificate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No credential (client secret or certificate) found for the Service Principal. Please re-authenticate using Connect-AzAccount with -CertificateThumbprint, -CertificatePath, or a client secret.. + /// + public static string ServicePrincipalCredentialNotFound { + get { + return ResourceManager.GetString("ServicePrincipalCredentialNotFound", resourceCulture); + } + } } } diff --git a/src/Accounts/Authentication/Properties/Resources.resx b/src/Accounts/Authentication/Properties/Resources.resx index 2d06378d1b8d..791aec92b5df 100644 --- a/src/Accounts/Authentication/Properties/Resources.resx +++ b/src/Accounts/Authentication/Properties/Resources.resx @@ -434,4 +434,10 @@ Run the cmdlet below to authenticate interactively; additional parameters may be Connect-AzAccount -Tenant (Get-AzContext).Tenant.Id -ClaimsChallenge "{1}" 0 = error message about policy violation; 1 = claims challenge in base64 + + Account type '{0}' is not supported for SSH certificate generation. Supported types are: User, ServicePrincipal. + + + No credential (client secret or certificate) found for the Service Principal. Please re-authenticate using Connect-AzAccount with -CertificateThumbprint, -CertificatePath, or a client secret. + \ No newline at end of file diff --git a/src/Sftp/Sftp.Test/ScenarioTests/FileUtilsTests.cs b/src/Sftp/Sftp.Test/ScenarioTests/FileUtilsTests.cs index e1deab50a0c5..444ebc1fdf61 100644 --- a/src/Sftp/Sftp.Test/ScenarioTests/FileUtilsTests.cs +++ b/src/Sftp/Sftp.Test/ScenarioTests/FileUtilsTests.cs @@ -1,8 +1,11 @@ using System; using System.IO; +using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Models; +using Microsoft.Azure.Commands.Common.Authentication.Models; using Microsoft.Azure.Commands.Common.Exceptions; using Microsoft.Azure.PowerShell.Cmdlets.Sftp.Common; using Xunit; @@ -15,7 +18,7 @@ namespace Microsoft.Azure.Commands.Sftp.Test.ScenarioTests /// Port of Azure CLI test_file_utils.py /// Owner: johnli1 /// - public class FileUtilsTests + public class FileUtilsTests : IDisposable { private string _tempDir; @@ -23,6 +26,37 @@ public FileUtilsTests() { _tempDir = Path.Combine(Path.GetTempPath(), "sftp_file_utils_test_" + Guid.NewGuid().ToString("N").Substring(0, 8)); Directory.CreateDirectory(_tempDir); + EnsureAzureSessionInitialized(); + } + + private static bool _sessionInitialized = false; + private static readonly object _sessionLock = new object(); + + private static void EnsureAzureSessionInitialized() + { + lock (_sessionLock) + { + if (!_sessionInitialized) + { + var dataStore = new MemoryDataStore(); + var session = new AzureSessionInitializer.AdalSession + { + DataStore = dataStore, + ProfileDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".Azure"), + ProfileFile = "AzureProfile.json", + TokenCacheDirectory = Path.GetTempPath(), + TokenCacheFile = "msal.cache" + }; + session.TokenCache = session.TokenCache ?? new AzureTokenCache(); + AzureSession.Initialize(() => session, true); + _sessionInitialized = true; + } + } + } + + public void Dispose() + { + TearDown(); } private void TearDown() @@ -594,5 +628,232 @@ public void TestGetAndWriteCertificateUnsupportedCloud() TearDown(); } } + + /// + /// Generates a valid SSH RSA public key file for testing. + /// + private string CreateTestPublicKeyFile() + { + using (var rsa = RSA.Create(2048)) + { + var parameters = rsa.ExportParameters(false); + // Build SSH public key format: ssh-rsa + using (var ms = new System.IO.MemoryStream()) + using (var writer = new System.IO.BinaryWriter(ms)) + { + var algorithmBytes = System.Text.Encoding.ASCII.GetBytes("ssh-rsa"); + WriteSshField(writer, algorithmBytes); + WriteSshField(writer, parameters.Exponent); + WriteSshField(writer, parameters.Modulus); + var base64 = Convert.ToBase64String(ms.ToArray()); + var publicKeyText = $"ssh-rsa {base64} test@test"; + var publicKeyFile = Path.Combine(_tempDir, "id_rsa.pub"); + File.WriteAllText(publicKeyFile, publicKeyText); + return publicKeyFile; + } + } + } + + private static void WriteSshField(System.IO.BinaryWriter writer, byte[] data) + { + // SSH uses big-endian 4-byte length prefix + var lengthBytes = BitConverter.GetBytes(data.Length); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(lengthBytes); + } + writer.Write(lengthBytes); + writer.Write(data); + } + + /// + /// Sets up AzureSession with a mock ISshCredentialFactory that returns a credential + /// with the given token string. + /// + private void SetupMockSshCredentialFactory(string credentialToken) + { + var mockCredential = new SshCredential() + { + Credential = credentialToken, + ExpiresOn = DateTimeOffset.UtcNow.AddHours(1), + }; + + var mockFactory = new Mock(); + mockFactory.Setup(f => f.GetSshCredential( + It.IsAny(), + It.IsAny())) + .Returns(mockCredential); + + AzureSession.Instance.RegisterComponent( + nameof(ISshCredentialFactory), () => mockFactory.Object, true); + } + + private IAzureContext CreateMockContext(string accountType) + { + var contextMock = new Mock(); + var envMock = new Mock(); + envMock.Setup(e => e.Name).Returns("AzureCloud"); + envMock.Setup(e => e.ActiveDirectoryAuthority).Returns("https://login.microsoftonline.com/"); + contextMock.Setup(c => c.Environment).Returns(envMock.Object); + + var tenantMock = new Mock(); + tenantMock.Setup(t => t.Id).Returns("00000000-0000-0000-0000-000000000001"); + contextMock.Setup(c => c.Tenant).Returns(tenantMock.Object); + + var accountMock = new Mock(); + accountMock.Setup(a => a.Type).Returns(accountType); + accountMock.Setup(a => a.Id).Returns("test-app-id"); + contextMock.Setup(c => c.Account).Returns(accountMock.Object); + + return contextMock.Object; + } + + [Fact] + public void TestGetAndWriteCertificateServicePrincipalCallsFactory() + { + try + { + // Arrange + var publicKeyFile = CreateTestPublicKeyFile(); + var certFile = Path.Combine(_tempDir, "id_rsa-cert.pub"); + var dummyToken = "AAAAB3NzaC1yc2EAAAADAQAB_test_sp_token"; + + SetupMockSshCredentialFactory(dummyToken); + var context = CreateMockContext(AzureAccount.AccountType.ServicePrincipal); + + // Act - GetAndWriteCertificate will call factory.GetSshCredential, write cert, + // then try to extract principals via ssh-keygen (which won't be available in test). + // We expect it to either succeed or throw at principal extraction stage. + Exception caughtException = null; + try + { + FileUtils.GetAndWriteCertificate(context, publicKeyFile, certFile, null); + } + catch (Exception ex) + { + caughtException = ex; + } + + // Assert - The cert file should have been written (factory was called successfully) + Assert.True(File.Exists(certFile), "Certificate file should have been written by the factory"); + var certContent = File.ReadAllText(certFile); + Assert.Contains(dummyToken, certContent); + Assert.StartsWith("ssh-rsa-cert-v01@openssh.com", certContent); + } + finally + { + TearDown(); + } + } + + [Fact] + public void TestGetAndWriteCertificateUserAccountCallsFactory() + { + try + { + // Arrange + var publicKeyFile = CreateTestPublicKeyFile(); + var certFile = Path.Combine(_tempDir, "id_rsa-cert.pub"); + var dummyToken = "AAAAB3NzaC1yc2EAAAADAQAB_test_user_token"; + + SetupMockSshCredentialFactory(dummyToken); + var context = CreateMockContext(AzureAccount.AccountType.User); + + // Act + Exception caughtException = null; + try + { + FileUtils.GetAndWriteCertificate(context, publicKeyFile, certFile, null); + } + catch (Exception ex) + { + caughtException = ex; + } + + // Assert - The cert file should have been written + Assert.True(File.Exists(certFile), "Certificate file should have been written by the factory"); + var certContent = File.ReadAllText(certFile); + Assert.Contains(dummyToken, certContent); + Assert.StartsWith("ssh-rsa-cert-v01@openssh.com", certContent); + } + finally + { + TearDown(); + } + } + + [Fact] + public void TestGetAndWriteCertificateFactoryNotRegisteredThrows() + { + try + { + // Arrange - ensure no factory is registered by registering a null-returning component + var publicKeyFile = CreateTestPublicKeyFile(); + var certFile = Path.Combine(_tempDir, "id_rsa-cert.pub"); + var context = CreateMockContext(AzureAccount.AccountType.ServicePrincipal); + + // Act & Assert - Without a factory registered, this should throw + Assert.ThrowsAny(() => + FileUtils.GetAndWriteCertificate(context, publicKeyFile, certFile, null)); + } + finally + { + TearDown(); + } + } + + [Fact] + public void TestGetAndWriteCertificateNullCredentialThrows() + { + try + { + // Arrange - factory returns null + var mockFactory = new Mock(); + mockFactory.Setup(f => f.GetSshCredential( + It.IsAny(), + It.IsAny())) + .Returns((SshCredential)null); + + AzureSession.Instance.RegisterComponent( + nameof(ISshCredentialFactory), () => mockFactory.Object, true); + + var publicKeyFile = CreateTestPublicKeyFile(); + var certFile = Path.Combine(_tempDir, "id_rsa-cert.pub"); + var context = CreateMockContext(AzureAccount.AccountType.ServicePrincipal); + + // Act & Assert + var ex = Assert.Throws(() => + FileUtils.GetAndWriteCertificate(context, publicKeyFile, certFile, null)); + Assert.Contains("Failed to obtain SSH certificate credential", ex.Message); + } + finally + { + TearDown(); + } + } + + [Fact] + public void TestGetAndWriteCertificateEmptyCredentialStringThrows() + { + try + { + // Arrange - factory returns credential with empty string + SetupMockSshCredentialFactory(string.Empty); + + var publicKeyFile = CreateTestPublicKeyFile(); + var certFile = Path.Combine(_tempDir, "id_rsa-cert.pub"); + var context = CreateMockContext(AzureAccount.AccountType.ServicePrincipal); + + // Act & Assert - The inner AzPSInvalidOperationException gets caught and re-wrapped + // by the outer catch block as AzPSApplicationException + var ex = Assert.Throws(() => + FileUtils.GetAndWriteCertificate(context, publicKeyFile, certFile, null)); + Assert.Contains("SSH credential string is null or empty", ex.Message); + } + finally + { + TearDown(); + } + } } } diff --git a/src/Sftp/Sftp/CHANGELOG.md b/src/Sftp/Sftp/CHANGELOG.md index fae847bea162..53d0d19db6f9 100644 --- a/src/Sftp/Sftp/CHANGELOG.md +++ b/src/Sftp/Sftp/CHANGELOG.md @@ -19,6 +19,9 @@ --> ## Upcoming Release +* Added Service Principal support for SFTP with Entra ID (AAD) login + - Users authenticated as Service Principals can now use SFTP commands with Entra ID certificate authentication + - Removed restriction that limited SSH certificate generation to User accounts only ## Version 0.2.0 * Added confirmation prompt when an SSH key pair already exists at the target location diff --git a/src/Sftp/Sftp/Common/FileUtils.cs b/src/Sftp/Sftp/Common/FileUtils.cs index 9b4524f8b1cc..7d6e94084e50 100644 --- a/src/Sftp/Sftp/Common/FileUtils.cs +++ b/src/Sftp/Sftp/Common/FileUtils.cs @@ -311,13 +311,9 @@ public static Tuple GetAndWriteCertificate(IAzureContext context "Authentication failed. User interaction is required. " + "This may be due to conditional access policy settings such as multi-factor authentication (MFA). ", ex); } - catch (System.Collections.Generic.KeyNotFoundException exception) + catch (Exception ex2) when (ex2 is System.Collections.Generic.KeyNotFoundException || ex2 is InvalidOperationException) { - if (context.Account.Type != AzureAccount.AccountType.User) - { - throw new AzPSApplicationException($"Failed to generate AAD certificate. Unsupported account type: {context.Account.Type}. Only User accounts are supported for SSH certificate generation."); - } - throw new AzPSApplicationException($"Failed to generate AAD certificate: {exception.Message}. Please ensure you are properly authenticated with 'Connect-AzAccount'."); + throw new AzPSApplicationException($"Failed to generate AAD certificate: {ex2.Message}. Please ensure you are properly authenticated with 'Connect-AzAccount'."); } // Write OpenSSH certificate diff --git a/src/Ssh/Ssh/ChangeLog.md b/src/Ssh/Ssh/ChangeLog.md index 15313a9789aa..734a60a3bad9 100644 --- a/src/Ssh/Ssh/ChangeLog.md +++ b/src/Ssh/Ssh/ChangeLog.md @@ -19,6 +19,9 @@ --> ## Upcoming Release +* Added Service Principal support for SSH with Entra ID (AAD) login + - Users authenticated as Service Principals can now use SSH commands with Entra ID certificate authentication + - Removed restriction that limited SSH certificate generation to User accounts only ## Version 0.2.3 * Implemented code refactoring, no behavior changes expected. diff --git a/src/Ssh/Ssh/Common/SshBaseCmdlet.cs b/src/Ssh/Ssh/Common/SshBaseCmdlet.cs index 453e13096380..e1bb52e4eea0 100644 --- a/src/Ssh/Ssh/Common/SshBaseCmdlet.cs +++ b/src/Ssh/Ssh/Common/SshBaseCmdlet.cs @@ -1030,13 +1030,8 @@ private SshCredential GetAccessToken(string publicKeyFile) { token = factory.GetSshCredential(context, parameters); } - catch (KeyNotFoundException exception) + catch (Exception exception) when (exception is KeyNotFoundException || exception is InvalidOperationException) { - if (context.Account.Type != AzureAccount.AccountType.User) - { - throw new AzPSApplicationException(String.Format(Resources.FailedToAADUnsupportedAccountType, context.Account.Type)); - } - throw new AzPSApplicationException($"Failed to generate AAD certificate with exception: {exception.Message}."); }