Skip to content

Commit f89a265

Browse files
georgendGeorgepeombwa
authored
Allow Connect-Graph to take an x509Certificate. (#440)
* Allow Connect-Graph to take an in-memory x509Certificate. * Create Precedence chain CertificateName -> CertificateThumbprint -> InMemory Certificate Co-authored-by: George <[email protected]> Co-authored-by: Peter Ombwa <[email protected]>
1 parent 832ef07 commit f89a265

File tree

5 files changed

+230
-22
lines changed

5 files changed

+230
-22
lines changed

src/Authentication/Authentication.Test/Helpers/AuthenticationHelpersTests.cs

Lines changed: 171 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
using Microsoft.Graph.Auth;
44
using Microsoft.Graph.PowerShell.Authentication;
55
using Microsoft.Graph.PowerShell.Authentication.Helpers;
6+
67
using System;
78
using System.Linq;
89
using System.Net;
910
using System.Net.Http;
1011
using System.Security.Cryptography;
1112
using System.Security.Cryptography.X509Certificates;
1213
using System.Threading.Tasks;
14+
1315
using Xunit;
1416
public class AuthenticationHelpersTests
1517
{
@@ -78,7 +80,7 @@ public void ShouldUseClientCredentialProviderWhenAppOnlyContextIsProvided()
7880
CertificateName = "cn=dummyCert",
7981
ContextScope = ContextScope.Process
8082
};
81-
CreateSelfSignedCert(appOnlyAuthContext.CertificateName);
83+
CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateName);
8284

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

8991
// reset
90-
DeleteSelfSignedCert(appOnlyAuthContext.CertificateName);
92+
DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateName);
93+
GraphSession.Reset();
94+
95+
}
96+
97+
[Fact]
98+
public void ShouldUseInMemoryCertificateWhenProvided()
99+
{
100+
// Arrange
101+
var certificate = CreateSelfSignedCert("cn=inmemorycert");
102+
AuthContext appOnlyAuthContext = new AuthContext
103+
{
104+
AuthType = AuthenticationType.AppOnly,
105+
ClientId = Guid.NewGuid().ToString(),
106+
Certificate = certificate,
107+
ContextScope = ContextScope.Process
108+
};
109+
// Act
110+
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);
111+
112+
// Assert
113+
Assert.IsType<ClientCredentialProvider>(authProvider);
114+
var clientCredentialProvider = (ClientCredentialProvider)authProvider;
115+
// Assert: That the certificate created and set above is the same as used here.
116+
Assert.Equal(clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate, certificate);
91117
GraphSession.Reset();
92-
93118
}
94119

