Created
November 28, 2024 18:06
-
-
Save SMSAgentSoftware/e3f38160d4ef048c8b95308975b96046 to your computer and use it in GitHub Desktop.
Example function for how to obtain an access token for Microsoft Entra using the Web Account Manager (WAM) broker in Windows
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
Function Get-EntraAccessTokenWithWAM { | |
[CmdletBinding()] | |
param ( | |
[Parameter()] | |
[string] | |
$ClientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e", # Microsoft Graph PowerShell | |
[Parameter()] | |
[string[]] | |
$Scopes = "https://graph.microsoft.com/.default", | |
[Parameter()] | |
[string] | |
$RedirectUri = "http://localhost" | |
) | |
# Makre sure we're running in PowerShell Core edition | |
If ($PSEdition -ne "Core") | |
{ | |
Write-Warning "This function is only supported in PowerShell Core" | |
return | |
} | |
# Check for Az.Accounts module | |
$AzAccountsModule = Get-Module -Name Az.Accounts -ListAvailable | |
If ($null -eq $AzAccountsModule) | |
{ | |
try | |
{ | |
Install-Module -Name Az.Accounts -Force -AllowClobber -Scope CurrentUser -Repository PSGallery -ErrorAction Stop | |
} | |
catch | |
{ | |
throw "Failed to install Az.Accounts module: $_" | |
} | |
} | |
# Import Az.Accounts module | |
try | |
{ | |
Import-Module Az.Accounts -ErrorAction Stop | |
} | |
catch | |
{ | |
throw $_ | |
} | |
# Find the location of the Azure.Common assembly | |
$LoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() | Select -ExpandProperty Location | |
$AzureCommon = $LoadedAssemblies | | |
Where-Object { $_ -match "\\Modules\\Az.Accounts\\" -and $_ -match "Microsoft.Azure.Common" } | |
$AzureCommonLocation = $AzureCommon.TrimEnd("Microsoft.Azure.Common.dll") | |
# Locate the required assemblies | |
$mima = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.IdentityModel.Abstractions.dll" -Recurse -File | Select -ExpandProperty FullName | |
$mic = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.Identity.Client.dll" -Recurse -File | Select -ExpandProperty FullName | |
$micb = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.Identity.Client.Broker.dll" -Recurse -File | Select -ExpandProperty FullName | |
$micni = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.Identity.Client.NativeInterop.dll" -Recurse -File | Select -ExpandProperty FullName | |
$micem = Get-ChildItem -Path $AzureCommonLocation -Filter "Microsoft.Identity.Client.Extensions.Msal.dll" -Recurse -File | Select -ExpandProperty FullName | |
$sscpd = Get-ChildItem -Path $AzureCommonLocation -Filter "System.Security.Cryptography.ProtectedData.dll" -Recurse -File | Select -ExpandProperty FullName | |
# This next one need to come from the .Net Core installation being used | |
$RuntimeFrameworkMajorVersion = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription.Split()[-1].Split(".")[0] | |
$dotNetDirectory = Get-ChildItem -Path "C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref" -Filter "$RuntimeFrameworkMajorVersion.*" -Directory | | |
Sort-Object -Property Name -Descending | Select -First 1 | |
$sdts = Get-ChildItem -Path $dotNetDirectory -Filter "System.Diagnostics.TraceSource.dll" -Recurse -File | Select -ExpandProperty FullName | |
# Load the assemblies | |
try | |
{ | |
[void][System.Reflection.Assembly]::LoadFrom($mima) | |
[void][System.Reflection.Assembly]::LoadFrom($mic) | |
[void][System.Reflection.Assembly]::LoadFrom($micb) | |
[void][System.Reflection.Assembly]::LoadFrom($micni) | |
[void][System.Reflection.Assembly]::LoadFrom($sscpd) | |
[void][System.Reflection.Assembly]::LoadFrom($sdts) | |
[void][System.Reflection.Assembly]::LoadFrom($micem) | |
} | |
catch | |
{ | |
throw $_ | |
} | |
# C# code to do the work since PS will not recognize the .WithBroker() extension method from the broker library | |
$code = @" | |
using System; | |
using System.IO; | |
using System.Linq; | |
using System.Runtime.InteropServices; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Microsoft.Identity.Client; | |
using Microsoft.Identity.Client.Broker; | |
using Microsoft.IdentityModel.Abstractions; | |
using Microsoft.Identity.Client.NativeInterop; | |
using Microsoft.Identity.Client.Extensions.Msal; | |
public class PublicClientAppHelper | |
{ | |
// This gets the window handle of the console window | |
[DllImport("user32.dll", ExactSpelling = true)] | |
public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags); | |
[DllImport("kernel32.dll")] | |
public static extern IntPtr GetConsoleWindow(); | |
public enum GetAncestorFlags | |
{ | |
GetParent = 1, | |
GetRoot = 2, | |
GetRootOwner = 3 | |
} | |
public static IntPtr GetConsoleOrTerminalWindow() | |
{ | |
IntPtr consoleHandle = GetConsoleWindow(); | |
IntPtr handle = GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner); | |
return handle; | |
} | |
// Setup a cache for the tokens | |
private static BrokerOptions brokerOptions = new BrokerOptions(BrokerOptions.OperatingSystems.Windows); | |
private static string cacheFileName = "msalcache.bin"; | |
private static string cacheFilePath = Path.Combine(MsalCacheHelper.UserRootDirectory, cacheFileName); | |
private static string cacheDir = Path.GetDirectoryName(cacheFilePath); | |
private static StorageCreationProperties storageProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDir).Build(); | |
// Method for retrieving the access token | |
public static async Task<string> GetAccessTokenWithWAM(string clientId, string redirectUri, string[] scopes) | |
{ | |
IPublicClientApplication publicClientApp = PublicClientApplicationBuilder.Create(clientId) | |
.WithBroker(brokerOptions) | |
.WithParentActivityOrWindow(GetConsoleOrTerminalWindow) | |
.WithRedirectUri(redirectUri) | |
.Build(); | |
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties).ConfigureAwait(false); | |
cacheHelper.RegisterCache(publicClientApp.UserTokenCache); | |
var accounts = await publicClientApp.GetAccountsAsync(); | |
var existingAccount = accounts.FirstOrDefault(); | |
AuthenticationResult result; | |
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(90)); // Automatic cancellation after 90 seconds | |
// try the cache first, fallback to interactive if necessary | |
try | |
{ | |
result = existingAccount != null | |
? await publicClientApp.AcquireTokenSilent(scopes, existingAccount).ExecuteAsync(cancellationTokenSource.Token) | |
: await publicClientApp.AcquireTokenInteractive(scopes).ExecuteAsync(cancellationTokenSource.Token); | |
} | |
catch (MsalUiRequiredException) | |
{ | |
result = await publicClientApp.AcquireTokenInteractive(scopes).ExecuteAsync(cancellationTokenSource.Token); | |
} | |
return result.AccessToken; | |
} | |
} | |
"@ | |
# List of assemblies we need to reference | |
$assemblies = @($mima, $mic, $micb, $micni, $micem, $sscpd, $sdts, "netstandard", "System.Linq") | |
# Get the access token | |
try | |
{ | |
# If the type already exists in the current session | |
$token = [PublicClientAppHelper]::GetAccessTokenWithWAM($ClientId, $RedirectUri, $Scopes).GetAwaiter().GetResult() | |
return $token | |
} | |
catch | |
{ | |
if ($_.FullyQualifiedErrorId -eq "TypeNotFound") | |
{ | |
try | |
{ | |
# Add the type if it doesn't exist yet | |
Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $code -Language CSharp -ErrorAction Stop | |
$token = [PublicClientAppHelper]::GetAccessTokenWithWAM($ClientId, $RedirectUri, $Scopes).GetAwaiter().GetResult() | |
return $token | |
} | |
catch | |
{ | |
throw $_ | |
} | |
} | |
else | |
{ | |
throw $_ | |
} | |
} | |
} | |
## Example usage ## | |
# Obtain an access token for the Microsoft Graph API using the Microsoft Graph PowerShell SDK app | |
Get-EntraAccessTokenWithWAM | |
# Obtain an access token for the Azure SQL Database using the Azure CLI app | |
Get-EntraAccessTokenWithWAM -Scopes "https://database.windows.net/.default" -ClientId "04b07795-8ddb-461a-bbee-02f9e1bf7b46" | |
# Obtain an access token for the Azure SQL Database using the Azure PowerShell app | |
Get-EntraAccessTokenWithWAM -Scopes "https://database.windows.net/.default" -ClientId "1950a258-227b-4e31-a9cf-717495945fc2" |
Getting below error when trying: Exception: C:\temp\Get-EntraAccessTokenWithWAM.ps1:73 Line | 73 | [void][System.Reflection.Assembly]::LoadFrom($sdts) | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | Exception calling "LoadFrom" with "1" argument(s): "Value cannot be null. (Parameter 'assemblyFile')"
Check lines 60-63 - make sure it can find "System.Diagnostics.TraceSource.dll" in your .Net core install directory
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Getting below error when trying:
Exception: C:\temp\Get-EntraAccessTokenWithWAM.ps1:73
Line |
73 | [void][System.Reflection.Assembly]::LoadFrom($sdts)
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Exception calling "LoadFrom" with "1" argument(s): "Value cannot be null. (Parameter 'assemblyFile')"