Skip to content

Allow Connect-Graph to take an x509Certificate. #440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 18, 2020
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
Expand Up @@ -3,13 +3,15 @@
using Microsoft.Graph.Auth;
using Microsoft.Graph.PowerShell.Authentication;
using Microsoft.Graph.PowerShell.Authentication.Helpers;

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using Xunit;
public class AuthenticationHelpersTests
{
Expand Down Expand Up @@ -78,7 +80,7 @@ public void ShouldUseClientCredentialProviderWhenAppOnlyContextIsProvided()
CertificateName = "cn=dummyCert",
ContextScope = ContextScope.Process
};
CreateSelfSignedCert(appOnlyAuthContext.CertificateName);
CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateName);

// Act
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);
Expand All @@ -87,12 +89,155 @@ public void ShouldUseClientCredentialProviderWhenAppOnlyContextIsProvided()
Assert.IsType<ClientCredentialProvider>(authProvider);

// reset
DeleteSelfSignedCert(appOnlyAuthContext.CertificateName);
DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateName);
GraphSession.Reset();

}

[Fact]
public void ShouldUseInMemoryCertificateWhenProvided()
{
// Arrange
var certificate = CreateSelfSignedCert("cn=inmemorycert");
AuthContext appOnlyAuthContext = new AuthContext
{
AuthType = AuthenticationType.AppOnly,
ClientId = Guid.NewGuid().ToString(),
Certificate = certificate,
ContextScope = ContextScope.Process
};
// Act
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);

// Assert
Assert.IsType<ClientCredentialProvider>(authProvider);
var clientCredentialProvider = (ClientCredentialProvider)authProvider;
// Assert: That the certificate created and set above is the same as used here.
Assert.Equal(clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate, certificate);
GraphSession.Reset();

}

private void CreateSelfSignedCert(string certName)
[Fact]
public void ShouldUseCertNameInsteadOfPassedInCertificateWhenBothAreSpecified()
{
// Arrange
var dummyCertName = "CN=dummycert";
var inMemoryCertName = "CN=inmemorycert";
CreateAndStoreSelfSignedCert(dummyCertName);
var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName);
AuthContext appOnlyAuthContext = new AuthContext
{
AuthType = AuthenticationType.AppOnly,
ClientId = Guid.NewGuid().ToString(),
CertificateName = dummyCertName,
Certificate = inMemoryCertificate,
ContextScope = ContextScope.Process
};
// Act
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);

// Assert
Assert.IsType<ClientCredentialProvider>(authProvider);
var clientCredentialProvider = (ClientCredentialProvider)authProvider;
// Assert: That the certificate used is dummycert, that is in the store
Assert.NotEqual(inMemoryCertName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name);
Assert.Equal(appOnlyAuthContext.CertificateName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name);

//CleanUp
DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateName);
GraphSession.Reset();
}

[Fact]
public void ShouldUseCertThumbPrintInsteadOfPassedInCertificateWhenBothAreSpecified()
{
// Arrange
var dummyCertName = "CN=dummycert";
var inMemoryCertName = "CN=inmemorycert";
var storedDummyCertificate = CreateAndStoreSelfSignedCert(dummyCertName);
var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName);
AuthContext appOnlyAuthContext = new AuthContext
{
AuthType = AuthenticationType.AppOnly,
ClientId = Guid.NewGuid().ToString(),
CertificateThumbprint = storedDummyCertificate.Thumbprint,
Certificate = inMemoryCertificate,
ContextScope = ContextScope.Process
};
// Act
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);

// Assert
Assert.IsType<ClientCredentialProvider>(authProvider);
var clientCredentialProvider = (ClientCredentialProvider)authProvider;
// Assert: That the certificate used is dummycert (Thumbprint), that is in the store
Assert.NotEqual(inMemoryCertName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name);
Assert.Equal(appOnlyAuthContext.CertificateThumbprint, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.Thumbprint);

//CleanUp
DeleteSelfSignedCertByThumbprint(appOnlyAuthContext.CertificateThumbprint);
GraphSession.Reset();
}

[Fact]
public void ShouldThrowIfNonExistentCertNameIsProvided()
{
// Arrange
var dummyCertName = "CN=NonExistingCert";
AuthContext appOnlyAuthContext = new AuthContext
{
AuthType = AuthenticationType.AppOnly,
ClientId = Guid.NewGuid().ToString(),
CertificateName = dummyCertName,
ContextScope = ContextScope.Process
};
// Act
Action action = () => AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);

//Assert
Assert.ThrowsAny<Exception>(action);
}

[Fact]
public void ShouldThrowIfNullInMemoryCertIsProvided()
{
// Arrange
AuthContext appOnlyAuthContext = new AuthContext
{
AuthType = AuthenticationType.AppOnly,
ClientId = Guid.NewGuid().ToString(),
Certificate = null,
ContextScope = ContextScope.Process
};
// Act
Action action = () => AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);

//Assert
Assert.Throws<ArgumentNullException>(action);
}

/// <summary>
/// Create and Store a Self Signed Certificate
/// </summary>
/// <param name="certName"></param>
private static X509Certificate2 CreateAndStoreSelfSignedCert(string certName)
{
var cert = CreateSelfSignedCert(certName);
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
}

return cert;
}

/// <summary>
/// Create a Self Signed Certificate
/// </summary>
/// <param name="certName"></param>
/// <returns></returns>
private static X509Certificate2 CreateSelfSignedCert(string certName)
{
ECDsa ecdsaKey = ECDsa.Create();
CertificateRequest certificateRequest = new CertificateRequest(certName, ecdsaKey, HashAlgorithmName.SHA256);
Expand All @@ -108,14 +253,11 @@ private void CreateSelfSignedCert(string certName)
{
dummyCert = new X509Certificate2(cert.Export(X509ContentType.Pfx, "P@55w0rd"), "P@55w0rd", X509KeyStorageFlags.PersistKeySet);
}
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.Add(dummyCert);
}

return dummyCert;
}

private void DeleteSelfSignedCert(string certificateName)
private static void DeleteSelfSignedCertByName(string certificateName)
{
using (X509Store xStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
Expand All @@ -134,6 +276,25 @@ private void DeleteSelfSignedCert(string certificateName)
xStore.Remove(xCertificate);
}
}
private static void DeleteSelfSignedCertByThumbprint(string certificateThumbPrint)
{
using (X509Store xStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
xStore.Open(OpenFlags.ReadWrite);

X509Certificate2Collection unexpiredCerts = xStore.Certificates
.Find(X509FindType.FindByTimeValid, DateTime.Now, false)
.Find(X509FindType.FindByThumbprint, certificateThumbPrint, false);

// Only return current cert.
var xCertificate = unexpiredCerts
.OfType<X509Certificate2>()
.OrderByDescending(c => c.NotBefore)
.FirstOrDefault();

xStore.Remove(xCertificate);
}
}
#endif

}
Expand Down
13 changes: 9 additions & 4 deletions src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets
using System.Globalization;
using Microsoft.Graph.PowerShell.Authentication.Interfaces;
using Microsoft.Graph.PowerShell.Authentication.Common;
using System.Security.Cryptography.X509Certificates;

[Cmdlet(VerbsCommunications.Connect, "MgGraph", DefaultParameterSetName = Constants.UserParameterSet)]
[Alias("Connect-Graph")]
Expand Down Expand Up @@ -45,7 +46,7 @@ public class ConnectMgGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssem
Position = 3,
HelpMessage = "The thumbprint of your certificate. The Certificate will be retrieved from the current user's certificate store.")]
public string CertificateThumbprint { get; set; }

[Parameter(ParameterSetName = Constants.AccessTokenParameterSet,
Position = 1,
HelpMessage = "Specifies a bearer token for Microsoft Graph service. Access tokens do timeout and you'll have to handle their refresh.")]
Expand All @@ -69,6 +70,9 @@ public class ConnectMgGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssem
[Alias("EnvironmentName", "NationalCloud")]
public string Environment { get; set; }

[Parameter(ParameterSetName = Constants.AppParameterSet, Mandatory = false, HelpMessage = "An x509 Certificate supplied during invocation")]
public X509Certificate2 Certificate { get; set; }

private CancellationTokenSource cancellationTokenSource;

private IGraphEnvironment environment;
Expand Down Expand Up @@ -125,6 +129,7 @@ protected override void ProcessRecord()
authContext.ClientId = ClientId;
authContext.CertificateThumbprint = CertificateThumbprint;
authContext.CertificateName = CertificateName;
authContext.Certificate = Certificate;
// Default to Process but allow the customer to change this via `ContextScope` param.
authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.Process;
}
Expand Down Expand Up @@ -256,10 +261,10 @@ private void ValidateParameters()
this.ThrowParameterError(nameof(ClientId));
}

// Certificate Thumbprint or name
if (string.IsNullOrEmpty(CertificateThumbprint) && string.IsNullOrEmpty(CertificateName))
// Certificate Thumbprint, Name or Actual Certificate
if (string.IsNullOrEmpty(CertificateThumbprint) && string.IsNullOrEmpty(CertificateName) && this.Certificate == null)
{
this.ThrowParameterError($"{nameof(CertificateThumbprint)} or {nameof(CertificateName)}");
this.ThrowParameterError($"{nameof(CertificateThumbprint)} or {nameof(CertificateName)} or {nameof(Certificate)}");
}

// Tenant Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ namespace Microsoft.Graph.PowerShell.Authentication.Helpers
using Microsoft.Graph.PowerShell.Authentication.Models;
using Microsoft.Graph.PowerShell.Authentication.TokenCache;
using Microsoft.Identity.Client;

using System;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

using AuthenticationException = System.Security.Authentication.AuthenticationException;

internal static class AuthenticationHelpers
Expand All @@ -40,7 +42,8 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
.Build();

ConfigureTokenCache(publicClientApp.UserTokenCache, authContext);
authProvider = new DeviceCodeProvider(publicClientApp, authContext.Scopes, async (result) => {
authProvider = new DeviceCodeProvider(publicClientApp, authContext.Scopes, async (result) =>
{
await Console.Out.WriteLineAsync(result.Message);
});
break;
Expand All @@ -51,7 +54,7 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
.Create(authContext.ClientId)
.WithTenantId(authContext.TenantId)
.WithAuthority(authorityUrl)
.WithCertificate(string.IsNullOrEmpty(authContext.CertificateThumbprint) ? GetCertificateByName(authContext.CertificateName) : GetCertificateByThumbprint(authContext.CertificateThumbprint))
.WithCertificate(GetCertificate(authContext))
.Build();

ConfigureTokenCache(confidentialClientApp.AppTokenCache, authContext);
Expand All @@ -61,7 +64,8 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
}
case AuthenticationType.UserProvidedAccessToken:
{
authProvider = new DelegateAuthenticationProvider((requestMessage) => {
authProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
new NetworkCredential(string.Empty, GraphSession.Instance.UserProvidedToken).Password);
return Task.CompletedTask;
Expand All @@ -71,6 +75,35 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
}
return authProvider;
}
/// <summary>
/// Gets a certificate based on the current context.
/// Priority is Name, ThumbPrint, then In-Memory Cert
/// </summary>
/// <param name="context">Current <see cref="IAuthContext"/> context</param>
/// <returns>A <see cref="X509Certificate2"/> based on provided <see cref="IAuthContext"/> context</returns>
private static X509Certificate2 GetCertificate(IAuthContext context)
{
X509Certificate2 certificate;
if (!string.IsNullOrWhiteSpace(context.CertificateName))
{
certificate = GetCertificateByName(context.CertificateName);
}
else if (!string.IsNullOrWhiteSpace(context.CertificateThumbprint))
{
certificate = GetCertificateByThumbprint(context.CertificateThumbprint);
}
else
{
certificate = context.Certificate;
}

if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate), $"Certificate with the Specified ThumbPrint {context.CertificateThumbprint}, Name {context.CertificateName} or In-Memory could not be found");
}

return certificate;
}

private static string GetAuthorityUrl(IAuthContext authContext)
{
Expand Down Expand Up @@ -108,7 +141,8 @@ internal static void Logout(IAuthContext authConfig)

private static void ConfigureTokenCache(ITokenCache tokenCache, IAuthContext authContext)
{
tokenCache.SetBeforeAccess((TokenCacheNotificationArgs args) => {
tokenCache.SetBeforeAccess((TokenCacheNotificationArgs args) =>
{
try
{
_cacheLock.EnterReadLock();
Expand All @@ -120,7 +154,8 @@ private static void ConfigureTokenCache(ITokenCache tokenCache, IAuthContext aut
}
});

tokenCache.SetAfterAccess((TokenCacheNotificationArgs args) => {
tokenCache.SetAfterAccess((TokenCacheNotificationArgs args) =>
{
if (args.HasStateChanged)
{
try
Expand Down Expand Up @@ -164,8 +199,8 @@ private static X509Certificate2 GetCertificateByThumbprint(string CertificateThu
.FirstOrDefault();
}
return xCertificate;
}
}

/// <summary>
/// Gets unexpired certificate of the specified certificate subject name for the current user in My store..
/// </summary>
Expand All @@ -184,7 +219,7 @@ private static X509Certificate2 GetCertificateByName(string CertificateName)
.Find(X509FindType.FindByTimeValid, DateTime.Now, false)
.Find(X509FindType.FindBySubjectDistinguishedName, CertificateName, false);

if (unexpiredCerts == null)
if (unexpiredCerts.Count < 1)
throw new Exception($"{CertificateName} certificate was not found or has expired.");

// Only return current cert.
Expand Down
Loading