95-
private void CreateSelfSignedCert(string certName)
120+
[Fact]
121+
public void ShouldUseCertNameInsteadOfPassedInCertificateWhenBothAreSpecified()
122+
{
123+
// Arrange
124+
var dummyCertName = "CN=dummycert";
125+
var inMemoryCertName = "CN=inmemorycert";
126+
CreateAndStoreSelfSignedCert(dummyCertName);
127+
var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName);
128+
AuthContext appOnlyAuthContext = new AuthContext
129+
{
130+
AuthType = AuthenticationType.AppOnly,
131+
ClientId = Guid.NewGuid().ToString(),
132+
CertificateName = dummyCertName,
133+
Certificate = inMemoryCertificate,
134+
ContextScope = ContextScope.Process
135+
};
136+
// Act
137+
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);
138+
139+
// Assert
140+
Assert.IsType<ClientCredentialProvider>(authProvider);
141+
var clientCredentialProvider = (ClientCredentialProvider)authProvider;
142+
// Assert: That the certificate used is dummycert, that is in the store
143+
Assert.NotEqual(inMemoryCertName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name);
144+
Assert.Equal(appOnlyAuthContext.CertificateName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name);
145+
146+
//CleanUp
147+
DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateName);
148+
GraphSession.Reset();
149+
}
150+
151+
[Fact]
152+
public void ShouldUseCertThumbPrintInsteadOfPassedInCertificateWhenBothAreSpecified()
153+
{
154+
// Arrange
155+
var dummyCertName = "CN=dummycert";
156+
var inMemoryCertName = "CN=inmemorycert";
157+
var storedDummyCertificate = CreateAndStoreSelfSignedCert(dummyCertName);
158+
var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName);
159+
AuthContext appOnlyAuthContext = new AuthContext
160+
{
161+
AuthType = AuthenticationType.AppOnly,
162+
ClientId = Guid.NewGuid().ToString(),
163+
CertificateThumbprint = storedDummyCertificate.Thumbprint,
164+
Certificate = inMemoryCertificate,
165+
ContextScope = ContextScope.Process
166+
};
167+
// Act
168+
IAuthenticationProvider authProvider = AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);
169+
170+
// Assert
171+
Assert.IsType<ClientCredentialProvider>(authProvider);
172+
var clientCredentialProvider = (ClientCredentialProvider)authProvider;
173+
// Assert: That the certificate used is dummycert (Thumbprint), that is in the store
174+
Assert.NotEqual(inMemoryCertName, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.SubjectName.Name);
175+
Assert.Equal(appOnlyAuthContext.CertificateThumbprint, clientCredentialProvider.ClientApplication.AppConfig.ClientCredentialCertificate.Thumbprint);
176+
177+
//CleanUp
178+
DeleteSelfSignedCertByThumbprint(appOnlyAuthContext.CertificateThumbprint);
179+
GraphSession.Reset();
180+
}
181+
182+
[Fact]
183+
public void ShouldThrowIfNonExistentCertNameIsProvided()
184+
{
185+
// Arrange
186+
var dummyCertName = "CN=NonExistingCert";
187+
AuthContext appOnlyAuthContext = new AuthContext
188+
{
189+
AuthType = AuthenticationType.AppOnly,
190+
ClientId = Guid.NewGuid().ToString(),
191+
CertificateName = dummyCertName,
192+
ContextScope = ContextScope.Process
193+
};
194+
// Act
195+
Action action = () => AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);
196+
197+
//Assert
198+
Assert.ThrowsAny<Exception>(action);
199+
}
200+
201+
[Fact]
202+
public void ShouldThrowIfNullInMemoryCertIsProvided()
203+
{
204+
// Arrange
205+
AuthContext appOnlyAuthContext = new AuthContext
206+
{
207+
AuthType = AuthenticationType.AppOnly,
208+
ClientId = Guid.NewGuid().ToString(),
209+
Certificate = null,
210+
ContextScope = ContextScope.Process
211+
};
212+
// Act
213+
Action action = () => AuthenticationHelpers.GetAuthProvider(appOnlyAuthContext);
214+
215+
//Assert
216+
Assert.Throws<ArgumentNullException>(action);
217+
}
218+
219+
/// <summary>
220+
/// Create and Store a Self Signed Certificate
221+
/// </summary>
222+
/// <param name="certName"></param>
223+
private static X509Certificate2 CreateAndStoreSelfSignedCert(string certName)
224+
{
225+
var cert = CreateSelfSignedCert(certName);
226+
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
227+
{
228+
store.Open(OpenFlags.ReadWrite);
229+
store.Add(cert);
230+
}
231+
232+
return cert;
233+
}
234+
235+
/// <summary>
236+
/// Create a Self Signed Certificate
237+
/// </summary>
238+
/// <param name="certName"></param>
239+
/// <returns></returns>
240+
private static X509Certificate2 CreateSelfSignedCert(string certName)
96241
{
97242
ECDsa ecdsaKey = ECDsa.Create();
98243
CertificateRequest certificateRequest = new CertificateRequest(certName, ecdsaKey, HashAlgorithmName.SHA256);
@@ -108,14 +253,11 @@ private void CreateSelfSignedCert(string certName)
108253
{
109254
dummyCert = new X509Certificate2(cert.Export(X509ContentType.Pfx, "P@55w0rd"), "P@55w0rd", X509KeyStorageFlags.PersistKeySet);
110255
}
111-
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
112-
{
113-
store.Open(OpenFlags.ReadWrite);
114-
store.Add(dummyCert);
115-
}
256+
257+
return dummyCert;
116258
}
117259

