Can anyone help me out troubleshooting this script as to why doesn't return the $TokenResponse?
I was able to use secret key and user auth but can't find a way to auth using the self-signed cert which is the most secure option of all.
In Azure, permissions are assigned to Application and work fine with said secret key
#Define Client Variables Here
########################
$TenantName = "contoso.com"
$AppId = "11111-2222-3333-4444-5555"
$Certificate = Get-Item Cert:LocalMachine\My\1234567890987654321
$Scope = "https://graph.microsoft.com/.default"
#Create base64 hash of certificate
##################################
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
#Create JWT timestamp for expiration
####################################
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)
#Create JWT validity start timestamp
####################################
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)
#Create JWT header
#################
$JWTHeader = #{
alg = "RS256"
typ = "JWT"
x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
}
#Create JWT payload
#################
$JWTPayLoad = #{
aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"
exp = $JWTExpiration
iss = $AppId
jti = [guid]::NewGuid()
nbf = $NotBefore
sub = $AppId
}
# Convert header and payload to base64
#################################
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)
$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)
# Join header and Payload with "." to create a valid (unsigned) JWT
######################################################
$JWT = $EncodedHeader + "." + $EncodedPayload
# Get the private key object of your certificate
######################################
$PrivateKey = $Certificate.PrivateKey
# Define RSA signature and hashing algorithm
#####################################
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256
# Create a signature of the JWT
#########################
$Signature = [Convert]::ToBase64String(
$PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
) -replace '\+','-' -replace '/','_' -replace '='
# Join the signature to the JWT with "."
###############################
$JWT = $JWT + "." + $Signature
# Use the self-generated JWT as Authorization to get the Access Token
##########################################################
$Header = #{
Authorization = "Bearer $JWT"
}
$Body = #{
client_id = $AppId
client_assertion = $JWT
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
scope = $Scope
grant_type = "client_credentials"
}
#$authUri = "https://login.microsoftonline.com/common/oauth2/token"
$authUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"
$TokenResponse = Invoke-RestMethod -Header $Header -Uri $authUri -Method POST -Body $Body
$TokenResponse
When I run it as Admin, I simply get an empty $TokenResponse
Any help will be highly appreciated.
Thanks!
Sorted by replacing the AUD and payload token with this URL:
https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token
Related
I have researched quite a bit and have not found any information how to get an access token from graph with Powershell via REST by using:
TenantID
Client/AppID
Certificate
with the following conditions:
using invoke-restmethod
What Have I already looked at?
https://www.wesleytrust.com/blog/obtain-access-token/ (does use clientsecret but not certificate)
https://learn.microsoft.com/en-us/graph/auth-v2-service (does use clientsecret but not certificate)
Microsoft Graph: How to get access token with certificate in client credentials flow? (instead of using a client_secret) (this does it but I don't understand the script language completely there to transform this into a working powershell solution)
After hours of research, I finally got a working solution. It uses a project on github to create a JWT token.
One very important point: do not use the certificate thumbprint from within the certificate for calculating x5t but calculate the certificate hash yourself (look at the code below for details).
Hope, this helps someone.
cls
$TenantID = '<Your Tenant ID>'
$AppID = '<your app / clientID>'
$CertThumbprint = '<thumbprint of certificate with private key, stored within machine-personal folder>' # *** Alternatively, the Certificate can be loaded directly from a PFX file - see main script further below ***
# ******************************************************************************************************************************************************************************************
# * JWT functions - Start
function ConvertFrom-Base64UrlString {
<#
.SYNOPSIS
Base64url decoder.
.DESCRIPTION
Decodes base64url-encoded string to the original string or byte array.
.PARAMETER Base64UrlString
Specifies the encoded input. Mandatory string.
.PARAMETER AsByteArray
Optional switch. If specified, outputs byte array instead of string.
.INPUTS
You can pipe the string input to ConvertFrom-Base64UrlString.
.OUTPUTS
ConvertFrom-Base64UrlString returns decoded string by default, or the bytes if -AsByteArray is used.
.EXAMPLE
PS Variable:> 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' | ConvertFrom-Base64UrlString
{"alg":"RS256","typ":"JWT"}
.LINK
https://github.com/SP3269/posh-jwt
.LINK
https://jwt.io/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$Base64UrlString,
[Parameter(Mandatory=$false)][switch]$AsByteArray
)
$s = $Base64UrlString.replace('-','+').replace('_','/')
switch ($s.Length % 4) {
0 { $s = $s }
1 { $s = $s.Substring(0,$s.Length-1) }
2 { $s = $s + "==" }
3 { $s = $s + "=" }
}
if ($AsByteArray) {
return [Convert]::FromBase64String($s) # Returning byte array - convert to string by using [System.Text.Encoding]::{{UTF8|Unicode|ASCII}}.GetString($s)
}
else {
return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s))
}
}
function ConvertTo-Base64UrlString {
<#
.SYNOPSIS
Base64url encoder.
.DESCRIPTION
Encodes a string or byte array to base64url-encoded string.
.PARAMETER in
Specifies the input. Must be string, or byte array.
.INPUTS
You can pipe the string input to ConvertTo-Base64UrlString.
.OUTPUTS
ConvertTo-Base64UrlString returns the encoded string by default.
.EXAMPLE
PS Variable:> '{"alg":"RS256","typ":"JWT"}' | ConvertTo-Base64UrlString
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.LINK
https://github.com/SP3269/posh-jwt
.LINK
https://jwt.io/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]$in
)
if ($in -is [string]) {
return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($in)) -replace '\+','-' -replace '/','_' -replace '='
}
elseif ($in -is [byte[]]) {
return [Convert]::ToBase64String($in) -replace '\+','-' -replace '/','_' -replace '='
}
else {
throw "ConvertTo-Base64UrlString requires string or byte array input, received $($in.GetType())"
}
}
function Get-JwtHeader {
<#
.SYNOPSIS
Gets JSON payload from a JWT (JSON Web Token).
.DESCRIPTION
Decodes and extracts JSON header from JWT. Ignores payload and signature.
.PARAMETER jwt
Specifies the JWT. Mandatory string.
.INPUTS
You can pipe JWT as a string object to Get-JwtHeader.
.OUTPUTS
String. Get-JwtHeader returns decoded header part of the JWT.
.EXAMPLE
PS Variable:> Get-JwtHeader 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ.'
{"alg":"none","typ":"JWT"}
.LINK
https://github.com/SP3269/posh-jwt
.LINK
https://jwt.io/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$jwt
)
Write-Verbose "Processing JWT: $jwt"
$parts = $jwt.Split('.')
$header = ConvertFrom-Base64UrlString $parts[0]
return $header
}
function Get-JwtPayload {
<#
.SYNOPSIS
Gets JSON payload from a JWT (JSON Web Token).
.DESCRIPTION
Decodes and extracts JSON payload from JWT. Ignores headers and signature.
.PARAMETER jwt
Specifies the JWT. Mandatory string.
.INPUTS
You can pipe JWT as a string object to Get-JwtPayload.
.OUTPUTS
String. Get-JwtPayload returns decoded payload part of the JWT.
.EXAMPLE
PS Variable:> $jwt | Get-JwtPayload -Verbose
VERBOSE: Processing JWT: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbjEiOiJ2YWx1ZTEiLCJ0b2tlbjIiOiJ2YWx1ZTIifQ.Kd12ryF7Uuk9Y1UWsqdSk6cXNoYZBf9GBoqcEz7R5e4ve1Kyo0WmSr-q4XEjabcbaG0hHJyNGhLDMq6BaIm-hu8ehKgDkvLXPCh15j9AzabQB4vuvSXSWV3MQO7v4Ysm7_sGJQjrmpiwRoufFePcurc94anLNk0GNkTWwG59wY4rHaaHnMXx192KnJojwMR8mK-0_Q6TJ3bK8lTrQqqavnCW9vrKoWoXkqZD_4Qhv2T6vZF7sPkUrgsytgY21xABQuyFrrNLOI1g-EdBa7n1vIyeopM4n6_Uk-ttZp-U9wpi1cgg2pRIWYV5ZT0AwZwy0QyPPx8zjh7EVRpgAKXDAg
{"token1":"value1","token2":"value2"}
.LINK
https://github.com/SP3269/posh-jwt
.LINK
https://jwt.io/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true,ValueFromPipeline=$true)][string]$jwt
)
Write-Verbose "Processing JWT: $jwt"
$parts = $jwt.Split('.')
$payload = ConvertFrom-Base64UrlString $parts[1]
return $payload
}
function New-Jwt {
<#
.SYNOPSIS
Creates a JWT (JSON Web Token).
.DESCRIPTION
Creates signed JWT given a signing certificate and claims in JSON.
.PARAMETER Payload
Specifies the claim to sign in JSON. Mandatory string.
.PARAMETER Header
Specifies a JWT header. Optional. Defaults to '{"alg":"RS256","typ":"JWT"}'.
.PARAMETER Cert
Specifies the signing certificate of type System.Security.Cryptography.X509Certificates.X509Certificate2. Must be specified and contain the private key if the algorithm in the header is RS256.
.PARAMETER Secret
Specifies the HMAC secret. Can be byte array, or a string, which will be converted to bytes. Must be specified if the algorithm in the header is HS256.
.INPUTS
You can pipe a string object (the JSON payload) to New-Jwt.
.OUTPUTS
System.String. New-Jwt returns a string with the signed JWT.
.EXAMPLE
PS Variable:\> $cert = (Get-ChildItem Cert:\CurrentUser\My)[1]
PS Variable:\> New-Jwt -Cert $cert -PayloadJson '{"token1":"value1","token2":"value2"}'
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbjEiOiJ2YWx1ZTEiLCJ0b2tlbjIiOiJ2YWx1ZTIifQ.Kd12ryF7Uuk9Y1UWsqdSk6cXNoYZBf9GBoqcEz7R5e4ve1Kyo0WmSr-q4XEjabcbaG0hHJyNGhLDMq6BaIm-hu8ehKgDkvLXPCh15j9AzabQB4vuvSXSWV3MQO7v4Ysm7_sGJQjrmpiwRoufFePcurc94anLNk0GNkTWwG59wY4rHaaHnMXx192KnJojwMR8mK-0_Q6TJ3bK8lTrQqqavnCW9vrKoWoXkqZD_4Qhv2T6vZF7sPkUrgsytgY21xABQuyFrrNLOI1g-EdBa7n1vIyeopM4n6_Uk-ttZp-U9wpi1cgg2pRIWYV5ZT0AwZwy0QyPPx8zjh7EVRpgAKXDAg
.EXAMPLE
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("/mnt/c/PS/JWT/jwt.pfx","jwt")
$now = (Get-Date).ToUniversalTime()
$createDate = [Math]::Floor([decimal](Get-Date($now) -UFormat "%s"))
$expiryDate = [Math]::Floor([decimal](Get-Date($now.AddHours(1)) -UFormat "%s"))
$rawclaims = [Ordered]#{
iss = "examplecom:apikey:uaqCinPt2Enb"
iat = $createDate
exp = $expiryDate
} | ConvertTo-Json
$jwt = New-Jwt -PayloadJson $rawclaims -Cert $cert
$apiendpoint = "https://api.example.com/api/1.0/systems"
$splat = #{
Method="GET"
Uri=$apiendpoint
ContentType="application/json"
Headers = #{authorization="bearer $jwt"}
}
Invoke-WebRequest #splat
.LINK
https://github.com/SP3269/posh-jwt
.LINK
https://jwt.io/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$false)][string]$Header = '{"alg":"RS256","typ":"JWT"}',
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][string]$PayloadJson,
[Parameter(Mandatory=$false)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert,
[Parameter(Mandatory=$false)]$Secret # Can be string or byte[] - checks in the code
)
Write-Verbose "Payload to sign: $PayloadJson"
try { $Alg = (ConvertFrom-Json -InputObject $Header -ErrorAction Stop).alg } # Validating that the parameter is actually JSON - if not, generate breaking error
catch { throw "The supplied JWT header is not JSON: $Header" }
Write-Verbose "Algorithm: $Alg"
try { ConvertFrom-Json -InputObject $PayloadJson -ErrorAction Stop | Out-Null } # Validating that the parameter is actually JSON - if not, generate breaking error
catch { throw "The supplied JWT payload is not JSON: $PayloadJson" }
$encodedHeader = ConvertTo-Base64UrlString $Header
$encodedPayload = ConvertTo-Base64UrlString $PayloadJson
$jwt = $encodedHeader + '.' + $encodedPayload # The first part of the JWT
$toSign = [System.Text.Encoding]::UTF8.GetBytes($jwt)
switch($Alg) {
"RS256" {
if (-not $PSBoundParameters.ContainsKey("Cert")) {
throw "RS256 requires -Cert parameter of type System.Security.Cryptography.X509Certificates.X509Certificate2"
}
Write-Verbose "Signing certificate: $($Cert.Subject)"
$rsa = $Cert.PrivateKey
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 {
# Overloads tested with RSACryptoServiceProvider, RSACng, RSAOpenSsl
try { $sig = ConvertTo-Base64UrlString $rsa.SignData($toSign,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1) }
catch { throw New-Object System.Exception -ArgumentList ("Signing with SHA256 and Pkcs1 padding failed using private key $($rsa): $_", $_.Exception) }
}
}
"HS256" {
if (-not ($PSBoundParameters.ContainsKey("Secret"))) {
throw "HS256 requires -Secret parameter"
}
try {
$hmacsha256 = New-Object System.Security.Cryptography.HMACSHA256
if ($Secret -is [byte[]]) {
$hmacsha256.Key = $Secret
}
elseif ($Secret -is [string]) {
$hmacsha256.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
else {
throw "Expected Secret parameter as byte array or string, instead got $($Secret.gettype())"
}
$sig = ConvertTo-Base64UrlString $hmacsha256.ComputeHash($toSign)
}
catch { throw New-Object System.Exception -ArgumentList ("Signing with HMACSHA256 failed: $_", $_.Exception) }
}
"none" {
$sig = $null
}
default {
throw 'The algorithm is not one of the supported: "RS256", "HS256", "none"'
}
}
$jwt = $jwt + '.' + $sig
return $jwt
}
function Test-Jwt {
<#
.SYNOPSIS
Tests cryptographic integrity of a JWT (JSON Web Token).
.DESCRIPTION
Verifies a digital signature of a JWT given the signing certificate (for RS256) or the secret (for HS256).
.PARAMETER Cert
Specifies the signing certificate of type System.Security.Cryptography.X509Certificates.X509Certificate2.
Must be specified if the algorithm in the header is RS256. Doesn't have to, and generally shouldn't, contain the private key.
.PARAMETER Secret
Specifies the HMAC secret. Can be byte array, or a string, which will be converted to bytes.
Must be specified if the algorithm in the header is HS256.
.INPUTS
You can pipe JWT as a string object to Test-Jwt.
.OUTPUTS
Boolean. Test-Jwt returns $true if the signature successfully verifies.
.EXAMPLE
PS Variable:> $jwt | Test-Jwt -cert $cert -Verbose
VERBOSE: Verifying JWT: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbjEiOiJ2YWx1ZTEiLCJ0b2tlbjIiOiJ2YWx1ZTIifQ.Kd12ryF7Uuk9Y1UWsqdSk6cXNoYZBf9GBoqcEz7R5e4ve1Kyo0WmSr-q4XEjabcbaG0hHJyNGhLDMq6BaIm-hu8ehKgDkvLXP
Ch15j9AzabQB4vuvSXSWV3MQO7v4Ysm7_sGJQjrmpiwRoufFePcurc94anLNk0GNkTWwG59wY4rHaaHnMXx192KnJojwMR8mK-0_Q6TJ3bK8lTrQqqavnCW9vrKoWoXkqZD_4Qhv2T6vZF7sPkUrgsytgY21xABQuyFrrNLOI1g-EdBa7n1vIyeopM4n6_Uk-ttZp-U9wpi1cgg2p
RIWYV5ZT0AwZwy0QyPPx8zjh7EVRpgAKXDAg
VERBOSE: Using certificate with subject: CN=jwt_signing_test
True
.LINK
https://github.com/SP3269/posh-jwt
.LINK
https://jwt.io/
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][string]$jwt,
[Parameter(Mandatory=$false)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert,
[Parameter(Mandatory=$false)]$Secret
)
Write-Verbose "Verifying JWT: $jwt"
$parts = $jwt.Split('.')
$Header = ConvertFrom-Base64UrlString $Parts[0]
try { $Alg = (ConvertFrom-Json -InputObject $Header -ErrorAction Stop).alg } # Validating that the parameter is actually JSON - if not, generate breaking error
catch { throw "The supplied JWT header is not JSON: $Header" }
Write-Verbose "Algorithm: $Alg"
switch($Alg) {
"RS256" {
if (-not $PSBoundParameters.ContainsKey("Cert")) {
throw "RS256 requires -Cert parameter of type System.Security.Cryptography.X509Certificates.X509Certificate2"
}
$bytes = ConvertFrom-Base64URLString $parts[2] -AsByteArray
Write-Verbose "Using certificate with subject: $($Cert.Subject)"
$SHA256 = New-Object Security.Cryptography.SHA256Managed
$computed = $SHA256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($parts[0]+"."+$parts[1])) # Computing SHA-256 hash of the JWT parts 1 and 2 - header and payload
return $cert.PublicKey.Key.VerifyHash($computed,$bytes,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1) # Returns True if the hash verifies successfully
}
"HS256" {
if (-not ($PSBoundParameters.ContainsKey("Secret"))) {
throw "HS256 requires -Secret parameter"
}
$hmacsha256 = New-Object System.Security.Cryptography.HMACSHA256
if ($Secret -is [byte[]]) {
$hmacsha256.Key = $Secret
}
elseif ($Secret -is [string]) {
$hmacsha256.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret)
}
else {
throw "Expected Secret parameter as byte array or string, instead got $($Secret.gettype())"
}
$signature = $hmacsha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($parts[0]+"."+$parts[1]))
$encoded = ConvertTo-Base64UrlString $signature
return $encoded -eq $parts[2]
}
"none" {
return -not $parts[2] # Must not have the signature part
}
default {
throw 'The algorithm is not one of the supported: "RS256", "HS256", "none"'
}
}
}
Set-Alias -Name "Verify-JwtSignature" -Value "Test-Jwt" -Description "An alias, using non-standard verb"
# * JWT functions - Stop
# ******************************************************************************************************************************************************************************************
# ******************************************************************************************************************************************************************************************
# * Main Script start
$CertificatePath = "cert:\localmachine\my\$CertThumbPrint" #<Add the Certificate Path Including Thumbprint here e.g. cert:\currentuser\my\6C1EE1A11F57F2495B57A567211220E0ADD72DC1 >#
$Cert = Get-Item -Path $CertificatePath
# *** use this (and comment out the above line) if you want to load the cert from a pfx file instead of getting it from the machine cert store
#$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2((join-path -path $PSScriptRoot -ChildPath "ExtractContractsFrom-ITSystemMBX.pfx"),"<PFX password>")
$Method = "Post"
$AuthenticationUrl = "https://login.microsoft.com"
$GrantType = "client_credentials"
$TokenUri = "oauth2/v2.0/token"
$uri = "$AuthenticationUrl/$TenantID/$TokenUri"
# *** Generate JWT ***
$ClientID = $AppID
$CertificateBase64Hash = [System.Convert]::ToBase64String($cert.GetCertHash())
# Use the CertificateBase64Hash and replace/strip to match web encoding of base64
$x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
$exp = get-date (get-date).add((new-timespan -Seconds 150)) -UFormat %s # *** Allow for 150 seconds usage of token ***
$nbf = get-date (get-date).Subtract((new-timespan -Seconds 10)) -UFormat %s # *** Allow for 10 seconds clock skew between your clock and azure ***
$iat = get-date -UFormat %s # *** Issued at NOW ***
$iss = $ClientID
$sub = $ClientID
$jti = New-Guid
$header = #"
{
"alg": "RS256",
"typ": "JWT",
"x5t": "$x5t"
}
"#
$PayloadJSON = #"
{
"aud": "$uri",
"exp": $exp,
"nbf": $nbf,
"iss": "$iss",
"sub": "$sub",
"jti": "$jti",
"iat": $iat
}
"#
$JWT = New-Jwt -Header $header -cert $cert -PayloadJson $PayloadJSON
$client_assertion = $JWT
# *** Define body ***
$body = #{}
#$body.add("","")
$body.add("scope","https://graph.microsoft.com/.default")
$body.add("client_id",$ClientID)
$body.add("client_assertion_type","urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
$body.add("client_assertion",$client_assertion)
$body.add("grant_type",$GrantType)
$response = invoke-restmethod -uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -Method $Method
$response.access_token # *** Show the Bearer token, obtained from Azure ***
Trying to submit a message to a service bus queue I have set up, and keep getting a 401 Unauthorized return.
I've tried configuring the SAS token myself using this method
$ResourceGroupName = 'myResourceGroup'
$NameSpaceName = "serviceBusNameSpace"
$QueueName = "myQueueName"
$PolicyName = "RootManageSharedAccessKey"
$body = "test message"
$Namespace = (Get-AzServiceBusNamespace -ResourceGroupName $ResourceGroupName -Name $namespacename).Name
$key = (Get-AzServiceBusKey -ResourceGroupName $ResourceGroupName -Namespace $namespacename -Name $PolicyName).PrimaryKey
$origin = [DateTime]"1/1/1970 00:00"
$Expiry = (Get-Date).AddMinutes(5)
#compute the token expiration time.
$diff = New-TimeSpan -Start $origin -End $Expiry
$tokenExpirationTime = [Convert]::ToInt32($diff.TotalSeconds)
#Create a new instance of the HMACSHA256 class and set the key to UTF8 for the size of $Key
$hmacsha = New-Object -TypeName System.Security.Cryptography.HMACSHA256
$hmacsha.Key = [Text.Encoding]::UTF8.GetBytes($Key)
$scope = "https://$Namespace.servicebus.windows.net/"
#create the string that will be used when cumputing the hash
$stringToSign = [Web.HttpUtility]::UrlEncode($scope) + "`n" + $tokenExpirationTime
#Compute hash from the HMACSHA256 instance we created above using the size of the UTF8 string above.
$hash = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
#Convert the hash to base 64 string
$signature = [Convert]::ToBase64String($hash)
$fullResourceURI = "https://$Namespace.servicebus.windows.net/$QueueName"
#create the token
$token = [string]::Format([Globalization.CultureInfo]::InvariantCulture, `
"SharedAccessSignature sr={0}sig={1}&se={2}&skn={3}", `
[Web.HttpUtility]::UrlEncode($fullResourceURI), `
[Web.HttpUtility]::UrlEncode($signature), `
$tokenExpirationTime, $PolicyName)
$headers = #{ "Authorization" = "$token"; "Content-Type" = "application/atom+xml;type=entry;charset=utf-8" }
$uri = "https://$Namespace.servicebus.windows.net/$QueueName/messages"
$headers.Add("BrokerProperties", "{}")
#Invoke-WebRequest call.
Invoke-WebRequest -Uri $uri -Headers $headers -Method Post -Body $body -UseBasicParsing
I've also tried generating it through a built in cmdlet in Az.ServiceBus
$ResourceGroupName = 'myResourceGroup'
$NameSpaceName = "serviceBusNameSpace"
$QueueName = "myQueueName"
$PolicyName = "RootManageSharedAccessKey"
$body = "test message"
$expiry = (Get-Date).AddHours(2)
$authRule = Get-AzServiceBusAuthorizationRule -ResourceGroupName $ResourceGroupName -Namespace $NamespaceName
$token = New-AzServiceBusAuthorizationRuleSASToken -AuthorizationRuleId $authRule.Id -KeyType Primary -ExpiryTime $Expiry
$headers = #{ "Authorization" = "SharedAccessSignature $($token.SharedAccessSignature)"; "Content-Type" = "application/atom+xml;type=entry;charset=utf-8" }
$uri = "https://$Namespace.servicebus.windows.net/$QueueName/messages"
$headers.Add("BrokerProperties", "{}")
#Invoke-WebRequest call.
Invoke-WebRequest -Uri $uri -Headers $headers -Method Post -Body $body -UseBasicParsing
Both give me a 401 unauthorized error
Invoke-WebRequest : The remote server returned an error: (401) Unauthorized.
At line:9 char:17
+ ... $response = Invoke-WebRequest -Uri $uri -Headers $headers -Method Pos ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
I'm not sure what else to do. Is there a setting I need to configure for my queue within the azure portal?
Have found solution. UTC time was originally expiring token before even sending, in addition to malformed SAS signature
Final code edit below
$key = (Get-AzServiceBusKey -ResourceGroupName $ResourceGroupName -Namespace $namespacename -Name $PolicyName).PrimaryKey
$origin = [DateTime]"1/1/1970 00:00"
$Expiry = (Get-Date).AddMinutes(20)
$Expiry = $Expiry.ToUniversalTime()
#compute the token expiration time.
$diff = New-TimeSpan -Start $origin -End $Expiry
$tokenExpirationTime = [Convert]::ToInt32($diff.TotalSeconds)
$uri = "https://$Namespace.servicebus.windows.net/$QueueName/messages"
$scope = "https://$Namespace.servicebus.windows.net/$QueueName"
#create the string that will be used when cumputing the hash
$stringToSign = [Web.HttpUtility]::UrlEncode($scope) + "`n" + $tokenExpirationTime
#Create a new instance of the HMACSHA256 class and set the key to UTF8 for the size of $Key
$hmacsha = New-Object -TypeName System.Security.Cryptography.HMACSHA256
$hmacsha.Key = [Text.Encoding]::UTF8.GetBytes($Key)
#Compute hash from the HMACSHA256 instance we created above using the size of the UTF8 string above.
$hash = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
#Convert the hash to base 64 string
$signature = [Convert]::ToBase64String($hash)
#create the token
$token = [string]::Format([Globalization.CultureInfo]::InvariantCulture, `
"SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", `
[Web.HttpUtility]::UrlEncode($scope), `
[Web.HttpUtility]::UrlEncode($signature), `
$tokenExpirationTime, $PolicyName)
$headers = #{ "Authorization" = "$token"}
$headers.Add("Content-Type", "application/atom+xml;type=entry;charset=utf-8")
#Invoke-WebRequest call.
Invoke-WebRequest -Uri $uri -Headers $headers -Method Post -Body $body -UseBasicParsing
I have made some changes in your script and it is working fine.
$ResourceGroupName = 'myResourceGroup'
$Namespace = "serviceBusNameSpace"
$QueueName = "myQueueName"
$PolicyName = "RootManageSharedAccessKey"
$body = "test message"
$key = (Get-AzServiceBusKey -ResourceGroupName $ResourceGroupName -Namespace $Namespace -Name $PolicyName).PrimaryKey
$origin = [DateTime]"1/1/1970 00:00"
$Expiry = (Get-Date).AddMinutes(5)
#compute the token expiration time.
$diff = New-TimeSpan -Start $origin -End $Expiry
$tokenExpirationTime = [Convert]::ToInt32($diff.TotalSeconds)
#Create a new instance of the HMACSHA256 class and set the key to UTF8 for the size of $Key
$hmacsha = New-Object -TypeName System.Security.Cryptography.HMACSHA256
$hmacsha.Key = [Text.Encoding]::UTF8.GetBytes($Key)
#create the string that will be used when cumputing the hash
$stringToSign = [Web.HttpUtility]::UrlEncode($Namespace) + "`n" + $tokenExpirationTime
#Compute hash from the HMACSHA256 instance we created above using the size of the UTF8 string above.
$hash = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
#Convert the hash to base 64 string
$signature = [Convert]::ToBase64String($hash)
#create the token
$token = [string]::Format([Globalization.CultureInfo]::InvariantCulture, `
"SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", `
[Web.HttpUtility]::UrlEncode($Namespace), `
[Web.HttpUtility]::UrlEncode($signature), `
$tokenExpirationTime, $PolicyName)
$headers = #{ "Authorization" = "$token"; "Content-Type" = "application/atom+xml;type=entry;charset=utf-8" }
$uri = "https://$Namespace.servicebus.windows.net/$QueueName/messages"
$headers.Add("BrokerProperties", "{}")
#Invoke-WebRequest call.
Invoke-WebRequest -Uri $uri -Headers $headers -Method Post -Body $body -UseBasicParsing
The changes which I have made are:
You don't need to create scope variable. You need to pass the $Namespace to stringToSign.
You don't need to use Get-AzServiceBusNamespace to get namespace name as you are already taking this as user input.
See post edit.
Token expiration time wasn't converted to UTC, making it always expired, in addition to not having the SaS token configuration string formed correctly.
I tried to use the below uri for REST call, but getting error(403 Forbidden)
https://$storageAccount.table.core.windows.net/$tableName()?$filter=PartitionKey%20eq%20'Key1'
Is there other way? Please help.
According to my test, we can use share key to call the Azure table rest api
$accesskey="<storage account key>"
$storageAccount = "<account name>"
$version = "2017-04-17"
$resource = "table name"
$key="Jim"
$table_url = "https://$storageAccount.table.core.windows.net/$($resource)?`$filter=PartitionKey%20eq%20'$($key)'"
# create share key
$GMTTime = (Get-Date).ToUniversalTime().AddYears(1).toString('R')
$stringToSign = "$GMTTime`n/$storageAccount/$resource"
$hmacsha = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha.key = [Convert]::FromBase64String($accesskey)
$signature = $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($stringToSign))
$signature = [Convert]::ToBase64String($signature)
$headers = #{
'x-ms-date' = $GMTTime
"Authorization" = "SharedKeyLite " + $storageAccount + ":" + $signature
"x-ms-version" = $version
"Accept" = "application/json"
}
$item = Invoke-RestMethod -Method GET -Uri $table_url -Headers $headers -ContentType application/json
$item.value
Update
Regarding how to create sas token via Azure Portal, please refer to the following steps
Create sas token
Test
GET https://myaccount.table.core.windows.net/mytable()
?$filter=<>
&sv=2019-02-02&ss=t&srt=o&sp=r&se=2020-03-27T13:01:24Z&st=2020-03-27T05:01:24Z&spr=https&sig=OFUNXShu6kTojIp3SU...TkG%2BXAVZXJ8sqc%3D
I'm trying to convert some EWS scripts to use oauth. I've found that it works when the Azure registered app has been given application permission full_access_as_app, but not when its only set to delegated permissions EWS.AccessAsUser.All . It seems that if full_access_as_app is given then anyone with the appID and secret can access any mailbox. When EWS.AccessAsUser.All is given no one can access any mailbox. What I was hoping for is that the appID and secret were essentially keys to the gate, but then access to a mailbox would be based on if the particular user running the script, or the credentials passed with it (which I don't know how to do unless it's the logged on user), has permissions to the mailbox. So, essentially, you get in the gate, but then you use impersonation. It also doesn't seem that the app secret can be held in a secure hash file like credentials can, so the secret is exposed. Thank you for your help.
## Request an access token
# Define AppId, secret and scope, your tenant name and endpoint URL
$AppId = 'AppId'
$AppSecret = 'AppSecret'
$Scope = "https://outlook.office365.com/.default"
$TenantName = "domain.onmicrosoft.com"
$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"
# Add System.Web for urlencode
Add-Type -AssemblyName System.Web
# Create body
$Body = #{
client_id = $AppId
client_secret = $AppSecret
scope = $Scope
grant_type = 'client_credentials'
}
# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = #{
ContentType = 'application/x-www-form-urlencoded'
Method = 'POST'
# Create string by joining bodylist with '&'
Body = $Body
Uri = $Url
}
# Request the token!
$Request = Invoke-RestMethod #PostSplat
#######################
$Email = "UserA#domain.com"
# Import "Microsoft Exchange Web Services Managed API 2.2"
Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll"
## Create the Exchange Service object with Oauth creds
$Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService -ArgumentList Exchange2013_SP1
$service.Url= new-object Uri("https://outlook.office365.com/EWS/Exchange.asmx")
$Service.TraceEnabled = $true
$service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$Email)
$service.HttpHeaders.Add("X-AnchorMailbox", $Email)
$Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($Request.access_token)
$OAuthCredentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($Request.access_token)
$service.Credentials = $OAuthCredentials
#####################
# WellKnown folders to adjust
$folderNames = "Inbox"
Foreach($folderName in $folderNames)
{
# Set the WellKnownFolder
$FolderId = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$folderName
# Bind to WellKnownFolder Notes
$folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service, $folderId)
Write-Host "$($Email): $($folderName): " -NoNewline
$folder.archivetag.RetentionId.Guid
} # Foreach($folderName... END
######################
You should change to use delegated permission. With delegated permission, you will be able to access the API as a specific user.
So, you need to add EWS.AccessAsUser.All permission, and get a token as following:
## Request an access token
# Define AppId, secret and scope, your tenant name and endpoint URL
$AppId = 'your app id'
$AppSecret = 'your app key'
$Scope = "https://outlook.office365.com/.default"
$TenantName = "{your_tenant}.onmicrosoft.com"
$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"
# Add System.Web for urlencode
Add-Type -AssemblyName System.Web
# Create body
$Body = #{
client_id = $AppId
client_secret = $AppSecret
scope = $Scope
grant_type = 'password'
username = 'your user name here, e.g.,jack#hanxia.onmicrosoft.com'
password = 'your password here ******************'
}
# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = #{
ContentType = 'application/x-www-form-urlencoded'
Method = 'POST'
# Create string by joining bodylist with '&'
Body = $Body
Uri = $Url
}
# Request the token for user!
$Request = Invoke-RestMethod #PostSplat
$Request.access_token
I've tried to authenticate to Azure Batch using REST API, to do so I wrote following PowerShell code
$Key = 'key'
$region = "region"
$sharedKey = [System.Convert]::FromBase64String($Key)
$date = [System.DateTime]::UtcNow.ToString("R")
$stringToSign = "GET`n`n`n`n`n`n`n`n`n`n`n`nocp-date:$date`n /$batchAccount/jobs`napi-version:2019-08-01.10.0`ntimeout:20"
[byte[]]$dataBytes = ([System.Text.Encoding]::UTF8).GetBytes($stringToSign)
$hmacsha256 = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha256.Key = [Convert]::FromBase64String($key)
$sig = [Convert]::ToBase64String($hmacsha256.ComputeHash($dataBytes))
$authhdr = "SharedKey $BatchAccount`:$sig"
$headers = #{
"ocp-date" = $date;
"Authorization" = "$authhdr";
}
Invoke-restmethod -Headers $headers -Uri 'https://$BatchAccount.$region.batch.azure.com/jobs?api-version=2019-08-01.10.0'
please note that I know that I can
use OAuth2 as alternative authentication mechanism
use Az.Batch powershell modules
I just wanted to do this using REST and SharedKey scheme as described here
https://learn.microsoft.com/en-us/rest/api/batchservice/authenticate-requests-to-the-azure-batch-service
for this API
https://learn.microsoft.com/en-us/rest/api/batchservice/job/list
But for some reason it doesn't work
I get this error but everything seems to be folowing the docs
"message":{
"lang":"en-US",
"value":"Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.\nRequestId:eb5134f2-2821-4244-ac5b-066bf19bec10\nTime:2019-11-24T21:08:20.3223384Z"
},
"values":[
{
"key":"AuthenticationErrorDetail",
"value":"The MAC signature found in the HTTP request 'signature-goes-here' is not the same as any computed signature. Server used following string to sign: 'GET\n\n\n\n\napplication/json; odata=minimalmetadata; charset=utf-8\n\n\n\n\n\n\nocp-date:Sun, 24 Nov 2019 21:08:20 GMT\n/name-goes-here/jobs\napi-version:2019-08-01.10.0'."
}
]
There is something wrong with $stringToSign . Try this :
$Key = "your key"
$region = "your region"
$BatchAccount = "your account name"
$BatchAccountURL = "Https://$BatchAccount.$region.batch.azure.com"
$sharedKey = [System.Convert]::FromBase64String($Key)
$date = [System.DateTime]::UtcNow.ToString("R")
$stringToSign = "GET`n`n`n`n`n`n`n`n`n`n`n`nocp-date:$date`n/$BatchAccount/jobs`napi-version:2019-08-01.10.0"
[byte[]]$dataBytes = ([System.Text.Encoding]::UTF8).GetBytes($stringToSign)
$hmacsha256 = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha256.Key = [Convert]::FromBase64String($key)
$sig = [Convert]::ToBase64String($hmacsha256.ComputeHash($dataBytes))
$authhdr = "SharedKey $BatchAccount`:$sig"
$headers = #{
"ocp-date" = $date;
"Authorization" = "$authhdr";
}
Invoke-restmethod -Headers $headers -Uri "$BatchAccountURL/jobs?api-version=2019-08-01.10.0"
Result :