JWT.psm1
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 "Signing with SHA256 and Pkcs1 padding failed using private key $rsa" } } } "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 "Signing with HMACSHA256 failed" } } "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" |