繁体   English   中英

微软图形; Powershell; 如何使用 invoke-restmethod 获取访问令牌:TenantID、ClientID 和 *certificate*

[英]Microsoft Graph; Powershell; how to get access token using invoke-restmethod with: TenantID, ClientID and *certificate*

我进行了相当多的研究,但没有找到任何信息,如何使用 Powershell 通过 REST 从图形中获取访问令牌:

  • 租户 ID
  • 客户端/AppID
  • 证书

具有以下条件:

  • 使用调用休息方法

我已经看过什么了?

经过数小时的研究,我终于找到了一个可行的解决方案。 它使用 github 上的项目来创建 JWT 令牌。

非常重要的一点:不要使用证书中的证书指纹来计算 x5t,而是自己计算证书 hash(详情请查看下面的代码)。

希望,这可以帮助某人。

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 ***

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM