ConvertFrom-JWTToken.ps1

Function ConvertFrom-JWTToken
{
<#
    .SYNOPSIS
        Get JWT token information.

    .PARAMETER JWTToken
        AccessToken string.

    .PARAMETER ValidateSignature
        Use additional signature validation.

    .PARAMETER AsJSON
        Return data as json.

    .EXAMPLE
        $AccessToken | ConvertFrom-JWTToken -AsJSON

    .EXAMPLE
        $AccessToken | ConvertFrom-JWTToken -ValidateSignature

    .NOTES
        Author: Michal Gajda

    .LINK
        https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [string]$JWTToken,
        [Switch]$ValidateSignature,
        [Switch]$AsJSON
    )

    #Check if token is ok
    if (!$JWTToken.Contains(".") -or !$JWTToken.StartsWith("eyJ"))
    {
        Write-Error "Invalid Access Token" -ErrorAction Stop
    }

    $DecodedToken = @{}
    $Header, $Payload, $Signature = $JWTToken.Split(".")

    #Decode Header
    $HeaderData = $Header.Replace('-', '+').Replace('_', '/')
    while($HeaderData.Length % 4 -ne 0) { $HeaderData += "=" }
    $DecodedHeader = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($HeaderData)) | ConvertFrom-Json
    $DecodedToken['Header'] = [PSCustomObject]$DecodedHeader

    #Decode Payload
    $PayloadData = $Payload.Replace('-', '+').Replace('_', '/')
    while($PayloadData.Length % 4 -ne 0) { $PayloadData += "=" }
    $DecodedPayload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($PayloadData)) | ConvertFrom-Json
    $DecodedToken['Payload'] = [PSCustomObject]$DecodedPayload

    #Get expiration time
    $UnixTime = (Get-Date -Year 1970 -Month 1 -Day 1 -hour 0 -Minute 0 -Second 0 -Millisecond 0)
    $UtcTime = $UnixTime.AddSeconds($DecodedToken['Payload'].exp)
    $ExpiryDateTime = $UtcTime.AddMinutes($(Get-TimeZone).GetUtcOffset($(Get-Date)).TotalMinutes)
    $DecodedToken['ExpiryDateTime'] = $ExpiryDateTime

    #Validation
    if($ValidateSignature)
    {
        $Issuer = $DecodedToken['Payload'].iss
        $Kid = $DecodedToken['Header'].kid

        #Get keys pool for tenant
        $Url = $Issuer + ".well-known/openid-configuration"
        #if($null -ne $AppId) { $Url += "?appid=" + $AppId }
        $Configuration = (ConvertFrom-Json (Invoke-WebRequest $Url).Content)
        $Keys = (ConvertFrom-Json (Invoke-WebRequest $Configuration.jwks_uri).Content).Keys
        $KeysHT = $Keys | Group-Object -Property Kid -AsHashTable

        #Get right key
        $Key = $KeysHT[$Kid]

        if($null -ne $Key)
        {
            #Get key info
            $KeyInfo = @{}
            $KeyInfo['e'] = $Key.e
            $KeyInfo['kty'] = $Key.kty
            $KeyInfo['n'] = $Key.n
            $KeyInfo['kid'] = $Key.kid

            #Decode public cert
            if($Key.x5c)
            {
                $X5C = $Key.x5c | Select-Object -First 1
                $X509Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($X5C))
                $KeyInfo['X509Certificate'] = $X509Certificate
            }
            $DecodedToken['KeyInfo'] = [PSCustomObject]$KeyInfo

            #Validate Signature
            $RSA = New-Object System.Security.Cryptography.RSACryptoServiceProvider
            $RSAParameters = [System.Security.Cryptography.RSAParameters]::New()

            $KaynData = $Key.n.Replace('-', '+').Replace('_', '/')
            while($KaynData.Length % 4 -ne 0) { $KaynData += "=" }
            $RSAParameters.Modulus = [Convert]::FromBase64String($KaynData)

            $KayeData = $Key.e.Replace('-', '+').Replace('_', '/')
            while($KayeData.Length % 4 -ne 0) { $KayeData += "=" }
            $RSAParameters.Exponent = [System.Convert]::FromBase64String($KayeData)

            $RSA.ImportParameters($RSAParameters)

            $SignatureDeformatter = [System.Security.Cryptography.RSAPKCS1SignatureDeformatter]::new($RSA)
            $SignatureDeformatter.SetHashAlgorithm("SHA256")

            #Get hash of header and payload
            $SHA256 = [System.Security.Cryptography.SHA256]::Create()
            $Hash = $SHA256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Header + '.' + $Payload))

            #Decode signature
            $SignatureData = $Signature.Replace('-', '+').Replace('_', '/')
            while($SignatureData.Length % 4 -ne 0) { $SignatureData += "=" }
            $SignatureBytes = [System.Convert]::FromBase64String($SignatureData)

            #Verify signature
            if($SignatureDeformatter.VerifySignature($Hash, $SignatureBytes))
            {
                $DecodedToken['SignatureStatus'] = "Signature Verified"
            } else
            {
                if($TokenClaims.aud -eq "https://graph.microsoft.com")
                {
                    $DecodedToken['SignatureStatus'] = "Cant validate MSGraph token signature"
                } else
                {
                    $DecodedToken['SignatureStatus'] = "Invalid Signature"
                }
            }
        }
    }

    if($AsJSON)
    {
        $Result = [PSCustomObject]$DecodedToken | ConvertTo-Json
    } else {
        $Result = [PSCustomObject]$DecodedToken
    }

    Return $Result
}