From 49d7ba1d46196c9a98fe340942dbe3ac515fa779 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 31 May 2026 00:43:37 +0300 Subject: [PATCH 1/2] Enhance documentation and functionality to support Linux Secret Service for managing App Ids across platforms --- documentation/Get-PnPManagedAppId.md | 4 +- documentation/Remove-PnPManagedAppId.md | 2 +- documentation/Set-PnPManagedAppId.md | 6 +- pages/articles/defaultclientid.md | 2 +- src/Commands/Utilities/CredentialManager.cs | 181 ++++++++++++++------ 5 files changed, 135 insertions(+), 60 deletions(-) diff --git a/documentation/Get-PnPManagedAppId.md b/documentation/Get-PnPManagedAppId.md index 3b9a3994c8..de6730df2e 100644 --- a/documentation/Get-PnPManagedAppId.md +++ b/documentation/Get-PnPManagedAppId.md @@ -10,7 +10,7 @@ online version: https://pnp.github.io/powershell/cmdlets/Get-PnPManagedAppId.htm # Get-PnPManagedAppId ## SYNOPSIS -Retrieve an App Id associated with a Url from either the Windows Credential Manager, the MacOS Key chain or if you use the Microsoft.PowerShell.SecretManagement module, a default vault. +Retrieve an App Id associated with a Url from the Windows Credential Manager, MacOS Key chain, Linux Secret Service, or if you use the Microsoft.PowerShell.SecretManagement module, a default vault. ## SYNTAX @@ -19,7 +19,7 @@ Get-PnPManagedAppId -Url ``` ## DESCRIPTION -Returns an associated App Id from the Windows Credential Manager or Mac OS Key Chain Entry. +Returns an associated App Id from the Windows Credential Manager, Mac OS Key Chain Entry, Linux Secret Service, or a default SecretManagement vault. ## EXAMPLES diff --git a/documentation/Remove-PnPManagedAppId.md b/documentation/Remove-PnPManagedAppId.md index a1ac6b745d..b90dd48381 100644 --- a/documentation/Remove-PnPManagedAppId.md +++ b/documentation/Remove-PnPManagedAppId.md @@ -19,7 +19,7 @@ Remove-PnPManagedAppId -Url [-Force] ``` ## DESCRIPTION -Removes an App Id from the Credential Manager +Removes an App Id from the Windows Credential Manager, Mac OS Key Chain Entry, Linux Secret Service, or a default SecretManagement vault. ## EXAMPLES diff --git a/documentation/Set-PnPManagedAppId.md b/documentation/Set-PnPManagedAppId.md index 221fdbf8ac..92eed0ae0f 100644 --- a/documentation/Set-PnPManagedAppId.md +++ b/documentation/Set-PnPManagedAppId.md @@ -10,7 +10,7 @@ title: Set-PnPManagedAppId # Set-PnPManagedAppId ## SYNOPSIS -Sets/Adds an App Id for use with Connect-PnPOnline to the Windows Credential Manager or Mac OS Key Chain Entry. If you the PowerShell Module Microsoft.PowerShell.SecretsStore and Microsoft.PowerShell.SecretsManagement installed and you have defined a default vault without a password than that will be used to store the App Id. +Sets/Adds an App Id for use with Connect-PnPOnline to the Windows Credential Manager, Mac OS Key Chain, or Linux Secret Service. If you have the PowerShell Module Microsoft.PowerShell.SecretManagement installed and you have defined a default vault without a password, that will be used to store the App Id. ## SYNTAX @@ -20,7 +20,7 @@ Set-PnPManagedAppId -Url -AppId [-Overwrite] ``` ## DESCRIPTION -Adds an App Id entry to the Windows Credential Manager or Mac OS Key Chain Entry. PnP PowerShell will check if an App Id is available when you connect using Connect-PnPOnline -Interactive. If it finds a matching URL it will use the associated App Id. You do not need to specify the -ClientId parameter then. +Adds an App Id entry to the Windows Credential Manager, Mac OS Key Chain, Linux Secret Service, or a default SecretManagement vault. PnP PowerShell will check if an App Id is available when you connect using Connect-PnPOnline -Interactive. If it finds a matching URL it will use the associated App Id. You do not need to specify the -ClientId parameter then. If you add a Credential with a name of "https://yourtenant.sharepoint.com" it will find a match when you connect to "https://yourtenant.sharepoint.com" but also when you connect to "https://yourtenant.sharepoint.com/sites/demo1". Of course you can specify more granular entries, allow you to automatically provide App Ids for different URLs. @@ -49,7 +49,7 @@ Accept wildcard characters: False ``` ### -Overwrite -Use parameter to overwrite existing Mac OS Key Chain Entry. Not required on Windows. +Use parameter to overwrite existing Mac OS Key Chain Entry. Not required on Windows or Linux. ```yaml Type: SwitchParameter diff --git a/pages/articles/defaultclientid.md b/pages/articles/defaultclientid.md index bf2b78be71..a4c6802a15 100644 --- a/pages/articles/defaultclientid.md +++ b/pages/articles/defaultclientid.md @@ -17,7 +17,7 @@ To set a client id for tenant with url `https://yourtenant.sharepoint.com`, you Set-PnPManagedAppId -Url https://yourtenant.sharepoint.com -AppId f0e2b362-8973-4fc7-a293-3c73e2677e79 ``` -This will add an entry to your Windows Credential Manager or the MacOS keychain if your are on MacOS. Connect-PnPOnline will use this value to match the correct client id with the url you are connecting to and it is not needed use -ClientId anymore, e.g. +This will add an entry to your Windows Credential Manager, the MacOS keychain, or the Linux Secret Service. If you have configured a default Microsoft.PowerShell.SecretManagement vault, that vault will be used instead. Connect-PnPOnline will use this value to match the correct client id with the url you are connecting to and it is not needed use -ClientId anymore, e.g. ```powershell Connect-PnPOnline -Url https://yourtenant.sharepoint.com -Interactive diff --git a/src/Commands/Utilities/CredentialManager.cs b/src/Commands/Utilities/CredentialManager.cs index 4ac5d4f544..a497e0fc4a 100644 --- a/src/Commands/Utilities/CredentialManager.cs +++ b/src/Commands/Utilities/CredentialManager.cs @@ -2,6 +2,8 @@ using Microsoft.Win32.SafeHandles; using PnP.Framework.Modernization.Cache; using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -9,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography; using System.Text; using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; @@ -17,6 +20,9 @@ namespace PnP.PowerShell.Commands.Utilities { internal static class CredentialManager { + private const string LinuxManagedAppIdSchemaName = "pnp.powershell.managedappid"; + private const string LinuxManagedAppIdSecretLabel = "PnP PowerShell managed App Id"; + private const string LinuxManagedAppIdCacheDirectory = ".m365pnppowershell"; public static bool AddCredential(string name, string username, SecureString password, bool overwrite) @@ -54,27 +60,27 @@ public static bool AddAppId(string name, string appid, bool overwrite) { name = $"PnPPSAppId:{name}"; } - if (HasSecretManagement()) - { - var defaultVault = GetDefaultVault(); - if (!string.IsNullOrEmpty(defaultVault)) - { - AddVaultAppId(defaultVault, name, appid); - } + var defaultVault = GetDefaultVaultIfAvailable(); + if (!string.IsNullOrEmpty(defaultVault)) + { + AddVaultAppId(defaultVault, name, appid); + return true; } - else + + var secureAppId = new NetworkCredential(null, appid).SecurePassword; + if (OperatingSystem.IsWindows()) { - var secureAppId = new NetworkCredential(null, appid).SecurePassword; - if (OperatingSystem.IsWindows()) - { - WriteWindowsCredentialManagerEntry(name, null, secureAppId); - } - else if (OperatingSystem.IsMacOS()) - { - WriteMacOSKeyChainEntry(name, appid); - } + WriteWindowsCredentialManagerEntry(name, null, secureAppId); + } + else if (OperatingSystem.IsMacOS()) + { + WriteMacOSKeyChainEntry(name, appid); + } + else if (OperatingSystem.IsLinux()) + { + WriteLinuxAppIdEntry(name, appid); } return true; } @@ -122,34 +128,32 @@ public static string GetAppId(string name) name = $"PnPPSAppId:{name}"; } // check if Microsoft.PowerShell.SecretManagement is available - if (HasSecretManagement()) + var defaultVault = GetDefaultVaultIfAvailable(); + if (!string.IsNullOrEmpty(defaultVault)) { - var defaultVault = GetDefaultVault(); + return GetVaultAppId(defaultVault, name); + } - if (!string.IsNullOrEmpty(defaultVault)) + if (OperatingSystem.IsWindows()) + { + var cred = ReadWindowsCredentialManagerEntry(name); + if (cred != null) { - return GetVaultAppId(defaultVault, name); + return SecureStringToString(cred.Password); } } - else + if (OperatingSystem.IsMacOS()) { - if (OperatingSystem.IsWindows()) - { - var cred = ReadWindowsCredentialManagerEntry(name); - if (cred != null) - { - return SecureStringToString(cred.Password); - } - } - if (OperatingSystem.IsMacOS()) + var cred = ReadMacOSKeyChainEntry(name); + if (cred != null) { - var cred = ReadMacOSKeyChainEntry(name); - if (cred != null) - { - return SecureStringToString(cred.Password).Trim('"'); - } + return SecureStringToString(cred.Password).Trim('"'); } } + if (OperatingSystem.IsLinux()) + { + return ReadLinuxAppIdEntry(name); + } return null; } @@ -198,27 +202,26 @@ public static bool RemoveAppid(string name) } bool success = false; - if (HasSecretManagement()) + var defaultVault = GetDefaultVaultIfAvailable(); + if (!string.IsNullOrEmpty(defaultVault)) { - var defaultVault = GetDefaultVault(); + RemoveVaultCredential(defaultVault, name); + return true; + } - if (!string.IsNullOrEmpty(defaultVault)) - { - RemoveVaultCredential(defaultVault, name); - return true; - } + if (OperatingSystem.IsWindows()) + { + success = DeleteWindowsCredentialManagerEntry(name); } - else + if (OperatingSystem.IsMacOS()) { - if (OperatingSystem.IsWindows()) - { - success = DeleteWindowsCredentialManagerEntry(name); - } - if (OperatingSystem.IsMacOS()) - { - success = DeleteMacOSKeyChainEntry(name); - return success; - } + success = DeleteMacOSKeyChainEntry(name); + return success; + } + if (OperatingSystem.IsLinux()) + { + success = DeleteLinuxAppIdEntry(name); + return success; } return success; } @@ -248,6 +251,16 @@ private static bool HasSecretManagement() } return false; } + + private static string GetDefaultVaultIfAvailable() + { + if (HasSecretManagement()) + { + return GetDefaultVault(); + } + return null; + } + private static string GetDefaultVault() { var defaultVaultName = ""; @@ -514,6 +527,68 @@ private static bool DeleteMacOSKeyChainEntry(string name) // return success; } + private static Storage CreateLinuxManagedAppIdStorage(string name) + { + var cacheDir = Path.Combine(MsalCacheHelper.UserRootDirectory, LinuxManagedAppIdCacheDirectory); + var cacheFileName = $"pnp.managedappid.{GetSha256Hash(name)}.cache"; + + var properties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDir) + .WithLinuxKeyring( + schemaName: LinuxManagedAppIdSchemaName, + collection: MsalCacheHelper.LinuxKeyRingDefaultCollection, + secretLabel: LinuxManagedAppIdSecretLabel, + attribute1: new KeyValuePair("Product", "PnPPowerShell"), + attribute2: new KeyValuePair("Name", name)) + .Build(); + + return Storage.Create(properties); + } + + private static void WriteLinuxAppIdEntry(string name, string appId) + { + var storage = CreateLinuxManagedAppIdStorage(name); + storage.VerifyPersistence(); + storage.WriteData(Encoding.UTF8.GetBytes(appId)); + } + + private static string ReadLinuxAppIdEntry(string name) + { + try + { + var data = CreateLinuxManagedAppIdStorage(name).ReadData(); + return data == null || data.Length == 0 ? null : Encoding.UTF8.GetString(data); + } + catch (MsalCachePersistenceException) + { + return null; + } + } + + private static bool DeleteLinuxAppIdEntry(string name) + { + try + { + var storage = CreateLinuxManagedAppIdStorage(name); + var data = storage.ReadData(); + if (data == null || data.Length == 0) + { + return false; + } + + storage.Clear(false); + return true; + } + catch (MsalCachePersistenceException) + { + return false; + } + } + + private static string GetSha256Hash(string value) + { + return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); + } + public static string SecureStringToString(SecureString value) { IntPtr valuePtr = IntPtr.Zero; From 8b26377aa701c5763fc4fdf40fb7160c23c2902a Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 31 May 2026 00:55:31 +0300 Subject: [PATCH 2/2] Enhance documentation and error handling for Linux Secret Service support in Get-PnPManagedAppId, Remove-PnPManagedAppId, and Set-PnPManagedAppId cmdlets --- documentation/Get-PnPManagedAppId.md | 4 ++-- documentation/Remove-PnPManagedAppId.md | 2 +- documentation/Set-PnPManagedAppId.md | 6 +++--- pages/articles/defaultclientid.md | 2 +- src/Commands/Utilities/CredentialManager.cs | 13 ++++++++++--- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/documentation/Get-PnPManagedAppId.md b/documentation/Get-PnPManagedAppId.md index de6730df2e..58c3749cf8 100644 --- a/documentation/Get-PnPManagedAppId.md +++ b/documentation/Get-PnPManagedAppId.md @@ -10,7 +10,7 @@ online version: https://pnp.github.io/powershell/cmdlets/Get-PnPManagedAppId.htm # Get-PnPManagedAppId ## SYNOPSIS -Retrieve an App Id associated with a Url from the Windows Credential Manager, MacOS Key chain, Linux Secret Service, or if you use the Microsoft.PowerShell.SecretManagement module, a default vault. +Retrieve an App Id associated with a URL from the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## SYNTAX @@ -19,7 +19,7 @@ Get-PnPManagedAppId -Url ``` ## DESCRIPTION -Returns an associated App Id from the Windows Credential Manager, Mac OS Key Chain Entry, Linux Secret Service, or a default SecretManagement vault. +Returns an associated App Id from the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## EXAMPLES diff --git a/documentation/Remove-PnPManagedAppId.md b/documentation/Remove-PnPManagedAppId.md index b90dd48381..e190ca857f 100644 --- a/documentation/Remove-PnPManagedAppId.md +++ b/documentation/Remove-PnPManagedAppId.md @@ -19,7 +19,7 @@ Remove-PnPManagedAppId -Url [-Force] ``` ## DESCRIPTION -Removes an App Id from the Windows Credential Manager, Mac OS Key Chain Entry, Linux Secret Service, or a default SecretManagement vault. +Removes an App Id from the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## EXAMPLES diff --git a/documentation/Set-PnPManagedAppId.md b/documentation/Set-PnPManagedAppId.md index 92eed0ae0f..d3cdd0d192 100644 --- a/documentation/Set-PnPManagedAppId.md +++ b/documentation/Set-PnPManagedAppId.md @@ -10,7 +10,7 @@ title: Set-PnPManagedAppId # Set-PnPManagedAppId ## SYNOPSIS -Sets/Adds an App Id for use with Connect-PnPOnline to the Windows Credential Manager, Mac OS Key Chain, or Linux Secret Service. If you have the PowerShell Module Microsoft.PowerShell.SecretManagement installed and you have defined a default vault without a password, that will be used to store the App Id. +Sets or adds an App Id for use with Connect-PnPOnline in the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. ## SYNTAX @@ -20,7 +20,7 @@ Set-PnPManagedAppId -Url -AppId [-Overwrite] ``` ## DESCRIPTION -Adds an App Id entry to the Windows Credential Manager, Mac OS Key Chain, Linux Secret Service, or a default SecretManagement vault. PnP PowerShell will check if an App Id is available when you connect using Connect-PnPOnline -Interactive. If it finds a matching URL it will use the associated App Id. You do not need to specify the -ClientId parameter then. +Adds an App Id entry to the Windows Credential Manager, macOS Keychain, Linux Secret Service, or a default vault configured through Microsoft.PowerShell.SecretManagement. PnP PowerShell will check if an App Id is available when you connect using Connect-PnPOnline -Interactive. If it finds a matching URL it will use the associated App Id. You do not need to specify the -ClientId parameter then. If you add a Credential with a name of "https://yourtenant.sharepoint.com" it will find a match when you connect to "https://yourtenant.sharepoint.com" but also when you connect to "https://yourtenant.sharepoint.com/sites/demo1". Of course you can specify more granular entries, allow you to automatically provide App Ids for different URLs. @@ -49,7 +49,7 @@ Accept wildcard characters: False ``` ### -Overwrite -Use parameter to overwrite existing Mac OS Key Chain Entry. Not required on Windows or Linux. +Use parameter to overwrite existing macOS Keychain Entry. Not required on Windows or Linux. ```yaml Type: SwitchParameter diff --git a/pages/articles/defaultclientid.md b/pages/articles/defaultclientid.md index a4c6802a15..f5e1a8289c 100644 --- a/pages/articles/defaultclientid.md +++ b/pages/articles/defaultclientid.md @@ -17,7 +17,7 @@ To set a client id for tenant with url `https://yourtenant.sharepoint.com`, you Set-PnPManagedAppId -Url https://yourtenant.sharepoint.com -AppId f0e2b362-8973-4fc7-a293-3c73e2677e79 ``` -This will add an entry to your Windows Credential Manager, the MacOS keychain, or the Linux Secret Service. If you have configured a default Microsoft.PowerShell.SecretManagement vault, that vault will be used instead. Connect-PnPOnline will use this value to match the correct client id with the url you are connecting to and it is not needed use -ClientId anymore, e.g. +This will add an entry to your Windows Credential Manager, the macOS keychain, or the Linux Secret Service. If you have configured a default vault through Microsoft.PowerShell.SecretManagement, that vault will be used instead. Connect-PnPOnline will use this value to match the correct client ID with the URL you are connecting to, so you no longer need to pass -ClientId, e.g. ```powershell Connect-PnPOnline -Url https://yourtenant.sharepoint.com -Interactive diff --git a/src/Commands/Utilities/CredentialManager.cs b/src/Commands/Utilities/CredentialManager.cs index a497e0fc4a..2852f26772 100644 --- a/src/Commands/Utilities/CredentialManager.cs +++ b/src/Commands/Utilities/CredentialManager.cs @@ -546,9 +546,16 @@ private static Storage CreateLinuxManagedAppIdStorage(string name) private static void WriteLinuxAppIdEntry(string name, string appId) { - var storage = CreateLinuxManagedAppIdStorage(name); - storage.VerifyPersistence(); - storage.WriteData(Encoding.UTF8.GetBytes(appId)); + try + { + var storage = CreateLinuxManagedAppIdStorage(name); + storage.VerifyPersistence(); + storage.WriteData(Encoding.UTF8.GetBytes(appId)); + } + catch (MsalCachePersistenceException ex) + { + throw new InvalidOperationException("Unable to store the managed App Id in Linux Secret Service. Ensure a Secret Service provider such as GNOME Keyring or KWallet is installed and unlocked, or configure a default vault through Microsoft.PowerShell.SecretManagement.", ex); + } } private static string ReadLinuxAppIdEntry(string name)