118-
private void DeleteSelfSignedCert(string certificateName)
260+
private static void DeleteSelfSignedCertByName(string certificateName)
119261
{
120262
using (X509Store xStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
121263
{
@@ -134,6 +276,25 @@ private void DeleteSelfSignedCert(string certificateName)
134276
xStore.Remove(xCertificate);
135277
}
136278
}
279+
private static void DeleteSelfSignedCertByThumbprint(string certificateThumbPrint)
280+
{
281+
using (X509Store xStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
282+
{
283+
xStore.Open(OpenFlags.ReadWrite);
284+
285+
X509Certificate2Collection unexpiredCerts = xStore.Certificates
286+
.Find(X509FindType.FindByTimeValid, DateTime.Now, false)
287+
.Find(X509FindType.FindByThumbprint, certificateThumbPrint, false);
288+
289+
// Only return current cert.
290+
var xCertificate = unexpiredCerts
291+
.OfType<X509Certificate2>()
292+
.OrderByDescending(c => c.NotBefore)
293+
.FirstOrDefault();
294+
295+
xStore.Remove(xCertificate);
296+
}
297+
}
137298
#endif
138299

139300
}

src/Authentication/Authentication/Cmdlets/ConnectMgGraph.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets
1818
using System.Globalization;
1919
using Microsoft.Graph.PowerShell.Authentication.Interfaces;
2020
using Microsoft.Graph.PowerShell.Authentication.Common;
21+
using System.Security.Cryptography.X509Certificates;
2122

2223
[Cmdlet(VerbsCommunications.Connect, "MgGraph", DefaultParameterSetName = Constants.UserParameterSet)]
2324
[Alias("Connect-Graph")]
@@ -45,7 +46,7 @@ public class ConnectMgGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssem
4546
Position = 3,
4647
HelpMessage = "The thumbprint of your certificate. The Certificate will be retrieved from the current user's certificate store.")]
4748
public string CertificateThumbprint { get; set; }
48-
49+
4950
[Parameter(ParameterSetName = Constants.AccessTokenParameterSet,
5051
Position = 1,
5152
HelpMessage = "Specifies a bearer token for Microsoft Graph service. Access tokens do timeout and you'll have to handle their refresh.")]
@@ -69,6 +70,9 @@ public class ConnectMgGraph : PSCmdlet, IModuleAssemblyInitializer, IModuleAssem
6970
[Alias("EnvironmentName", "NationalCloud")]
7071
public string Environment { get; set; }
7172

73+
[Parameter(ParameterSetName = Constants.AppParameterSet, Mandatory = false, HelpMessage = "An x509 Certificate supplied during invocation")]
74+
public X509Certificate2 Certificate { get; set; }
75+
7276
private CancellationTokenSource cancellationTokenSource;
7377

7478
private IGraphEnvironment environment;
@@ -125,6 +129,7 @@ protected override void ProcessRecord()
125129
authContext.ClientId = ClientId;
126130
authContext.CertificateThumbprint = CertificateThumbprint;
127131
authContext.CertificateName = CertificateName;
132+
authContext.Certificate = Certificate;
128133
// Default to Process but allow the customer to change this via `ContextScope` param.
129134
authContext.ContextScope = this.IsParameterBound(nameof(ContextScope)) ? ContextScope : ContextScope.Process;
130135
}
@@ -256,10 +261,10 @@ private void ValidateParameters()
256261
this.ThrowParameterError(nameof(ClientId));
257262
}
258263

259-
// Certificate Thumbprint or name
260-
if (string.IsNullOrEmpty(CertificateThumbprint) && string.IsNullOrEmpty(CertificateName))
264+
// Certificate Thumbprint, Name or Actual Certificate
265+
if (string.IsNullOrEmpty(CertificateThumbprint) && string.IsNullOrEmpty(CertificateName) && this.Certificate == null)
261266
{
262-
this.ThrowParameterError($"{nameof(CertificateThumbprint)} or {nameof(CertificateName)}");
267+
this.ThrowParameterError($"{nameof(CertificateThumbprint)} or {nameof(CertificateName)} or {nameof(Certificate)}");
263268
}
264269

265270
// Tenant Id

src/Authentication/Authentication/Helpers/AuthenticationHelpers.cs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ namespace Microsoft.Graph.PowerShell.Authentication.Helpers
77
using Microsoft.Graph.PowerShell.Authentication.Models;
88
using Microsoft.Graph.PowerShell.Authentication.TokenCache;
99
using Microsoft.Identity.Client;
10+
1011
using System;
1112
using System.Linq;
1213
using System.Net;
1314
using System.Net.Http.Headers;
1415
using System.Security.Cryptography.X509Certificates;
1516
using System.Threading;
1617
using System.Threading.Tasks;
18+
1719
using AuthenticationException = System.Security.Authentication.AuthenticationException;
1820

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

