From 491ea9fad24496c82309575eda12219e76024ac1 Mon Sep 17 00:00:00 2001 From: crcobb Date: Wed, 18 Sep 2024 11:09:42 -0400 Subject: [PATCH] Add ability to do cert based authentication and app supersedening (#118) * Fix issue with superseding app having been superseded. --------- Co-authored-by: Crcobb <1474340+crcobb@users.noreply.github.com> --- .../Commands/ConnectWtWinTuner.cs | 79 +++++++++++++++++++ .../Commands/RemoveWtWin32App.cs | 2 +- .../Svrooij.WinTuner.CmdLets.dll-Help.xml | 58 ++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/ConnectWtWinTuner.cs b/src/Svrooij.WinTuner.CmdLets/Commands/ConnectWtWinTuner.cs index c8d88e0..5a2a356 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/ConnectWtWinTuner.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/ConnectWtWinTuner.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Kiota.Abstractions; +using System.Security.Cryptography.X509Certificates; namespace Svrooij.WinTuner.CmdLets.Commands; /// @@ -33,6 +34,10 @@ namespace Svrooij.WinTuner.CmdLets.Commands; /// Let's say you have a token from another source, just hand us to token and we'll use it to connect to Intune. This token has a limited lifetime, so you'll be responsible for refreshing it. /// /// +/// ClientCertificateCredentials +/// Client credentials flow using a certificate in the user or local computer store.\r\n\r\nMake sure to mark the certificate as not exportable, this helps in keeping the certificate secure. +/// +/// /// ClientCredentials /// :::warning Last resort\r\nUsing client credentials is not recommended because you'll have to keep the secret, **secret**!\r\n\r\nPlease let us know if you have to use this method, we might be able to help you with a better solution.\r\n::: /// @@ -59,6 +64,7 @@ public class ConnectWtWinTuner : DependencyCmdlet private const string DefaultClientCredentialScope = "https://graph.microsoft.com/.default"; private const string ParamSetInteractive = "Interactive"; private const string ParamSetClientCredentials = "ClientCredentials"; + private const string ParamSetClientCertificateCredentials = "ClientCertificateCredentials"; /// /// Used default scopes @@ -144,6 +150,13 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the tenant ID. Loaded from `AZURE_TENANT_ID`")] + [Parameter( + Mandatory = true, + Position = 2, + ParameterSetName = ParamSetClientCertificateCredentials, + ValueFromPipeline = false, + ValueFromPipelineByPropertyName = false, + HelpMessage = "Specify the tenant ID. Loaded from `AZURE_TENANT_ID`")] public string? TenantId { get; set; } = Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); /// @@ -156,6 +169,13 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the client ID, mandatory for Client Credentials flow. Loaded from `AZURE_CLIENT_ID`")] + [Parameter( + Mandatory = true, + Position = 0, + ParameterSetName = ParamSetClientCertificateCredentials, + ValueFromPipeline = false, + ValueFromPipelineByPropertyName = false, + HelpMessage = "Specify the client ID, mandatory for Client Certificate flow. Loaded from `AZURE_CLIENT_ID`")] [Parameter( Mandatory = false, Position = 3, @@ -178,6 +198,17 @@ public class ConnectWtWinTuner : DependencyCmdlet HelpMessage = "Specify the client secret. Loaded from `AZURE_CLIENT_SECRET`")] public string? ClientSecret { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET"); + /// + /// Certificate Thumbprint for client authentication + /// + [Parameter( + Mandatory = true, + Position = 1, + ParameterSetName = ParamSetClientCertificateCredentials, + ValueFromPipeline = false, + HelpMessage = "Specify the thumbprint of the certificate. Loaded from `AZURE_CLIENT_CERT_THUMBPRINT`")] + public string? ClientCertificateThumbprint { get; set; } = Environment.GetEnvironmentVariable("AZURE_CLIENT_CERT_THUMBPRINT"); + /// /// Specify scopes to use /// @@ -202,6 +233,13 @@ public class ConnectWtWinTuner : DependencyCmdlet ValueFromPipeline = false, ValueFromPipelineByPropertyName = false, HelpMessage = "Specify the scopes to request, default is `https://graph.microsoft.com/.default`")] + [Parameter( + Mandatory = false, + Position = 10, + ParameterSetName = ParamSetClientCertificateCredentials, + ValueFromPipeline = false, + ValueFromPipelineByPropertyName = false, + HelpMessage = "Specify the scopes to request, default is `https://graph.microsoft.com/.default`")] [Parameter( Mandatory = false, Position = 10, @@ -278,6 +316,47 @@ private IAuthenticationProvider CreateAuthenticationProvider(CancellationToken c } } + if (ParameterSetName == ParamSetClientCertificateCredentials) + { + if (!string.IsNullOrEmpty(ClientId) && !string.IsNullOrEmpty(TenantId) && + !string.IsNullOrEmpty(ClientCertificateThumbprint)) + { + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + var certificate = store.Certificates.Cast().FirstOrDefault(cert => cert.Thumbprint == ClientCertificateThumbprint); + store.Close(); + if (certificate == null) + { + using var storeLocal = new X509Store(StoreName.My, StoreLocation.LocalMachine); + storeLocal.Open(OpenFlags.ReadOnly); + certificate = storeLocal.Certificates.Cast().FirstOrDefault(cert => cert.Thumbprint == ClientCertificateThumbprint); + storeLocal.Close(); + } + if (certificate == null) + { + throw new ArgumentException("Cannot find cert thumbprint in User or Machine store"); + } + + return new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider( + new Azure.Identity.ClientCertificateCredential(TenantId, ClientId, certificate, + new Azure.Identity.ClientCertificateCredentialOptions + { + TokenCachePersistenceOptions = new Azure.Identity.TokenCachePersistenceOptions + { + Name = "WinTuner-PowerShell-CC", + UnsafeAllowUnencryptedStorage = true, + } + } + ), isCaeEnabled: false, scopes: DefaultClientCredentialScope); + + } + else + { + throw new ArgumentException("Not all parameters for client certificate are specified", + nameof(ClientId)); + } + + } if (ParameterSetName == ParamSetInteractive) { if (NoBroker || RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) diff --git a/src/Svrooij.WinTuner.CmdLets/Commands/RemoveWtWin32App.cs b/src/Svrooij.WinTuner.CmdLets/Commands/RemoveWtWin32App.cs index 5ae689a..5b67277 100644 --- a/src/Svrooij.WinTuner.CmdLets/Commands/RemoveWtWin32App.cs +++ b/src/Svrooij.WinTuner.CmdLets/Commands/RemoveWtWin32App.cs @@ -58,7 +58,7 @@ protected override async Task ProcessAuthenticatedAsync(IAuthenticationProvider var parentRelationShips = await graphServiceClient.DeviceAppManagement.MobileApps[relationship.TargetId].Relationships.GetAsync(cancellationToken: cancellationToken); await graphServiceClient.DeviceAppManagement.MobileApps[relationship.TargetId].UpdateRelationships.PostAsync(new Microsoft.Graph.Beta.DeviceAppManagement.MobileApps.Item.UpdateRelationships.UpdateRelationshipsPostRequestBody { - Relationships = parentRelationShips?.Value?.Where(r => r.TargetId != AppId).ToList() ?? new List() + Relationships = parentRelationShips?.Value?.Where(r => r.TargetId != AppId && r.TargetType != Microsoft.Graph.Beta.Models.MobileAppRelationshipType.Parent).ToList() ?? new List() }, cancellationToken: cancellationToken); } diff --git a/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml b/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml index d01f046..1edf94b 100644 --- a/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml +++ b/src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.dll-Help.xml @@ -168,6 +168,53 @@ None + + Connect-WtWinTuner + + ClientId + + Specify the client ID, mandatory for Client Certificate flow. Loaded from `AZURE_CLIENT_ID` + + String + + String + + None + + + ClientCertificateThumbprint + + Specify the thumbprint of the certificate. Loaded from `AZURE_CLIENT_CERT_THUMBPRINT` + + String + + String + + None + + + TenantId + + Specify the tenant ID. Loaded from `AZURE_TENANT_ID` + + String + + String + + None + + + Scopes + + Specify the scopes to request, default is `https://graph.microsoft.com/.default` + + String[] + + String[] + + None + + Connect-WtWinTuner @@ -316,6 +363,17 @@ None + + ClientCertificateThumbprint + + Specify the thumbprint of the certificate. Loaded from `AZURE_CLIENT_CERT_THUMBPRINT` + + String + + String + + None + Scopes