OAuth2Toolkit.psm1
function ConvertFrom-Timestamp { param( [Parameter(Mandatory = $true)] [int]$Timestamp ) $utc = (Get-Date 01.01.1970) + ([System.TimeSpan]::fromseconds($Timestamp)) $datetime = [datetime]::SpecifyKind($utc, 'Utc').ToLocalTime() $datetime } function Invoke-ClientCredentialsFlow { param( [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(ParameterSetName='ClientCredential')] [pscredential]$Client, [Parameter(ParameterSetName='ClientExplicit')] [string]$ClientId, [Parameter(ParameterSetName='ClientExplicit')] [string]$ClientSecret, [string]$Scope = "https://graph.microsoft.com/.default" ) $authUrl = "https://login.microsoftonline.com/{0}/oauth2/token" -f $Tenant $parameters = @{ grant_type = "client_credentials" client_secret = $ClientSecret scope = $Scope client_id = $ClientId } $response = Invoke-RestMethod -Uri $authUrl -Method Post -Body $parameters $expires = ConvertFrom-Timestamp -Timestamp $response.expires_on $result = [PSCustomObject]@{ Expires = $expires AccessToken = $response.access_token } $result } function New-AccessToken { param( [string]$Tenant, [Parameter(ParameterSetName='ClientCredential')] [pscredential]$Client, [Parameter(ParameterSetName='ClientExplicit')] [string]$ClientId, [Parameter(ParameterSetName='ClientExplicit')] [string]$ClientSecret, [string]$RefreshToken ) $authUrl = "https://login.microsoftonline.com/{0}/oauth2/token" -f $Tenant $parameters = @{ grant_type = "refresh_token" client_secret= $ClientSecret refresh_token = $RefreshToken client_id = $ClientId } $response = Invoke-RestMethod -Uri $authUrl -Method Post -Body $parameters $expires = ConvertFrom-Timestamp -Timestamp $response.expires_on $result = [PSCustomObject]@{ Expires = $expires AccessToken = $response.access_token } $result } function Invoke-OnBehalfOfFlow { # https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-on-behalf-of-flow param( [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$clientSecret, [Parameter(Mandatory = $true)] [string]$AccessToken, [Parameter()] [string]$Resource = "https://graph.microsoft.com" ) $payload = @{ grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" requested_token_use = "on_behalf_of" scope = "openid" assertion = $AccessToken resource = $Resource client_id = $ClientId client_secret = $clientSecret } $response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$Tenant/oauth2/token" -Body $payload $response } function ConvertTo-AuthorizationHeaders { param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline)] $response ) $headers = @{ 'Content-Type' = 'application/json' 'Authorization' = "Bearer " + $response.access_token 'ExpiresOn' = (ConvertFrom-Timestamp -Timestamp $response.expires_on) } $headers } function Add-AdalAssemblies { $assemblyPath = Join-path $PSScriptRoot "NetCoreAssemblies\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" Add-Type -path $assemblyPath $assemblyPath = Join-path $PSScriptRoot "NetCoreAssemblies\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll" Add-Type -path $assemblyPath } function New-OnBehalfOfAccessToken { param( [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$clientSecret, [Parameter(Mandatory = $true)] [string]$AccessToken, [Parameter()] [string]$ResourcePrincial = "https://graph.microsoft.com" ) Add-AdalAssemblies $authority = "https://login.microsoftonline.com/$Tenant" $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority $clientCredentials = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential" -ArgumentList ($ClientId, $ClientSecret) $userAssertion = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserAssertion" -ArgumentList ($AccessToken) $authResult = $authContext.AcquireTokenAsync($ResourcePrincial, $clientCredentials, $userAssertion) if ($authResult.Result.AccessToken) { # Creating header for Authorization token $authHeader = @{ 'Content-Type' = 'application/json' 'Authorization' = "Bearer " + $authResult.Result.AccessToken 'ExpiresOn' = $authResult.Result.ExpiresOn } $authHeader } elseif ($authResult.Exception) { throw "An error occured getting access token: $($authResult.Exception.InnerException)" } } # Oauth password flow function Add-Win32HelperType { $nativeHelperTypeDefinition = @" using System; using System.Runtime.InteropServices; public static class WinApi { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetForegroundWindow(IntPtr hWnd); public static bool SetForeground(IntPtr windowHandle) { return SetForegroundWindow(windowHandle); } [DllImport("user32.dll", SetLastError=true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); public static void KillProcess(IntPtr windowHandle) { uint pid; GetWindowThreadProcessId(windowHandle, out pid); System.Diagnostics.Process p = System.Diagnostics.Process.GetProcessById((int)pid); if(p != null) p.Kill(); } } "@ if(-not ([System.Management.Automation.PSTypeName] "WinApi").Type) { Add-Type -TypeDefinition $nativeHelperTypeDefinition } } function Invoke-BrowserLogin { param ( [Parameter(HelpMessage='Authorization URL', Mandatory = $true)] [ValidateNotNull()] [string]$AuthorizationUrl, [Parameter(HelpMessage='Redirect URI', Mandatory = $true)] [ValidateNotNull()] $RedirectUrl, $ExpectedSuccessParameter = "code" ) Add-Type -AssemblyName System.Web Add-Win32HelperType # Create an Internet Explorer Window for the Login Experience $ie = New-Object -ComObject InternetExplorer.Application $ie.Width = 550 $ie.Height = 600 $ie.AddressBar = $false $ie.ToolBar = $false $ie.StatusBar = $false $ie.visible = $true $ie.navigate($authorizationUrl) $handle = $ie.HWND $winForeground = [WinApi]::SetForeground($handle) # Grab the window $wind = (New-Object -ComObject Shell.Application).Windows() | Where-Object { $_.HWND -eq $handle } $sleepCounter = 0 while ($ie.Busy) { Start-Sleep -Milliseconds 50 $sleepCounter++ if ($sleepCounter -eq 100) { throw "Unable to connect to $authorizationUrl, timed out waiting for page." } } try { while($true) { $urls = @() $urls += $wind.LocationUrl | Where-Object { $_ -and $_ -match "(^https?://.+)|(^ftp://)" } if (-not $urls) { # "No urls found, refreshing window" $wind = (New-Object -ComObject Shell.Application).Windows() | Where-Object { $_.HWND -eq $handle } if (-not $wind) { throw "Could not find IE window with handle: $handle" } } foreach ($url in $urls) { $urlPrefix = "{0}?{1}=" -f $RedirectUrl, $ExpectedSuccessParameter if (($url).StartsWith($urlPrefix)) { $code = $url -replace (".*$($ExpectedSuccessParameter)=") -replace ("&.*") # | Out-File $outputAuth return $code } elseif (($url).StartsWith($RedirectUrl + "?error=")) { $error = [System.Web.HttpUtility]::UrlDecode(($a.LocationUrl) -replace (".*error=")) throw $error } } } } finally { [WinApi]::KillProcess($handle) } } function Invoke-CodeGrantFlow { param( [Parameter(HelpMessage='Redirect Uri', Mandatory = $true)] [ValidateNotNull()] [string]$RedirectUrl, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$ClientSecret, [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$Resource, [bool]$AlwaysPrompt = $false ) # https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code $authorizationUrl = ("https://login.microsoftonline.com/{0}/oauth2/authorize?response_type=code&client_id={1}&redirect_uri={2}&resource={3}" -f $Tenant, $ClientId, $RedirectUrl, $Resource) if($AlwaysPrompt) { $authorizationUrl += "&prompt=select_account" } $code = Invoke-BrowserLogin -AuthorizationUrl $authorizationUrl -RedirectUrl $RedirectUrl if(-not $code) { throw "Code Grant Flow failed" } $url = "https://login.microsoftonline.com/{0}/oauth2/token" -f $Tenant $fields = @{ grant_type = "authorization_code" client_id = $clientId code = $code redirect_uri = $RedirectUrl resource = $Resource client_secret = $clientSecret } $response = Invoke-RestMethod -Method Post -Uri $url -Body $fields $response } function Invoke-DeviceCodeFlow { param( [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$ClientSecret, [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$Resource ) $postParams = @{ resource = $Resource client_id = $ClientId } $url = "https://login.microsoftonline.com/{0}/oauth2/devicecode" -f $Tenant $response = Invoke-RestMethod -Method Post -Uri $url -Body $postParams if(-not $response.device_code) { throw "Device Code Flow failed" } $tokenResponse = $null $maxDate = (Get-Date).AddSeconds($response.expires_in) $url = "https://login.microsoftonline.com/{0}/oauth2/token" -f $Tenant $tokenParams = @{ grant_type = "device_code" resource = $Resource client_id = "$ClientId" code = $response.device_code } while (!$tokenResponse -and (Get-Date) -lt $maxDate) { try { $tokenResponse = Invoke-RestMethod -Method Post -Uri $url -Body $tokenParams } catch [System.Net.WebException], [Microsoft.PowerShell.Commands.HttpResponseException] { if ($_.Exception.Response -eq $null) { throw } $errBody = $null if($PSEdition -eq "Core") { $errBody = ConvertFrom-Json ($_.ErrorDetails | Select-Object -ExpandProperty Message) } else { $result = $_.Exception.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($result) $reader.BaseStream.Position = 0 $errBody = ConvertFrom-Json $reader.ReadToEnd(); } if(-not $errBody -or $errBody.Error -ne "authorization_pending") { throw } Start-Sleep($response.interval) Write-Host -NoNewline "." } } $tokenResponse } function Invoke-ResourceOwnerPasswordGrantFlow { param( [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [pscredential]$UserCredentials, [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$Resource ) $btsr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($UserCredentials.Password) $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($btsr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($btsr) $payload = @{ grant_type = "password" client_id = $ClientId resource = $Resource username = $UserCredentials.UserName password = $plainPassword } $response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$Tenant/oauth2/token" -Body $payload $response } function Invoke-AdminConsentForApplication { param( [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$RedirectUrl ) # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#using-the-admin-consent-endpoint $consentUrl = "https://login.microsoftonline.com/{0}/adminconsent?client_id={1}&state=12345&redirect_uri={2}" -f $Tenant, $ClientId, $RedirectUrl $response = Invoke-BrowserLogin -AuthorizationUrl $consentUrl -RedirectUrl $RedirectUrl -ExpectedSuccessParameter "admin_consent" if($response -ne "True") { throw "Admin consent failed" } } function Invoke-OnBehalfOfCertificateFlow { # https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-on-behalf-of-flow param( [Parameter(Mandatory = $true)] [string]$Tenant, [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$CertificateThumbprint, [Parameter(Mandatory = $true)] [string]$AccessToken, [Parameter()] [string]$Resource = "https://graph.microsoft.com" ) $certificate = Get-ChildItem Cert:\CurrentUser\My\$CertificateThumbprint $jwt = New-Jwt -Subject $ClientId -Issuer $ClientId -ValidforSeconds 180 -Certificate $certificate -Audience "https://login.microsoftonline.com/$($Tenant)/oauth2/token" $payload = @{ grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" requested_token_use = "on_behalf_of" scope = "openid" assertion = $AccessToken client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" client_assertion = $jwt resource = $Resource client_id = $ClientId } $response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$Tenant/oauth2/token" -Body $payload $response } function New-Jwt { param( $Type = "JWT", [Parameter(Mandatory = $True)] [string]$Issuer = $null, [int]$ValidforSeconds = 180, [Parameter(Mandatory=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate, $Audience = $null, $Subject = $null ) $Algorithm = "RS256" $exp = [int][double]::parse((Get-Date -Date $((Get-Date).addseconds($ValidforSeconds).ToUniversalTime()) -UFormat %s)) # Grab Unix Epoch Timestamp and add desired expiration. [hashtable]$header = @{ alg = $Algorithm typ = $type x5t = [System.Convert]::ToBase64String($Certificate.GetCertHash()) } [hashtable]$payload = @{ iss = $Issuer exp = $exp aud = $Audience sub = $Subject } $headerjson = $header | ConvertTo-Json -Compress $payloadjson = $payload | ConvertTo-Json -Compress $headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_') $payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_') $jwt = $headerjsonbase64 + "." + $payloadjsonbase64 $toSign = [System.Text.Encoding]::UTF8.GetBytes($jwt) $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) if ($null -eq $rsa) { # Requiring the private key to be present; else cannot sign! throw "There's no private key in the supplied certificate - cannot sign" } else { try { $signed = $rsa.SignData($toSign, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) $sig = [Convert]::ToBase64String($signed) -replace '\+','-' -replace '/','_' -replace '=' } catch { throw "Signing with SHA256 and Pkcs1 padding failed using private key $rsa" } } $jwt = $jwt + '.' + $sig return $jwt } # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow function Invoke-ClientCredentalsCertificateFlow { param( [Parameter(Mandatory = $true)] [string]$Tenant, [string]$ClientId, [string]$CertificateThumbprint, [string]$Resource = "https://graph.microsoft.com" ) $certificate = Get-ChildItem Cert:\CurrentUser\My\$CertificateThumbprint $jwt = New-Jwt -Subject $ClientId -Issuer $ClientId -ValidforSeconds 180 -Certificate $certificate -Audience "https://login.microsoftonline.com/$($Tenant)/oauth2/token" $authUrl = "https://login.microsoftonline.com/{0}/oauth2/token" -f $Tenant $parameters = @{ grant_type = "client_credentials" resource = $Resource client_id = $ClientId client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" client_assertion = $jwt } $response = Invoke-RestMethod -Uri $authUrl -Method Post -Body $parameters $expires = ConvertFrom-Timestamp -Timestamp $response.expires_on $result = [PSCustomObject]@{ Expires = $expires AccessToken = $response.access_token } $result } function Invoke-ClientCredentialsFlow { param( [string]$Tenant, [Parameter(ParameterSetName='ClientCredential')] [pscredential]$Client, [Parameter(ParameterSetName='ClientExplicit')] [string]$ClientId, [Parameter(ParameterSetName='ClientExplicit')] [string]$ClientSecret, [string]$Resource = "https://graph.microsoft.com", [string]$AuthorizationEndpoint = "https://login.microsoftonline.com/{0}/oauth2/token" ) $authUrl = $AuthorizationEndpoint -f $Tenant $parameters = @{ grant_type = "client_credentials" client_secret = $ClientSecret resource = $Resource client_id = $ClientId } $response = Invoke-RestMethod -Uri $authUrl -Method Post -Body $parameters $expires = ConvertFrom-Timestamp -Timestamp $response.expires_on $result = [PSCustomObject]@{ Expires = $expires AccessToken = $response.access_token } $result } |