JWT.psm1

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.
 
.PARAMETER Cert
Specifies the signing certificate. Mandatory.
 
.PARAMETER Header
Specifies a JWT header. Optional. Defaults to '{"alg":"RS256","typ":"JWT"}'.
 
.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=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert
    )

    Write-Verbose "Payload to sign: $PayloadJson"
    Write-Verbose "Signing certificate: $($Cert.Subject)"

    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 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Header)) -replace '\+','-' -replace '/','_' -replace '='
    $encodedPayload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($PayloadJson)) -replace '\+','-' -replace '/','_' -replace '='

    $jwt = $encodedHeader + '.' + $encodedPayload # The first part of the JWT

    $toSign = [System.Text.Encoding]::UTF8.GetBytes($jwt)
    
    $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 = [Convert]::ToBase64String($rsa.SignData($toSign,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)) -replace '\+','-' -replace '/','_' -replace '=' }
        catch { throw "Signing with SHA256 and Pkcs1 padding failed using private key $rsa" }
    }

    $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 a signing certificate. Assumes SHA-256 hashing algorithm. Optionally produces the original signed JSON payload.
 
.PARAMETER Payload
Specifies the JWT. Mandatory string.
 
.PARAMETER Cert
Specifies the signing certificate. Mandatory X509Certificate2.
 
.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=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert
    )

    Write-Verbose "Verifying JWT: $jwt"
    Write-Verbose "Using certificate with subject: $($Cert.Subject)"

    $parts = $jwt.Split('.')

    if ($OutputJSON) {
        $OutputJSON.value = [Convert]::FromBase64String($parts[1].replace('-','+').replace('_','/'))
    }

    $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
    
    $signed = $parts[2].replace('-','+').replace('_','/') # Decoding Base64url to the original byte array
    $mod = $signed.Length % 4
    switch ($mod) {
        0 { $signed = $signed }
        1 { $signed = $signed.Substring(0,$signed.Length-1) }
        2 { $signed = $signed + "==" }
        3 { $signed = $signed + "=" }
    }
    $bytes = [Convert]::FromBase64String($signed) # Conversion completed

    return $cert.PublicKey.Key.VerifyHash($computed,$bytes,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1) # Returns True if the hash verifies successfully

}

New-Alias -Name "Verify-JwtSignature" -Value "Test-Jwt" -Description "An alias, using non-standard verb"

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 Payload
    Specifies the JWT. Mandatory string.
     
    .INPUTS
    You can pipe JWT as a string object to Get-JwtPayload.
     
    .OUTPUTS
    String. Get-JwtPayload returns $true if the signature successfully verifies.
     
    .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 = $parts[1].replace('-','+').replace('_','/') # Decoding Base64url to the original byte array
        $mod = $payload.Length % 4
        switch ($mod) {
            # 0 { $payload = $payload } - do nothing
            1 { $payload = $payload.Substring(0,$payload.Length-1) }
            2 { $payload = $payload + "==" }
            3 { $payload = $payload + "=" }
        }
        $bytes = [Convert]::FromBase64String($payload) # Conversion completed

        return [System.Text.Encoding]::UTF8.GetString($bytes)
    
    }