Skip to content

Instantly share code, notes, and snippets.

@keystroke
Last active April 25, 2024 22:03
Show Gist options
  • Save keystroke/5d54c3db4fe02ef6507daf97b9cc7bd8 to your computer and use it in GitHub Desktop.
Save keystroke/5d54c3db4fe02ef6507daf97b9cc7bd8 to your computer and use it in GitHub Desktop.
Self-contained example showing token APIs for AAD and ADFS.
# To import in your PowerShell runspace, run below command:
# iex ([System.Net.WebClient]@{}).DownloadString('https://gist.githubusercontent.com/keystroke/5d54c3db4fe02ef6507daf97b9cc7bd8/raw')
# To import in remote scriptblock, use below syntax:
# . ([scriptblock]::Create(([System.Net.WebClient]@{}).DownloadString('https://gist.githubusercontent.com/keystroke/5d54c3db4fe02ef6507daf97b9cc7bd8/raw')))
<#
.Synopsis
Gets an OAuth context, including tokens, from AAD or ADFS.
#>
function Get-ADContext
{
[CmdletBinding(DefaultParameterSetName='ByEnviromentAndInteraction')]
[OutputType([pscustomobject])]
param
(
# The target resource identifier for which to acquire a token. If not provided, will default to Microsoft Graph service.
[Parameter(ParameterSetName='ByAuthorityAndSecret')]
[Parameter(ParameterSetName='ByAuthorityAndCertificate')]
[Parameter(ParameterSetName='ByAuthorityAndCertificateReference')]
[Parameter(ParameterSetName='ByAuthorityAndCredential')]
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByAuthorityAndRefreshToken')]
[Parameter(ParameterSetName='ByEnviromentAndSecret')]
[Parameter(ParameterSetName='ByEnviromentAndCertificate')]
[Parameter(ParameterSetName='ByEnviromentAndCertificateReference')]
[Parameter(ParameterSetName='ByEnviromentAndCredential')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndRefreshToken')]
[ValidateNotNullOrEmpty()]
[string] $Resource = $null,
# The target identity system environment.
[Parameter(ParameterSetName='ByEnviromentAndSecret')]
[Parameter(ParameterSetName='ByEnviromentAndCertificate')]
[Parameter(ParameterSetName='ByEnviromentAndCertificateReference')]
[Parameter(ParameterSetName='ByEnviromentAndCredential')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndRefreshToken')]
[ValidateNotNullOrEmpty()]
[ValidateSet('ADFS', 'AzureChinaCloud', 'AzureCloud', 'AzureGermanCloud', 'AzureUSGovernment')]
[string] $IdentitySystem = 'AzureCloud',
# The directory tenant name, identifier, or verified domain.
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndSecret')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCertificate')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCertificateReference')]
[Parameter(ParameterSetName='ByEnviromentAndCredential')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndRefreshToken')]
[ValidateNotNullOrEmpty()]
[string] $DirectoryTenant = 'Common',
# The authority URI of the AD or ADFS identity system from which to acquire a token or authorization code.
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndSecret')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCertificate')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCertificateReference')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCredential')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndRefreshToken')]
[ValidateNotNull()]
[ValidateScript({ $_.IsAbsoluteUri -and $_.Scheme -ieq 'https' })]
[Uri] $AuthorityUri = $null,
# A user credential for which to acquire a token. Must support a non-interactive login flow.
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCredential')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCredential')]
[ValidateNotNull()]
[pscredential] $Credential = $null,
# The refresh token to use to acquire an access token targeting the specified resource.
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndRefreshToken')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndRefreshToken')]
[ValidateNotNull()]
[SecureString] $RefreshToken = $null,
# The identifier of the client identity application which is used to acquire a token.
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndSecret')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCertificate')]
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCertificateReference')]
[Parameter(ParameterSetName='ByAuthorityAndCredential')]
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByAuthorityAndRefreshToken')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndSecret')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCertificate')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCertificateReference')]
[Parameter(ParameterSetName='ByEnviromentAndCredential')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndRefreshToken')]
[ValidateNotNullOrEmpty()]
[string] $ClientId = '1950a258-227b-4e31-a9cf-717495945fc2',
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndSecret')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndSecret')]
[ValidateNotNullOrEmpty()]
[SecureString] $ClientSecret = $null,
# The certificate to use when authenticating as a service principal to acquire a token.
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCertificate')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCertificate')]
[ValidateNotNull()]
[ValidateScript({ $_.HasPrivateKey })]
[System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate = $null,
# The thumbprint of the certificate to use when authenticating as a service principal to acquire a token.
# Used in conjunction with the 'CertificateStoreLocation' and 'CertificateStoreName' parameters to locate the certificate.
[Parameter(Mandatory=$true, ParameterSetName='ByAuthorityAndCertificateReference')]
[Parameter(Mandatory=$true, ParameterSetName='ByEnviromentAndCertificateReference')]
[ValidateNotNullOrEmpty()]
[string] $CertificateThumbprint = $null,
# The certificate store location of the certificate to use when authenticating as a service principal to acquire a token.
# Used in conjunction with the 'CertificateThumbprint' and 'CertificateStoreName' parameters to locate the certificate.
[Parameter(ParameterSetName='ByAuthorityAndCertificateReference')]
[Parameter(ParameterSetName='ByEnviromentAndCertificateReference')]
[ValidateNotNull()]
[System.Security.Cryptography.X509Certificates.StoreLocation] $CertificateStoreLocation = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine,
# The certificate store name of the certificate to use when authenticating as a service principal to acquire a token.
# Used in conjunction with the 'CertificateThumbprint' and 'CertificateStoreLocation' parameters to locate the certificate.
[Parameter(ParameterSetName='ByAuthorityAndCertificateReference')]
[Parameter(ParameterSetName='ByEnviromentAndCertificateReference')]
[ValidateNotNull()]
[System.Security.Cryptography.X509Certificates.StoreName] $CertificateStoreName = [System.Security.Cryptography.X509Certificates.StoreName]::My,
# The redirect URI to use with an interactive authentication session.
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[ValidateNotNullOrEmpty()]
[string] $RedirectUri = 'urn:ietf:wg:oauth:2.0:oob',
# The prompt to use with an interactive authentication session. Some values are not valid in certain circumstances.
# login: The user should be prompted to reauthenticate.
# select_account: The user is prompted to select an account, interrupting single sign on. The user may select an existing signed-in account, enter their credentials for a remembered account, or choose to use a different account altogether.
# consent: User consent has been granted, but needs to be updated. The user should be prompted to consent.
# admin_consent: An administrator should be prompted to consent on behalf of all users in their organization
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[ValidateNotNullOrEmpty()]
[ValidateSet('login', 'select_account', 'consent', 'admin_consent')]
[string] $Prompt = 'select_account',
# A login hint to use with an interactive authentication session to pre-fill the username/email address field of the sign-in page.
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[ValidateNotNullOrEmpty()]
[string] $LoginHint = $null,
# A login hint to use with an interactive authentication session to suggest to the user which domain credential they should use to authenticate.
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[ValidateNotNullOrEmpty()]
[string] $DomainHint = $null,
# The scope value to use in the token requests.
[Parameter()]
[ValidateNotNullOrEmpty()]
[string] $Scope = 'openid',
# Indicates that the 'resource' parameter should not be sent token requests.
[Parameter()]
[switch] $ExcludeResource,
# Indicates that the 'PKCE' parameters should be sent in the token request.
[Parameter(ParameterSetName='ByAuthorityAndInteraction')]
[Parameter(ParameterSetName='ByEnviromentAndInteraction')]
[switch] $PKCE
)
begin
{
$originalErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
$defaultWebRequestParams = @{ UseBasicParsing = $true; TimeoutSec = 15 }
$token = $null
try
{
# Resolve target identity system endpoints and properties
if (-not $AuthorityUri)
{
$AuthorityUri = switch ($IdentitySystem)
{
'AzureChinaCloud' {"https://login.chinacloudapi.cn/$DirectoryTenant"}
'AzureCloud' {"https://login.microsoftonline.com/$DirectoryTenant"}
'AzureGermanCloud' {"https://login.microsoftonline.de/$DirectoryTenant"}
'AzureUSGovernment' {"https://login.microsoftonline.us/$DirectoryTenant"}
'ADFS' { throw [System.NotSupportedException]"To retrieve a token for ADFS, please use the 'AuthorityUri' parameter to provide the correct endpoint." }
Default { throw [System.NotSupportedException]"Identity system '$IdentitySystem' is not supported." }
}
$params = @{ Message = "'AuthorityUri' not provided; resolved to: $AuthorityUri" }
if ($DirectoryTenant -eq 'Common') { $params += @{ Verbose = $true } }
Write-Verbose @params
}
try
{
$endpoint = "$("$AuthorityUri".TrimEnd('/'))/.well-known/openid-configuration"
$response = Invoke-WebRequest @defaultWebRequestParams -Uri $endpoint
$openIdConfig = ConvertFrom-Json $response.Content
Write-Debug "Identity System openid-configuration: $([ordered]@{ endpoint = "$endpoint"; response = $openIdConfig } | ConvertTo-Json -Depth 1)"
}
catch
{
$errorOut = $_.Exception.Response | Select Method,ResponseUri,StatusCode,StatusDescription,IsFromCache,LastModified | ConvertTo-Json
$errorOut = "Failed to retrieve openid-configuration at endpoint '$endpoint': $_`r`n`r`nAdditional details: $errorOut"
throw [System.InvalidOperationException]$errorOut
}
if (-not $Resource -and -not $ExcludeResource)
{
# Note - ADFS does not include this claim
if (-not $openIdConfig.cloud_graph_host_name)
{
throw [System.InvalidOperationException]"'Resource' not provided, and no suitable default was resolved. Please try again by providing an explicit value for the 'Resource' parameter."
}
$Resource = "https://$($openIdConfig.cloud_graph_host_name)"
Write-Verbose "'Resource' not provided; resolved to: $Resource" -Verbose
}
# Prepare to retrieve token
$dependantAssembly = 'System.Web'
if (-not [System.Reflection.Assembly]::LoadWithPartialName($dependantAssembly))
{
throw [System.NotSupportedException]"Unable to load required assembly '$dependantAssembly' for processing query string parameters and performing URL encoding."
}
function ConvertTo-QueryString([HashTable]$QueryParameters=@{})
{
$query = [System.Web.HttpUtility]::ParseQueryString("?")
$QueryParameters.GetEnumerator() | ForEach { $query.Add($_.Key, $_.Value) }
return $query.ToString()
}
function Get-TokenNonInteractive([Uri]$Uri, [HashTable]$Body)
{
$requestParams = $defaultWebRequestParams + @{
Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post
Uri = $openIdConfig.token_endpoint
ContentType = "application/x-www-form-urlencoded"
}
if ($Body)
{
$requestParams['Body'] = $Body
Write-Verbose "Non-Interactive Token Request: $($Body.Keys | ConvertTo-Json -Compress)"
}
try
{
$response = Invoke-WebRequest @requestParams
$token = ConvertFrom-Json $response.Content
# save the tokens as secure strings and add methods to retrieve plain-text tokens in various forms
foreach ($tokenType in @('Access', 'Refresh', 'Id'))
{
$propName = "${tokenType}_token"
if ($token.$propName)
{
$token.$propName = [System.Net.NetworkCredential]::new($tokenType, $token.$propName).SecurePassword
}
$getToken = [scriptblock]::Create("[System.Net.NetworkCredential]::new('$tokenType', `$this.$propName).Password")
$token | Add-Member -MemberType ScriptMethod -Name "Get${tokenType}Token" -Value $getToken
$getJson = [scriptblock]::Create("if (-not `$this.${tokenType}_token) { return `$null };try{ `$claimsBase64 = `$this.Get${tokenType}Token().Split('.')[1].Replace('-','+').Replace('_','/'); return [System.Text.UTF32Encoding]::UTF8.GetString(([System.Convert]::FromBase64String(""`$(`$claimsBase64)`$([string]::new('=',@{0=0;2=2;3=1;1=0}[`$claimsBase64.Length % 4]))""))); }catch{return ""`$_""}")
$token | Add-Member -MemberType ScriptMethod -Name "Get${tokenType}TokenJson" -Value $getJson
$getClaims = [scriptblock]::Create("if (-not `$this.${tokenType}_token) { return `$null };try{ return (ConvertFrom-Json `$this.Get${tokenType}TokenJson().Replace('`"AppId`"','`"AppId2`"')); }catch{return ""`$_""}")
$token | Add-Member -MemberType ScriptMethod -Name "Get${tokenType}TokenClaims" -Value $getClaims
}
# add method to build authorization header for use in subsequent web requests
$token | Add-Member -MemberType ScriptMethod -Name GetAuthorizationHeader -Value { return '{0} {1}' -f $this.token_type, $this.GetAccessToken() }
# add properties for certain claims in the access token and other metadata
$claims = if ($claims=$token.GetIdTokenClaims()) {$claims} else {$token.GetAccessTokenClaims()}
$props = [ordered]@{
open_id = $openIdConfig
tenant = $claims.tid
issuer = $claims.iss
authority = $AuthorityUri.AbsoluteUri
issued_at = [DateTime]::Now
issued_at_utc = [DateTime]::UtcNow
expires_at = [DateTime]::Now.AddSeconds($token.expires_in)
expires_at_utc = [DateTime]::UtcNow.AddSeconds($token.expires_in)
}
if ($openIdConfig.cloud_graph_host_name)
{
$props += @{ graph_endpoint = 'https://{0}/{1}' -f $openIdConfig.cloud_graph_host_name.TrimEnd('/'), $claims.tid }
}
$token | Add-Member -NotePropertyMembers $props
$token | Add-Member -MemberType ScriptProperty -Name expired -Value {
return [DateTime]::UtcNow -ge $this.expires_at_utc
}
Write-Output $token
}
catch
{
$errorOut = $_.Exception.Response | Select Method,ResponseUri,StatusCode,StatusDescription,IsFromCache,LastModified | ConvertTo-Json
$errorOut = "Failed to retrieve token: $_`r`n`r`nAdditional details: $errorOut"
throw [System.InvalidOperationException]$errorOut
}
}
function ConvertTo-Base64UrlEncode([byte[]]$bytes)
{
return [System.Convert]::ToBase64String($bytes).Replace('/','_').Replace('+','-').Trim('=')
}
function ConvertTo-Base64UrlEncode([string]$text, [byte[]]$bytes)
{
$bytes = if ($bytes) { $bytes } else { [System.Text.Encoding]::UTF8.GetBytes($text) }
return [System.Convert]::ToBase64String($bytes).Replace('/','_').Replace('+','-').Trim('=')
}
function Modify($hashtable=@{})
{
if ($ExcludeResource){$hashtable.Remove('Resource')}
return $hashtable
}
$nonce = [guid]::NewGuid().ToString()
$bytes = [byte[]]::new(32)
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$codeVerifier = ConvertTo-Base64UrlEncode -bytes $bytes
$codeChallenge = ConvertTo-Base64UrlEncode -bytes ([System.Security.Cryptography.sha256]::Create().ComputeHash([System.Text.Encoding]::ASCII.GetBytes($codeVerifier)))
# Retrieve the token
if ($Credential)
{
# User credential non-interactive flow
$token = Get-TokenNonInteractive -Uri $openIdConfig.token_endpoint -Body (Modify @{
resource = $Resource
client_id = $ClientId
grant_type = 'password'
scope = $Scope
username = $Credential.UserName
password = $Credential.GetNetworkCredential().Password
})
Write-Output $token
}
elseif ($RefreshToken)
{
# Refresh token non-interactive flow
$token = Get-TokenNonInteractive -Uri $openIdConfig.token_endpoint -Body (Modify @{
resource = $Resource
client_id = $ClientId
grant_type = 'refresh_token'
scope = $Scope
refresh_token = [System.Net.NetworkCredential]::new('refreshToken', $RefreshToken).Password
})
Write-Output $token
}
elseif ($Certificate -or $CertificateThumbprint)
{
# Service Principal non-interactive flow with certificate
if (-not $Certificate)
{
$path = "Cert:\$CertificateStoreLocation\$CertificateStoreName\$CertificateThumbprint"
$certificates = @(Get-Item -Path $path)
if ($certificates.Count -eq 0)
{
throw [InvalidOperationException]"Unable to find referenced certificate '$path'."
}
elseif ($certificates.Count -gt 1)
{
Write-Warning "More than one certificate found at '$path'; using first one."
}
$Certificate = $certificates[0]
if (-not $Certificate.HasPrivateKey)
{
throw [System.InvalidOperationException]"Certificate found at '$path' does not have associated private key installed."
}
}
$currentUtcDateTimeInSeconds = ([datetime]::UtcNow - [datetime]'1970-01-01 00:00:00').TotalSeconds
$notBeforeSecondsRelativeToNow = -90
$expirationSecondsRelativeToNow = 3600
$tokenHeaders = [ordered]@{
alg = 'RS256'
x5t = ConvertTo-Base64UrlEncode $Certificate.GetCertHash()
}
$tokenClaims = [ordered]@{
aud = $openIdConfig.token_endpoint
exp = [long]($currentUtcDateTimeInSeconds + $expirationSecondsRelativeToNow)
iss = $ClientId
jti = [guid]::NewGuid().ToString()
nbf = [long]($currentUtcDateTimeInSeconds + $notBeforeSecondsRelativeToNow)
sub = $ClientId
}
Write-Debug "Preparing client assertion with token header: '$(ConvertTo-Json $tokenHeaders -Compress)' and claims: $(ConvertTo-Json $tokenClaims)"
# Note - we escape the forward slashes ('/') as the ConvertTo-Json cmdlet does not. This may not actually be necessary.
$tokenParts = @()
$tokenParts += ConvertTo-Base64UrlEncode ([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json $tokenHeaders -Depth 10 -Compress).Replace('/', '\/')))
$tokenParts += ConvertTo-Base64UrlEncode ([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json $tokenClaims -Depth 10 -Compress).Replace('/', '\/')))
$sha256Hash = ''
$sha256 = [System.Security.Cryptography.SHA256]::Create()
try
{
$sha256Hash = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($tokenParts -join '.'))
}
finally
{
if ($sha256) { $sha256.Dispose(); $sha256 = $null }
}
if ($Certificate.PrivateKey)
{
# Note - the default instance of the RSACryptoServiceProvider instantiated on the client certificate may only support SHA1.
# E.g. Even when "$($ClientCertificate.SignatureAlgorithm.FriendlyName)" evaluates to "sha256RSA", the value of
# "$($ClientCertificate.PrivateKey.SignatureAlgorithm)" may evaulate to "http://www.w3.org/2000/09/xmldsig#rsa-sha1".
# Furthermore, the private key is likely not marked as exportable, so we cannot "simply" instantiate a new RSACryptoServiceProvider instance.
# We must first create new CSP parameters with a "better" cryptographic service provider that supports SHA256, and use those parameters
# to instantiate a "better" RSACryptoServiceProvider which also supports SAH256. Failure to do this will result in the following error:
# "Exception calling "CreateSignature" with "1" argument(s): "Invalid algorithm specified."
# It may be possible to bypass this issue of the certificate is generated with the "correct" cryptographic service provider, but if the certificate
# was created by a CA or if the provider type was not the "correct" type, then this workaround must be used.
# Note - this assumes certificate is installed in the local machine store.
try
{
$csp = [System.Security.Cryptography.CspParameters]::new(
($providerType=24),
($providerName='Microsoft Enhanced RSA and AES Cryptographic Provider'),
($keyContainerName=$Certificate.PrivateKey.CspKeyContainerInfo.KeyContainerName))
$csp.Flags = [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore # TODO support other key location
}
catch
{
throw "An error occurred trying to load the '$providerName' using the specified certificate '$($Certificate.Thumbprint)'. Please ensure the certificate is installed into the local machine certificate store, and is accessible to the windows identity calling this function. You may also need to run this function in an elevated context to access the certificate private key."
}
$sigBytes = $null
$rsa = [System.Security.Cryptography.RSACryptoServiceProvider]::new($csp)
try
{
$sigBytes = $rsa.SignHash($sha256Hash, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
}
finally
{
if ($rsa) { $rsa.Dispose(); $rsa = $null }
}
}
else
{
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
$sigBytes = $rsa.SignHash($sha256Hash, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
}
$tokenParts += ConvertTo-Base64UrlEncode $sigBytes
$clientAssertion = $tokenParts -join '.'
$token = Get-TokenNonInteractive -Uri $openIdConfig.token_endpoint -Body (Modify @{
resource = $Resource
client_id = $ClientId
grant_type = 'client_credentials'
client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
client_assertion = $clientAssertion
scope = $Scope
})
Write-Output $token
}
elseif ($ClientSecret)
{
# Service Principal non-interactive flow with secret
$token = Get-TokenNonInteractive -Uri $openIdConfig.token_endpoint -Body (Modify @{
resource = $Resource
client_id = $ClientId
client_secret = [System.Net.NetworkCredential]::new('clientSecret', $ClientSecret).Password
grant_type = 'client_credentials'
scope = $Scope
})
Write-Output $token
}
else
{
# User interactive flow
$dependantAssembly = 'System.Windows.Forms'
if (-not [System.Reflection.Assembly]::LoadWithPartialName($dependantAssembly))
{
throw [System.NotSupportedException]"Unable to load required assembly '$dependantAssembly' for an interactive login."
}
# https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#request-an-authorization-code
$params = @{
resource = $Resource
client_Id = $ClientId
redirect_uri = $RedirectUri
prompt = $Prompt
response_type = 'code'
response_mode = 'query'
state = ($state = (New-Guid).ToString())
scope = $Scope
nonce = $nonce
}
if ($PKCE)
{
$params += @{
code_challenge_method = 'S256'
code_challenge = $codeChallenge
}
}
if ($LoginHint)
{
$params['login_hint'] = $LoginHint
}
if ($DomainHint)
{
$params['domain_hint'] = $DomainHint
}
$authorizationEndpointUri = '{0}?{1}' -f $openIdConfig.authorization_endpoint.TrimEnd(), (ConvertTo-QueryString (Modify $params))
Write-Verbose "Interactive Authorization URI: '$authorizationEndpointUri'"
try
{
$size = @{ Width = 700; Height = 800 }
$form = [Windows.Forms.Form]@{
Text = $title = "PS C:\> $($MyInvocation.MyCommand.Name)"
StartPosition = 'CenterScreen'
Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($PSHOME + "\powershell.exe")
AutoScroll = $true
AutoSize = $true
AutoSizeMode = 'GrowAndShrink'
SizeGripStyle = 'Hide'
MinimizeBox = $false
MaximizeBox = $false
ShowInTaskbar = $true
}
$browser = [System.Windows.Forms.WebBrowser]@{ Width = 700; Height = 800; Margin = 0; Padding = 0; }
$form.Controls.Add($browser)
$result = @{}
$browser.Add_Navigated({
Write-Debug "Navigated: '$($browser.Url.GetLeftPart([System.UriPartial]::Path))'"
if ($browser.Url.AbsoluteUri.StartsWith($RedirectUri))
{
if ($browser.Url.Fragment -like '*error=*')
{
$result['error'] = [System.InvalidOperationException]"An error occurred while processing an interactive login session: $($browser.Url.Fragment)"
$form.Close()
return
}
$data = @{}
$query = [System.Web.HttpUtility]::ParseQueryString($browser.Url.Query)
$query.AllKeys | ForEach { $data[$_] = $query[$_] }
if ($data.error)
{
$result['error'] = [System.InvalidOperationException]"An error occurred while processing an interactive login session: $(ConvertTo-Json $data -Depth 2)"
$form.Close()
return
}
elseif ($data.state -and ($data.state -ne $state))
{
$result['error'] = [System.InvalidOperationException]"Unexpected state! Authentication requests were expected to contain state flag '$state' but instead contained '$($data.state)'; someone may be trying to intefere with your communication!"
$form.Close()
return
}
elseif ($data.code)
{
$result['code'] = $data.code
$form.Close()
return
}
}
})
Write-Host "Please authenticate interactively using the launched window with title: '$title'"
$form.BringToFront()
$browser.Navigate($authorizationEndpointUri)
[System.Windows.Forms.Application]::Run($form) # This is a blocking call!
Write-Debug "Authentication window closed."
if ($result.code)
{
# https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#use-the-authorization-code-to-request-an-access-token
$params = @{
tenant = $DirectoryTenant
client_id = $ClientId
grant_type = 'authorization_code'
code = $result.code
redirect_uri = $RedirectUri
resource = $Resource
scope = $Scope
}
if ($PKCE)
{
$params += @{ code_verifier = $codeVerifier }
}
$token = Get-TokenNonInteractive -Uri $openIdConfig.token_endpoint -Body (Modify $params)
Write-Output $token
}
elseif ($result.error)
{
throw $result.error
}
else
{
throw [System.InvalidOperationException]"The interactive authentication session was cancelled or failed with an unknown error."
}
}
finally
{
if ($browser) { $browser.Dispose(); $browser = $null }
if ($form) { $form.Dispose(); $form = $null }
}
}
}
catch
{
Write-Error -ErrorAction $originalErrorActionPreference -Exception $_.Exception
}
finally
{
$ErrorActionPreference = $originalErrorActionPreference
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment