Skip to content

Instantly share code, notes, and snippets.

@SMSAgentSoftware
Created December 5, 2024 16:07
Show Gist options
  • Save SMSAgentSoftware/e0737d683d4301767362c2a9587fd09e to your computer and use it in GitHub Desktop.
Save SMSAgentSoftware/e0737d683d4301767362c2a9587fd09e to your computer and use it in GitHub Desktop.
PowerShell function providing a few options for obtaining an access token for Microsoft Entra
Function Get-EntraAccessToken {
[CmdletBinding(DefaultParameterSetName = 'Default')]
param (
[Parameter(ParameterSetName = "UseWAM")]
[Parameter(ParameterSetName = "GraphSDK")]
[Parameter(ParameterSetName = "ACRSClaim")]
[Parameter(ParameterSetName = "AzureIdentity")]
[Parameter(ParameterSetName = "Default")]
[string]
$ClientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e", # Microsoft Graph PowerShell
[Parameter(ParameterSetName = "GraphSDK")]
[Parameter(ParameterSetName = "ACRSClaim")]
[Parameter(ParameterSetName = "AzureIdentity")]
[Parameter(ParameterSetName = "Default")]
[string]
$TenantId = "<YourTenantId>",
[Parameter(ParameterSetName = "UseWAM")]
[Parameter(ParameterSetName = "GraphSDK")]
[Parameter(ParameterSetName = "ACRSClaim")]
[Parameter(ParameterSetName = "AzureIdentity")]
[Parameter(ParameterSetName = "Default")]
[string[]]
$Scopes = "https://graph.microsoft.com/.default",
[Parameter(ParameterSetName = "AzureIdentity")]
[Parameter(ParameterSetName = "UseWAM")]
[Parameter(ParameterSetName = "Default")]
[string]
$RedirectUri = "http://localhost",
[Parameter(ParameterSetName = "GraphSDK")]
[switch]
$UseGraphSDK,
[Parameter(ParameterSetName = "AzureIdentity")]
[switch]
$UseAzureIdentity,
[Parameter(ParameterSetName = "UseWAM")]
[switch]
$UseWAM,
[Parameter(ParameterSetName = "ACRSClaim")]
[Parameter(ParameterSetName = "Default")]
[switch]
$AddACRSClaim
)
if ($UseGraphSDK)
{
# Check for Microsoft.Graph.Authentication module
$MGAuthModule = Get-Module -Name Microsoft.Graph.Authentication -ListAvailable
If ($null -eq $MGAuthModule)
{
try
{
Install-Module -Name Microsoft.Graph.Authentication -Force -AllowClobber -Scope CurrentUser -Repository PSGallery -ErrorAction Stop
}
catch
{
throw "Failed to install Az.Accounts module: $_"
}
}
# Import Microsoft.Graph.Authentication module
try
{
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
}
catch
{
throw $_
}
try
{
Connect-MgGraph -NoWelcome -Scopes $Scopes -ClientId $ClientId -TenantId $TenantId -ErrorAction Stop
$token = (Invoke-MgGraphRequest -Method GET -Uri "/v1.0/me" -OutputType "HttpResponseMessage").RequestMessage.Headers.Authorization.Parameter
return $token
}
catch
{
throw $_
}
}
if ($AddACRSClaim)
{
# Define your application details
$redirectUri = "http://localhost:8080"
$claimValue = "c1" # The value of the authentication context claim from the conditional access policy
# Encode the additional claims
$additionalClaims = [ordered]@{"access_token" = [ordered]@{"acrs" = [ordered]@{"essential" = $true; "value" = $claimValue}}}
Add-Type -AssemblyName System.Web
$encodedClaims = [System.Web.HttpUtility]::UrlEncode(($additionalClaims | ConvertTo-Json -Compress))
# Generate the authorization URL
$authUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectUri&response_mode=query&scope=$scopes&claims=$encodedClaims"
# Start a local HTTP listener to capture the authorization code
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add($redirectUri + "/")
$listener.Start()
# Open the authorization URL in the default browser
Start-Process $authUrl
# Wait for the authorization response
$context = $listener.GetContext()
$response = $context.Response
$request = $context.Request
# Extract the authorization code from the query parameters
$authCode = $request.QueryString["code"]
if ($authCode)
{
$responseString = "Authorization code received. You can close this window."
}
else
{
$responseString = "No authorization code received. Please try again."
}
# Send a response to the browser
$buffer = [System.Text.Encoding]::UTF8.GetBytes($responseString)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
# Stop the listener
$listener.Stop()
$listener.Dispose()
# Get Access Token
$Body = @{
grant_type = 'authorization_code'
client_id = $ClientID
scope = $Scope
code = $AuthCode
redirect_uri = $RedirectUri
claims = '{"access_token":{"xms_cc":{"values":["cp1"]}}}' # Request the xms_cc optional claim with the value cp1
}
$Response = Invoke-RestMethod "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Method POST -Body $Body
return $Response.access_token
}
# 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 $_
}
if ($UseAzureIdentity)
{
# For PS Core, it is necessary to load the required assemblies
If ($PSEdition -eq "Core")
{
# 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")
# Load the required assemblies
$dllsToLoad = @(
'Microsoft.IdentityModel.Abstractions.dll'
'Microsoft.Identity.Client.dll'
'Azure.Identity.dll'
'Azure.Core.dll'
'Microsoft.Identity.Client.Extensions.Msal.dll'
)
foreach ($dll in $dllsToLoad)
{
try
{
$dllLocation = Get-ChildItem -Path $AzureCommonLocation -Filter $dll -Recurse -File | Select -First 1 -ExpandProperty FullName
[void][System.Reflection.Assembly]::LoadFrom($dllLocation)
}
catch
{
throw $_
}
}
}
# Set the credential options
$ibcOptions = [Azure.Identity.InteractiveBrowserCredentialOptions]::new()
$ibcOptions.ClientId = $ClientId
$ibcOptions.TenantId = $TenantId
$ibcOptions.RedirectUri = $RedirectUri
$ibcOptions.TokenCachePersistenceOptions = [Azure.Identity.TokenCachePersistenceOptions]::new()
if ($null -ne $script:authenticationRecord1976)
{
$ibcOptions.AuthenticationRecord = $authenticationRecord1976
}
# Acquire a token
$ibc = [Azure.Identity.InteractiveBrowserCredential]::new($ibcOptions)
$requestContext = [Azure.Core.TokenRequestContext]::new($Scopes)
$cancellationTokenSource = [System.Threading.CancellationTokenSource]::new([timespan]::FromSeconds(90)) # Automatic cancellation after 90 seconds
if ($null -eq $script:authenticationRecord1976)
{
try
{
$script:authenticationRecord1976 = $ibc.AuthenticateAsync($requestContext,$cancellationTokenSource.Token).GetAwaiter().GetResult()
}
catch
{
throw $_
}
}
return $ibc.GetTokenAsync($requestContext,$cancellationTokenSource.Token).GetAwaiter().GetResult().Token
}
if ($UseWAM)
{
# Make sure we're running in PowerShell Core edition
If ($PSEdition -ne "Core")
{
Write-Warning "This function is only supported in PowerShell Core"
return
}
# 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 $_
}
}
}
## default option - Microsoft.Identity.Client ##
# For PS Core, it is necessary to load the required assemblies
If ($PSEdition -eq "Core")
{
# 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")
# Load the required assemblies
$dllsToLoad = @(
'Microsoft.IdentityModel.Abstractions.dll'
'Microsoft.Identity.Client.dll'
'Microsoft.Identity.Client.Extensions.Msal.dll'
)
foreach ($dll in $dllsToLoad)
{
try
{
$dllLocation = Get-ChildItem -Path $AzureCommonLocation -Filter $dll -Recurse -File | Select -First 1 -ExpandProperty FullName
[void][System.Reflection.Assembly]::LoadFrom($dllLocation)
}
catch
{
throw $_
}
}
}
if ($AddACRSClaim)
{
$Capabilities = [string[]]@("cp1")
$script:publicClientApp1976 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).WithRedirectUri($RedirectUri).WithTenantId($TenantId).WithClientCapabilities($Capabilities).Build()
}
else
{
if ($null -eq $script:publicClientApp1976)
{
$script:publicClientApp1976 = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).WithRedirectUri($RedirectUri).WithTenantId($TenantId).Build()
}
}
# Alternate code to create / register a token cache on disk instead of in-memory cache
#$cacheFilePath = [System.IO.Path]::Combine([Microsoft.Identity.Client.Extensions.Msal.MsalCacheHelper]::UserRootDirectory, "msal.cache")
#$cacheFileName = [System.IO.Path]::GetFileName($cacheFilePath)
#$cacheDir = [System.IO.Path]::GetDirectoryName($cacheFilePath)
#$storageProperties = [Microsoft.Identity.Client.Extensions.Msal.StorageCreationPropertiesBuilder]::new($cacheFileName,$cacheDir,$ClientId).Build()
#$cacheHelper = [Microsoft.Identity.Client.Extensions.Msal.MsalCacheHelper]::CreateAsync($storageProperties).GetAwaiter().GetResult()
#$cacheHelper.RegisterCache($publicClientApp.UserTokenCache)
# Acquire a token
$cancellationTokenSource = [System.Threading.CancellationTokenSource]::new([timespan]::FromSeconds(90)) # Automatic cancellation after 90 seconds
if ($AddACRSClaim)
{
$Claims = [ordered]@{"access_token" = [ordered]@{"xms_cc" = [ordered]@{"values" = @("cp1")};"acrs" = [ordered]@{"essential" = $true;"value" = "c1"}}} | ConvertTo-Json -Depth 3 -Compress
$token = $publicClientApp1976.AcquireTokenInteractive($Scopes).WithClaims($Claims).ExecuteAsync($cancellationTokenSource.Token).GetAwaiter().GetResult().AccessToken
}
else
{
$account = $publicClientApp1976.GetAccountsAsync().GetAwaiter().GetResult() | Select -First 1
if ($account)
{
# Check the token cache first
try
{
$token = $publicClientApp1976.AcquireTokenSilent($Scopes,$account).ExecuteAsync($cancellationTokenSource.Token).GetAwaiter().GetResult().AccessToken
}
catch
{
$token = $publicClientApp1976.AcquireTokenInteractive($Scopes).ExecuteAsync($cancellationTokenSource.Token).GetAwaiter().GetResult().AccessToken
}
}
else
{
$token = $publicClientApp1976.AcquireTokenInteractive($Scopes).ExecuteAsync($cancellationTokenSource.Token).GetAwaiter().GetResult().AccessToken
}
}
return $token
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment