Skip to content

Instantly share code, notes, and snippets.

@jborean93
Last active June 21, 2024 15:27
Show Gist options
  • Save jborean93/586382429f869bdc1415d0ccb90db2e7 to your computer and use it in GitHub Desktop.
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
# 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)
}
}
}
}
@BiatuAutMiahn
Copy link

I can't use this with Connect-AzureAD: Connect-AzureAD : Unsupported User Type 'Unknown'. Please see https://aka.ms/msal-net-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment