Last active
June 21, 2024 15:27
-
-
Save jborean93/586382429f869bdc1415d0ccb90db2e7 to your computer and use it in GitHub Desktop.
Get-Credential but with the modern Windows form - inspired from https://github.com/dopyrory3/Get-ModernCredential
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright: (c) 2021, Jordan Borean (@jborean93) <[email protected]> | |
# MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
Function Get-ModernCredential { | |
<# | |
.SYNOPSIS | |
Modern credential prompt. | |
.DESCRIPTION | |
Uses the modern Windows credential prompt to build a credential object. | |
.PARAMETER Message | |
The message to display in the credential prompt. Defaults to 'Enter your credentials.'. | |
.PARAMETER Title | |
The title to display in the credential prompt. Defaults to 'PowerShell credential request'. | |
.PARAMETER Username | |
Optional default user to prefill in the credential prompt. Can be combined with '-ForceUsername' to force this | |
username in the credential prompt. | |
.PARAMETER Win32Error | |
The Win32 error code as an integer or Win32Exception that can be used to automatically display an error message in | |
the credential prompt. By default (0) means do not display any error message. | |
.PARAMETER ForceUsername | |
Do not allow the caller to type in another username, must be set with '-Username'. | |
.PARAMETER ShowCurrentUser | |
Add the current user to the More choices pick list. | |
.EXAMPLE Get a credential supplied by the user | |
$cred = Get-ModernCredential | |
.EXAMPLE Get the credential for 'username' | |
$cred = Get-ModernCredential -Username username | |
.EXAMPLE Get the credential for only 'username' | |
$cred = Get-ModernCredential -Username username -ForceUsername | |
.EXAMPLE Display error message from previous credential attempt | |
# 5 -eq ERROR_ACCESS_DENIED | |
$cred = Get-ModernCredential -Win32Error 5 | |
.NOTES | |
This only works on Windows for both Windows PowerShell and PowerShell. | |
#> | |
[CmdletBinding()] | |
[OutputType([PSCredential])] | |
param ( | |
[Parameter()] | |
[String] | |
$Message = 'Enter your credentials.', | |
[Parameter()] | |
[String] | |
$Title = 'PowerShell credential request', | |
[Parameter()] | |
[AllowEmptyString()] | |
[String] | |
$Username, | |
[Parameter()] | |
[Object] | |
$Win32Error = 0, | |
[Switch] | |
$ForceUsername, | |
[Switch] | |
$ShowCurrentUser | |
) | |
begin { | |
$addParams = @{} | |
$addTypeCommand = Get-Command -Name Add-Type | |
# CompilerParameters is used for Windows PowerShell only. | |
if ('CompilerParameters' -in $addTypeCommand.Parameters.Keys) { | |
$addParams.CompilerParameters = [CodeDom.Compiler.CompilerParameters]@{ | |
CompilerOptions = '/unsafe' | |
} | |
} | |
else { | |
$addParams.CompilerOptions = '/unsafe' | |
} | |
Add-Type @addParams -TypeDefinition @' | |
using System; | |
using System.Runtime.InteropServices; | |
using System.Security; | |
using System.Text; | |
namespace ModernPrompt | |
{ | |
public class NativeHelpers | |
{ | |
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] | |
public struct CREDUI_INFO | |
{ | |
public Int32 cbSize; | |
public IntPtr hwndParent; | |
public string pszMessageText; | |
public string pszCaptionText; | |
public IntPtr hbmBanner; | |
} | |
} | |
public class NativeMethods | |
{ | |
[DllImport("credui.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
public static extern bool CredPackAuthenticationBuffer( | |
Int32 dwFlags, | |
string pszUserName, | |
string pszPassword, | |
IntPtr pPackedCredentials, | |
ref Int32 pcbPackedCredentials); | |
[DllImport("credui.dll", CharSet = CharSet.Unicode)] | |
public static extern Int32 CredUIPromptForWindowsCredentials( | |
ref NativeHelpers.CREDUI_INFO pUiInfo, | |
Int32 dwAuthError, | |
ref uint pulAuthPackage, | |
IntPtr pvInAuthBuffer, | |
uint ulInAuthBufferSize, | |
out IntPtr ppvOutAuthBuffer, | |
out uint pulOutAuthBufferSize, | |
ref bool pfSave, | |
Int32 dwFlags); | |
[DllImport("credui.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
public static extern bool CredUnPackAuthenticationBuffer( | |
Int32 dwFlags, | |
IntPtr pAuthBuffer, | |
uint cbAuthBuffer, | |
StringBuilder pszUserName, | |
ref Int32 pcchMaxUserName, | |
StringBuilder pszDomainName, | |
ref Int32 pcchMaxDomainame, | |
IntPtr pszPassword, | |
ref Int32 pcchMaxPassword); | |
[DllImport("Ole32.dll")] | |
public static extern void CoTaskMemFree( | |
IntPtr pv); | |
[DllImport("Kernel32.dll")] | |
public static extern IntPtr GetConsoleWindow(); | |
public static SecureString PtrToSecureStringUni(IntPtr buffer, int length) | |
{ | |
unsafe | |
{ | |
char *charPtr = (char *)buffer; | |
return new SecureString(charPtr, length); | |
} | |
} | |
} | |
} | |
'@ | |
$credUI = [ModernPrompt.NativeHelpers+CREDUI_INFO]::new() | |
$credUI.hwndParent = [ModernPrompt.NativeMethods]::GetConsoleWindow() | |
$credUI.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf([type][ModernPrompt.NativeHelpers+CREDUI_INFO]) | |
$credUI.pszMessageText = $Message | |
$credUI.pszCaptionText = $Title | |
$ERROR_INSUFFICIENT_BUFFER = 0x0000007A | |
$ERROR_CANCELLED = 0x00004C7 | |
if ($Win32Error) { | |
if ($Win32Error -is [ComponentModel.Win32Exception]) { | |
$Win32Error = $Win32Error.NativeErrorCode | |
} | |
} | |
} | |
end { | |
$inCredBufferSize = 0 | |
$inCredBuffer = [IntPtr]::Zero | |
$outCredBufferSize = 0 | |
$outCredBuffer = [IntPtr]::Zero | |
try { | |
# If a default username is specified we need to specify an in credential buffer with that name | |
if (-not [String]::IsNullOrWhiteSpace($Username)) { | |
while ($true) { | |
$res = [ModernPrompt.NativeMethods]::CredPackAuthenticationBuffer( | |
0, | |
$Username, | |
'', | |
$inCredBuffer, | |
[ref]$inCredBufferSize | |
); $err = [Runtime.InteropServices.Marshal]::GetLastWin32Error() | |
if ($res) { | |
break | |
} | |
elseif ($err -eq $ERROR_INSUFFICIENT_BUFFER) { | |
$inCredBuffer = [Runtime.InteropServices.Marshal]::AllocHGlobal($inCredBufferSize) | |
} | |
else { | |
$exp = [ComponentModel.Win32Exception]$err | |
Write-Error -Message "Failed to pack input username: $($exp.Message)" -Exception $exp | |
return | |
} | |
} | |
} | |
$authPackage = 0 | |
$save = $false | |
$flags = 0 | |
if ($ForceUsername) { | |
$flags = $flags -bor 0x20 # CREDUIWIN_IN_CRED_ONLY | |
} | |
if ($ShowCurrentUser) { | |
$flags = $flags -bor 0x200 # CREDUIWIN_ENUMERATE_CURRENT_USER | |
} | |
$err = [ModernPrompt.NativeMethods]::CredUIPromptForWindowsCredentials( | |
[ref]$credUI, | |
$Win32Error, | |
[ref]$authPackage, | |
$inCredBuffer, | |
$inCredBufferSize, | |
[ref]$outCredBuffer, | |
[ref]$outCredBufferSize, | |
[ref]$save, | |
$flags | |
) | |
if ($err -eq $ERROR_CANCELLED) { | |
return # No credential was specified | |
} | |
elseif ($err) { | |
$exp = [ComponentModel.Win32Exception]$err | |
Write-Error -Message "Failed to prompt for credential: $($exp.Message)" -Exception $exp | |
return | |
} | |
$usernameLength = 0 | |
$domainLength = 0 | |
$passwordLength = 0 | |
$usernameBuffer = [Text.StringBuilder]::new(0) | |
$domainBuffer = [Text.StringBuilder]::new(0) | |
$passwordPtr = [IntPtr]::Zero | |
try { | |
while ($true) { | |
$res = [ModernPrompt.NativeMethods]::CredUnpackAuthenticationBuffer( | |
1, # CRED_PACK_PROTECTED_CREDENTIALS | |
$outCredBuffer, | |
$outCredBufferSize, | |
$usernameBuffer, | |
[ref]$usernameLength, | |
$domainBuffer, | |
[ref]$domainLength, | |
$passwordPtr, | |
[ref]$passwordLength | |
); $err = [Runtime.InteropServices.Marshal]::GetLastWin32Error() | |
if ($res) { | |
break | |
} | |
elseif ($err -eq $ERROR_INSUFFICIENT_BUFFER) { | |
[void]$usernameBuffer.EnsureCapacity($usernameLength) | |
[void]$domainBuffer.EnsureCapacity($domainLength) | |
$passwordPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($passwordLength * 2) | |
} | |
else { | |
$exp = [ComponentModel.Win32Exception]$err | |
Write-Error -Message "Failed to unpack credential: $($exp.Message)" -Exception $exp | |
return | |
} | |
} | |
# We want to avoid reading the password as a full string so use this "unsafe" method | |
$password = [ModernPrompt.NativeMethods]::PtrToSecureStringUni($passwordPtr, $passwordLength) | |
} | |
finally { | |
if ($passwordPtr -ne [IntPtr]::Zero) { | |
$blanks = [byte[]]::new($passwordLength * 2) # Char takes 2 bytes | |
[Runtime.InteropServices.Marshal]::Copy($blanks, 0, $passwordPtr, $blanks.Length) | |
[Runtime.InteropServices.Marshal]::FreeHGlobal($passwordPtr) | |
} | |
} | |
if ($domainLength) { | |
$credUsername = '{0}\{1}' -f ($domainBuffer.ToString(), $usernameBuffer.ToString()) | |
} | |
else { | |
$credUsername = $usernameBuffer.ToString() | |
} | |
[PSCredential]::new($credUsername, $password) | |
} | |
finally { | |
if ($outCredBuffer -ne [IntPtr]::Zero) { | |
# Should be calling SecureZeroMemory but we cannot access this in .NET so do the next best thing | |
# and wipe the unmanaged memory ourselves. | |
$blanks = [byte[]]::new($outCredBufferSize) | |
[Runtime.InteropServices.Marshal]::Copy($blanks, 0, $outCredBuffer, $blanks.Length) | |
[ModernPrompt.NativeMethods]::CoTaskMemFree($outCredBuffer) | |
} | |
if ($inCredBuffer -ne [IntPtr]::Zero) { | |
[Runtime.InteropServices.Marshal]::FreeHGlobal($inCredBuffer) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I can't use this with
Connect-AzureAD
:Connect-AzureAD : Unsupported User Type 'Unknown'. Please see https://aka.ms/msal-net-up.