Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ------------------------------------------------------------------------------

using System;
using System.Security;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.Graph.PowerShell.Authentication
Expand All @@ -27,7 +28,8 @@ public enum TokenCredentialType
DeviceCode,
ClientCertificate,
UserProvidedAccessToken,
ManagedIdentity
ManagedIdentity,
ClientSecret
}

public interface IAuthContext
Expand All @@ -46,5 +48,6 @@ public interface IAuthContext
ContextScope ContextScope { get; set; }
Version PSHostVersion { get; set; }
TimeSpan ClientTimeout { get; set; }
SecureString ClientSecret { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -38,7 +39,10 @@ public static async Task<TokenCredential> 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:
Expand All @@ -48,6 +52,20 @@ public static async Task<TokenCredential> GetTokenCredentialAsync(IAuthContext a
}
}

private static async Task<TokenCredential> 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<TokenCredential> GetManagedIdentityCredentialAsync(IAuthContext authContext)
{
if (authContext is null)
Expand Down
48 changes: 33 additions & 15 deletions src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand All @@ -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)]
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Copy link

@nkasco nkasco Nov 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this cause user confusion when users inevitably try to use -Credential within Connect-MgGraph for ROPC / User Credential auth flow use with delegated permission types? I get that you're only intending to add support for client secrets / App Only, I just think in the process it might be creating UX confusion.

Perhaps an alias or rename of the parameter could aleviate this. -ClientSecret, -ClientSecretCredential, -AppCredential, or -AppSecret might be a few possibilities.

#125 (comment) - Looks like it already has created some confusion unless I've completely misunderstood the code. Keep me honest here.

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:
Expand All @@ -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
Expand Down Expand Up @@ -244,7 +262,7 @@ private void ValidateParameters()
{
switch (ParameterSetName)
{
case Constants.AppParameterSet:
case Constants.AppCertificateParameterSet:
{
// Client Id
if (string.IsNullOrEmpty(ClientId))
Expand Down
4 changes: 3 additions & 1 deletion src/Authentication/Authentication/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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.";
Expand Down
2 changes: 2 additions & 0 deletions src/Authentication/Authentication/Models/AuthContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ------------------------------------------------------------------------------

using System;
using System.Security;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.Graph.PowerShell.Authentication
Expand All @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down