4244
ConfigureTokenCache(publicClientApp.UserTokenCache, authContext);
43-
authProvider = new DeviceCodeProvider(publicClientApp, authContext.Scopes, async (result) => {
45+
authProvider = new DeviceCodeProvider(publicClientApp, authContext.Scopes, async (result) =>
46+
{
4447
await Console.Out.WriteLineAsync(result.Message);
4548
});
4649
break;
@@ -51,7 +54,7 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
5154
.Create(authContext.ClientId)
5255
.WithTenantId(authContext.TenantId)
5356
.WithAuthority(authorityUrl)
54-
.WithCertificate(string.IsNullOrEmpty(authContext.CertificateThumbprint) ? GetCertificateByName(authContext.CertificateName) : GetCertificateByThumbprint(authContext.CertificateThumbprint))
57+
.WithCertificate(GetCertificate(authContext))
5558
.Build();
5659

5760
ConfigureTokenCache(confidentialClientApp.AppTokenCache, authContext);
@@ -61,7 +64,8 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
6164
}
6265
case AuthenticationType.UserProvidedAccessToken:
6366
{
64-
authProvider = new DelegateAuthenticationProvider((requestMessage) => {
67+
authProvider = new DelegateAuthenticationProvider((requestMessage) =>
68+
{
6569
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer",
6670
new NetworkCredential(string.Empty, GraphSession.Instance.UserProvidedToken).Password);
6771
return Task.CompletedTask;
@@ -71,6 +75,35 @@ internal static IAuthenticationProvider GetAuthProvider(IAuthContext authContext
7175
}
7276
return authProvider;
7377
}
78+
/// <summary>
79+
/// Gets a certificate based on the current context.
80+
/// Priority is Name, ThumbPrint, then In-Memory Cert
81+
/// </summary>
82+
/// <param name="context">Current <see cref="IAuthContext"/> context</param>
83+
/// <returns>A <see cref="X509Certificate2"/> based on provided <see cref="IAuthContext"/> context</returns>
84+
private static X509Certificate2 GetCertificate(IAuthContext context)
85+
{
86+
X509Certificate2 certificate;
87+
if (!string.IsNullOrWhiteSpace(context.CertificateName))
88+
{
89+
certificate = GetCertificateByName(context.CertificateName);
90+
}
91+
else if (!string.IsNullOrWhiteSpace(context.CertificateThumbprint))
92+
{
93+
certificate = GetCertificateByThumbprint(context.CertificateThumbprint);
94+
}
95+
else
96+
{
97+
certificate = context.Certificate;
98+
}
99+
100+
if (certificate == null)
101+
{
102+
throw new ArgumentNullException(nameof(certificate), $"Certificate with the Specified ThumbPrint {context.CertificateThumbprint}, Name {context.CertificateName} or In-Memory could not be found");
103+
}
104+
105+
return certificate;
106+
}
74107

75108
private static string GetAuthorityUrl(IAuthContext authContext)
76109
{
@@ -108,7 +141,8 @@ internal static void Logout(IAuthContext authConfig)
108141

109142
private static void ConfigureTokenCache(ITokenCache tokenCache, IAuthContext authContext)
110143
{
111-
tokenCache.SetBeforeAccess((TokenCacheNotificationArgs args) => {
144+
tokenCache.SetBeforeAccess((TokenCacheNotificationArgs args) =>
145+
{
112146
try
113147
{
114148
_cacheLock.EnterReadLock();
@@ -120,7 +154,8 @@ private static void ConfigureTokenCache(ITokenCache tokenCache, IAuthContext aut
120154
}
121155
});
122156

123-
tokenCache.SetAfterAccess((TokenCacheNotificationArgs args) => {
157+
tokenCache.SetAfterAccess((TokenCacheNotificationArgs args) =>
158+
{
124159
if (args.HasStateChanged)
125160
{
126161
try
@@ -164,8 +199,8 @@ private static X509Certificate2 GetCertificateByThumbprint(string CertificateThu
164199
.FirstOrDefault();
165200
}
166201
return xCertificate;
167-
}
168-
202+
}
203+
169204
/// <summary>
170205
/// Gets unexpired certificate of the specified certificate subject name for the current user in My store..
171206
/// </summary>
@@ -184,7 +219,7 @@ private static X509Certificate2 GetCertificateByName(string CertificateName)
184219
.Find(X509FindType.FindByTimeValid, DateTime.Now, false)
185220
.Find(X509FindType.FindBySubjectDistinguishedName, CertificateName, false);
186221

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

190225
// Only return current cert.

0 commit comments

Comments
 (0)