Created
December 5, 2024 16:07
-
-
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
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-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