From 9af25e10a81ffa7dcdc50da59be9dc3d6a6167ba Mon Sep 17 00:00:00 2001 From: Peter Ombwa Date: Wed, 5 Oct 2022 15:16:53 -0700 Subject: [PATCH] Add -Credential support. --- .../Extensions/SecureStringExtensions.cs | 30 ++++++++++++ .../Interfaces/IAuthContext.cs | 5 +- .../Utilities/AuthenticationHelpers.cs | 22 ++++++++- .../Authentication/Cmdlets/ConnectMgGraph.cs | 48 +++++++++++++------ .../Authentication/Constants.cs | 4 +- .../Authentication/Models/AuthContext.cs | 2 + .../test/Connect-MgGraph.Tests.ps1 | 20 +++++--- 7 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 src/Authentication/Authentication.Core/Extensions/SecureStringExtensions.cs diff --git a/src/Authentication/Authentication.Core/Extensions/SecureStringExtensions.cs b/src/Authentication/Authentication.Core/Extensions/SecureStringExtensions.cs new file mode 100644 index 00000000000..84c0aae1b94 --- /dev/null +++ b/src/Authentication/Authentication.Core/Extensions/SecureStringExtensions.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace Microsoft.Graph.PowerShell.Authentication.Core.Extensions +{ + public static class SecureStringExtensions + { + public static string ConvertToString(this SecureString secureString) + { + if (secureString == null) + throw new ArgumentNullException(nameof(secureString)); + + IntPtr intPtr = IntPtr.Zero; + try + { + intPtr = Marshal.SecureStringToBSTR(secureString); + return Marshal.PtrToStringBSTR(intPtr); + } + finally + { + Marshal.ZeroFreeBSTR(intPtr); + } + } + } +} diff --git a/src/Authentication/Authentication.Core/Interfaces/IAuthContext.cs b/src/Authentication/Authentication.Core/Interfaces/IAuthContext.cs index 2932b8904b9..38823c6ded6 100644 --- a/src/Authentication/Authentication.Core/Interfaces/IAuthContext.cs +++ b/src/Authentication/Authentication.Core/Interfaces/IAuthContext.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------ using System; +using System.Security; using System.Security.Cryptography.X509Certificates; namespace Microsoft.Graph.PowerShell.Authentication @@ -27,7 +28,8 @@ public enum TokenCredentialType DeviceCode, ClientCertificate, UserProvidedAccessToken, - ManagedIdentity + ManagedIdentity, + ClientSecret } public interface IAuthContext @@ -46,5 +48,6 @@ public interface IAuthContext ContextScope ContextScope { get; set; } Version PSHostVersion { get; set; } TimeSpan ClientTimeout { get; set; } + SecureString ClientSecret { get; set; } } } \ No newline at end of file diff --git a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs index f062e7d5349..edb38bb0517 100644 --- a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs +++ b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs @@ -3,14 +3,15 @@ // ------------------------------------------------------------------------------ using Azure.Core; using Azure.Identity; +using Microsoft.Graph.PowerShell.Authentication.Core.Extensions; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using System; using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Security.Cryptography.X509Certificates; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -38,7 +39,10 @@ public static async Task GetTokenCredentialAsync(IAuthContext a return await GetInteractiveBrowserCredentialAsync(authContext, cancellationToken).ConfigureAwait(false); return await GetDeviceCodeCredentialAsync(authContext, cancellationToken).ConfigureAwait(false); case AuthenticationType.AppOnly: - return await GetClientCertificateCredentialAsync(authContext).ConfigureAwait(false); + if (authContext.TokenCredentialType == TokenCredentialType.ClientCertificate) + return await GetClientCertificateCredentialAsync(authContext).ConfigureAwait(false); + else + return await GetClientSecretCredentialAsync(authContext).ConfigureAwait(false); case AuthenticationType.ManagedIdentity: return await GetManagedIdentityCredentialAsync(authContext).ConfigureAwait(false); case AuthenticationType.UserProvidedAccessToken: @@ -48,6 +52,20 @@ public static async Task GetTokenCredentialAsync(IAuthContext a } } + private static async Task GetClientSecretCredentialAsync(IAuthContext authContext) + { + if (authContext is null) + throw new AuthenticationException(ErrorConstants.Message.MissingAuthContext); + + var clientSecretCredentialOptions = new ClientSecretCredentialOptions + { + AuthorityHost = new Uri(GetAuthorityUrl(authContext)), + TokenCachePersistenceOptions = GetTokenCachePersistenceOptions(authContext) + }; + var clientSecretCredential = new ClientSecretCredential(authContext.TenantId, authContext.ClientId, authContext.ClientSecret.ConvertToString(), clientSecretCredentialOptions); + return await Task.FromResult(clientSecretCredential).ConfigureAwait(false); + } + private static async Task GetManagedIdentityCredentialAsync(IAuthContext authContext) { if (authContext is null) diff --git a/src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs b/src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs index 065472bf43b..d8ffa97b598 100644 --- a/src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs +++ b/src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Graph.PowerShell.Authentication.Common; +using Microsoft.Graph.PowerShell.Authentication.Core.Extensions; using Microsoft.Graph.PowerShell.Authentication.Core.TokenCache; using Microsoft.Graph.PowerShell.Authentication.Core.Utilities; using Microsoft.Graph.PowerShell.Authentication.Helpers; @@ -32,36 +33,42 @@ public class ConnectMgGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssem [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 1, HelpMessage = HelpMessages.Scopes)] public string[] Scopes { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, Position = 1, Mandatory = true, HelpMessage = HelpMessages.ClientId)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, Position = 1, Mandatory = true, HelpMessage = HelpMessages.ClientId)] [Parameter(ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = HelpMessages.ClientId)] [Parameter(ParameterSetName = Constants.IdentityParameterSet, Mandatory = false, HelpMessage = HelpMessages.ManagedIdentityClientId)] [Alias("AppId", "ApplicationId")] public string ClientId { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, Position = 2, HelpMessage = HelpMessages.CertificateSubjectName)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, Position = 2, HelpMessage = HelpMessages.CertificateSubjectName)] [Alias("CertificateSubject")] public string CertificateSubjectName { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, Position = 3, HelpMessage = HelpMessages.CertificateThumbprint)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, Position = 3, HelpMessage = HelpMessages.CertificateThumbprint)] public string CertificateThumbprint { get; set; } - [Parameter(Mandatory = false, ParameterSetName = Constants.AppParameterSet, HelpMessage = HelpMessages.Certificate)] + [Parameter(Mandatory = false, ParameterSetName = Constants.AppCertificateParameterSet, HelpMessage = HelpMessages.Certificate)] public X509Certificate2 Certificate { get; set; } + [Parameter(Mandatory = false, ParameterSetName = Constants.AppSecretCredentialParameterSet, HelpMessage = HelpMessages.Credential)] + public PSCredential Credential { get; set; } + [Parameter(ParameterSetName = Constants.AccessTokenParameterSet, Position = 1, Mandatory = true, HelpMessage = HelpMessages.AccessToken)] public SecureString AccessToken { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, HelpMessage = HelpMessages.TenantId)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, HelpMessage = HelpMessages.TenantId)] + [Parameter(ParameterSetName = Constants.AppSecretCredentialParameterSet, HelpMessage = HelpMessages.TenantId)] [Parameter(ParameterSetName = Constants.UserParameterSet, Position = 4, HelpMessage = HelpMessages.TenantId)] [Alias("Audience", "Tenant")] public string TenantId { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, HelpMessage = HelpMessages.ContextScope)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, HelpMessage = HelpMessages.ContextScope)] + [Parameter(ParameterSetName = Constants.AppSecretCredentialParameterSet, HelpMessage = HelpMessages.ContextScope)] [Parameter(ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = HelpMessages.ContextScope)] [Parameter(ParameterSetName = Constants.IdentityParameterSet, Mandatory = false, HelpMessage = HelpMessages.ContextScope)] public ContextScope ContextScope { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, HelpMessage = HelpMessages.Environment)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, HelpMessage = HelpMessages.Environment)] + [Parameter(ParameterSetName = Constants.AppSecretCredentialParameterSet, HelpMessage = HelpMessages.Environment)] [Parameter(ParameterSetName = Constants.AccessTokenParameterSet, HelpMessage = HelpMessages.Environment)] [Parameter(ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = HelpMessages.Environment)] [Parameter(ParameterSetName = Constants.IdentityParameterSet, Mandatory = false, HelpMessage = HelpMessages.Environment)] @@ -73,7 +80,8 @@ public class ConnectMgGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssem [Alias("UseDeviceAuthentication", "DeviceCode", "DeviceAuth", "Device")] public SwitchParameter UseDeviceCode { get; set; } - [Parameter(ParameterSetName = Constants.AppParameterSet, HelpMessage = HelpMessages.ClientTimeout)] + [Parameter(ParameterSetName = Constants.AppCertificateParameterSet, HelpMessage = HelpMessages.ClientTimeout)] + [Parameter(ParameterSetName = Constants.AppSecretCredentialParameterSet, HelpMessage = HelpMessages.ClientTimeout)] [Parameter(ParameterSetName = Constants.AccessTokenParameterSet, HelpMessage = HelpMessages.ClientTimeout)] [Parameter(ParameterSetName = Constants.UserParameterSet, Mandatory = false, HelpMessage = HelpMessages.ClientTimeout)] [Parameter(ParameterSetName = Constants.IdentityParameterSet, Mandatory = false, HelpMessage = HelpMessages.ClientTimeout)] @@ -168,7 +176,7 @@ private async Task ProcessRecordAsync() authContext.TokenCredentialType = UseDeviceCode ? TokenCredentialType.DeviceCode : TokenCredentialType.InteractiveBrowser; } break; - case Constants.AppParameterSet: + case Constants.AppCertificateParameterSet: { authContext.AuthType = AuthenticationType.AppOnly; authContext.ClientId = ClientId; @@ -180,12 +188,14 @@ private async Task ProcessRecordAsync() authContext.TokenCredentialType = TokenCredentialType.ClientCertificate; } break; - case Constants.AccessTokenParameterSet: + case Constants.AppSecretCredentialParameterSet: { - authContext.AuthType = AuthenticationType.UserProvidedAccessToken; - authContext.TokenCredentialType = TokenCredentialType.UserProvidedAccessToken; - authContext.ContextScope = ContextScope.Process; - GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(Encoding.UTF8.GetBytes(new NetworkCredential(string.Empty, AccessToken).Password)); + authContext.AuthType = AuthenticationType.AppOnly; + authContext.ClientId = Credential.UserName; + authContext.ClientSecret = Credential.Password; + authContext.ClientSecret.MakeReadOnly(); + authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.Process; + authContext.TokenCredentialType = TokenCredentialType.ClientSecret; } break; case Constants.IdentityParameterSet: @@ -196,6 +206,14 @@ private async Task ProcessRecordAsync() authContext.TokenCredentialType = TokenCredentialType.ManagedIdentity; } break; + case Constants.AccessTokenParameterSet: + { + authContext.AuthType = AuthenticationType.UserProvidedAccessToken; + authContext.TokenCredentialType = TokenCredentialType.UserProvidedAccessToken; + authContext.ContextScope = ContextScope.Process; + GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(Encoding.UTF8.GetBytes(AccessToken.ConvertToString())); + } + break; } try @@ -244,7 +262,7 @@ private void ValidateParameters() { switch (ParameterSetName) { - case Constants.AppParameterSet: + case Constants.AppCertificateParameterSet: { // Client Id if (string.IsNullOrEmpty(ClientId)) diff --git a/src/Authentication/Authentication/Constants.cs b/src/Authentication/Authentication/Constants.cs index b3e2dff6431..5e9a0aa0992 100644 --- a/src/Authentication/Authentication/Constants.cs +++ b/src/Authentication/Authentication/Constants.cs @@ -14,7 +14,8 @@ public static class Constants public const string PSSDKHeaderValueV1 = "graph-powershell/{0}.{1}.{2}"; public const string PSSDKHeaderValueBeta = "graph-powershell-beta/{0}.{1}.{2}"; internal const string UserParameterSet = nameof(UserParameterSet); - internal const string AppParameterSet = nameof(AppParameterSet); + internal const string AppCertificateParameterSet = nameof(AppCertificateParameterSet); + internal const string AppSecretCredentialParameterSet = nameof(AppSecretCredentialParameterSet); internal const string AccessTokenParameterSet = nameof(AccessTokenParameterSet); internal const string IdentityParameterSet = nameof(IdentityParameterSet); internal static readonly string ContextSettingsPath = Path.Combine(Core.Constants.GraphDirectoryPath, "mg.context.json"); @@ -26,6 +27,7 @@ public static class HelpMessages public const string CertificateSubjectName = "The subject distinguished name of a certificate. The Certificate will be retrieved from the current user's certificate store."; public const string CertificateThumbprint = "The thumbprint of your certificate. The Certificate will be retrieved from the current user's certificate store."; public const string Certificate = "An X.509 certificate supplied during invocation."; + public const string Credential = "The PSCredential object provides the application ID and client secret for service principal credentials. For more information about the PSCredential object, type Get-Help Get-Credential."; public const string AccessToken = "Specifies a bearer token for Microsoft Graph service. Access tokens do timeout and you'll have to handle their refresh."; public const string TenantId = "The id of the tenant to connect to. You can also use this parameter to specify your sign-in audience. i.e., common, organizations, or consumers. See https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#authority."; public const string ContextScope = "Determines the scope of authentication context. This accepts `Process` for the current process, or `CurrentUser` for all sessions started by user."; diff --git a/src/Authentication/Authentication/Models/AuthContext.cs b/src/Authentication/Authentication/Models/AuthContext.cs index a6cd6795647..673ca5a993b 100644 --- a/src/Authentication/Authentication/Models/AuthContext.cs +++ b/src/Authentication/Authentication/Models/AuthContext.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------ using System; +using System.Security; using System.Security.Cryptography.X509Certificates; namespace Microsoft.Graph.PowerShell.Authentication @@ -24,6 +25,7 @@ public class AuthContext: IAuthContext public Version PSHostVersion { get; set; } public TimeSpan ClientTimeout { get; set; } = TimeSpan.FromSeconds(Constants.ClientTimeout); public string ManagedIdentityId { get; set; } + public SecureString ClientSecret { get; set; } public AuthContext() { diff --git a/src/Authentication/Authentication/test/Connect-MgGraph.Tests.ps1 b/src/Authentication/Authentication/test/Connect-MgGraph.Tests.ps1 index 6a71bf26866..53c5f808ad4 100644 --- a/src/Authentication/Authentication/test/Connect-MgGraph.Tests.ps1 +++ b/src/Authentication/Authentication/test/Connect-MgGraph.Tests.ps1 @@ -18,7 +18,7 @@ Describe 'Connect-MgGraph ParameterSets' { } it 'Should have three ParameterSets' { $ConnectMgGraphCommand | Should -Not -BeNullOrEmpty - $ConnectMgGraphCommand.ParameterSets | Should -HaveCount 4 + $ConnectMgGraphCommand.ParameterSets | Should -HaveCount 5 } It 'Should have UserParameterSet' { $UserParameterSet = $ConnectMgGraphCommand.ParameterSets | Where-Object Name -eq 'UserParameterSet' @@ -28,15 +28,23 @@ Describe 'Connect-MgGraph ParameterSets' { @('ClientId', 'TenantId', 'ContextScope', 'Environment', 'ClientTimeout') | Should -BeIn $UserParameterSet.Parameters.Name } - It 'Should have AppParameterSet' { - $AppParameterSet = $ConnectMgGraphCommand.ParameterSets | Where-Object Name -eq 'AppParameterSet' - $AppParameterSet | Should -Not -BeNull - @('ClientId', 'TenantId', 'CertificateSubjectName', 'CertificateThumbprint', 'ContextScope', 'Environment', 'ClientTimeout') | Should -BeIn $AppParameterSet.Parameters.Name - $MandatoryParameters = $AppParameterSet.Parameters | Where-Object IsMandatory + It 'Should have AppCertificateParameterSet' { + $AppCertificateParameterSet = $ConnectMgGraphCommand.ParameterSets | Where-Object Name -eq 'AppCertificateParameterSet' + $AppCertificateParameterSet | Should -Not -BeNull + @('ClientId', 'TenantId', 'CertificateSubjectName', 'CertificateThumbprint', 'ContextScope', 'Environment', 'ClientTimeout') | Should -BeIn $AppCertificateParameterSet.Parameters.Name + $MandatoryParameters = $AppCertificateParameterSet.Parameters | Where-Object IsMandatory $MandatoryParameters | Should -HaveCount 1 $MandatoryParameters.Name | Should -Be 'ClientId' } + It 'Should have AppSecretCredentialParameterSet' { + $AppSecretCredentialParameterSet = $ConnectMgGraphCommand.ParameterSets | Where-Object Name -eq 'AppSecretCredentialParameterSet' + $AppSecretCredentialParameterSet | Should -Not -BeNull + @('Credential', 'TenantId', 'ContextScope', 'Environment', 'ClientTimeout') | Should -BeIn $AppSecretCredentialParameterSet.Parameters.Name + $MandatoryParameters = $AppSecretCredentialParameterSet.Parameters | Where-Object IsMandatory + $MandatoryParameters | Should -HaveCount 0 + } + It 'Should Have AccessTokenParameterSet' { $AccessTokenParameterSet = $ConnectMgGraphCommand.ParameterSets | Where-Object Name -eq 'AccessTokenParameterSet' $AccessTokenParameterSet | Should -Not -BeNull