Skip to content

Instantly share code, notes, and snippets.

@kevinblumenfeld
Forked from dindoliboon/Jwt-CreateToken.ps1
Created September 26, 2020 03:06
Show Gist options
  • Save kevinblumenfeld/c53fe3805de813fe88890b0f0ab8046b to your computer and use it in GitHub Desktop.
Save kevinblumenfeld/c53fe3805de813fe88890b0f0ab8046b to your computer and use it in GitHub Desktop.
System.IdentityModel.Tokens.Jwt with PowerShell to create JWT using RS256
<#
example01-create-jwt.ps1: Create JWT with RS256 signature.
http://blog.d-apps.com/2013/08/powershell-and-json-web-token-handler.html
Tested using:
Windows 10.0.18362.628
PowerShell 7.0.0
Windows PowerShell 5.1.18362.628
NuGet 5.4.0.6315
System.IdentityModel.Tokens.Jwt 5.6.0
#>
# Download latest NuGet tool.
Invoke-WebRequest -Uri 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile "$env:USERPROFILE\Downloads\nuget.exe"
# Download System.IdentityModel.Tokens.Jwt 5.6.0 dependency.
& "$env:USERPROFILE\Downloads\nuget.exe" Install System.IdentityModel.Tokens.Jwt -Version 5.6.0 -OutputDirectory "$env:USERPROFILE\Downloads\.nuget"
# Import Jwt-CreateToken function.
Invoke-Expression -Command (Invoke-WebRequest -Uri 'https://gist.githubusercontent.com/dindoliboon/a4deeaafa9b126026e3139386c245f8e/raw/Jwt-CreateToken.ps1' -UseBasicParsing).Content
# Create self-signed certificate PFX.
$generateCertBlock = {
# Error: check to make sure the SignatureAlgorithm is supported
# Workaround: https://stackoverflow.com/a/52063928
$cert = New-SelfSignedCertificate -DnsName '20200318-2236@developer.contoso.com' -CertStoreLocation 'Cert:\CurrentUser\My' -Provider 'Microsoft Enhanced RSA and AES Cryptographic Provider'
$certPass = ConvertTo-SecureString -String 'notasecret' -AsPlainText -Force
Export-PfxCertificate -FilePath "$env:USERPROFILE\Downloads\test2.pfx" -Cert $cert -Password $certPass | Out-Null
Write-Output -InputObject $cert
}
if ($PSVersionTable.PSEdition -eq 'Desktop') {
$cert = Invoke-Command -ScriptBlock $generateCertBlock
Add-Type -Path "$env:USERPROFILE\.nuget\packages\newtonsoft.json\10.0.1\lib\net45\Newtonsoft.Json.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Tokens\5.6.0\lib\net45\Microsoft.IdentityModel.Tokens.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\System.IdentityModel.Tokens.Jwt\5.6.0\lib\net45\System.IdentityModel.Tokens.Jwt.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Logging\5.6.0\lib\net45\Microsoft.IdentityModel.Logging.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.JsonWebTokens\5.6.0\lib\net45\Microsoft.IdentityModel.JsonWebTokens.dll"
} else {
# Error: Export-PfxCertificate: Cannot export non-exportable private key.
# Workaround: https://github.com/PowerShell/PowerShell/issues/12081#issuecomment-596777834
Import-Module -Name PKI -UseWindowsPowerShell
$winps = Get-PSSession -Name WinPSCompatSession
$cert = Invoke-Command -Session $winps -ScriptBlock $generateCertBlock
Add-Type -Path "$env:USERPROFILE\.nuget\packages\newtonsoft.json\10.0.1\lib\netstandard1.3\Newtonsoft.Json.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Tokens\5.6.0\lib\netstandard2.0\Microsoft.IdentityModel.Tokens.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\System.IdentityModel.Tokens.Jwt\5.6.0\lib\netstandard2.0\System.IdentityModel.Tokens.Jwt.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Logging\5.6.0\lib\netstandard2.0\Microsoft.IdentityModel.Logging.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.JsonWebTokens\5.6.0\lib\netstandard2.0\Microsoft.IdentityModel.JsonWebTokens.dll"
}
# Use certificate stored in CurrentUser\My store.
$claims = New-Object -TypeName System.Collections.Generic.List[System.Security.Claims.Claim]
$claims.Add((New-Object -TypeName System.Security.Claims.Claim('scope', 'https://www.contoso.com/api/users')))
$encToken = Jwt-CreateToken -Issuer '20200318-2236@developer.contoso.com' -Audience 'https://accounts.contoso.com/oauth2/token' -Claims $claims -Certificate $cert.Thumbprint
$decToken = (New-Object -TypeName System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler).ReadToken($encToken)
$decToken | Format-List
$decToken.Claims | Format-List
# Use certificate stored in PFX.
$pwd = ConvertTo-SecureString -String 'notasecret' -AsPlainText -Force
$encToken = Jwt-CreateToken -Issuer '20200318-2236@developer.contoso.com' -Audience 'https://accounts.contoso.com/oauth2/token' -Certificate "$env:USERPROFILE\Downloads\test2.pfx" -SigningCertificatePassword $pwd
$decToken = (New-Object -TypeName System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler).ReadToken($encToken)
$decToken | Format-List
$decToken.Claims | Format-List
<#
example02-google-admin-sdk-jwt.ps1: Accessing Google Admin SDK Directory API using JWT with RS256 signature.
You will need to create a service account on Google Cloud Platform.
See https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount
Tested using:
Windows 10.0.18362.628
PowerShell 7.0.0
Windows PowerShell 5.1.18362.628
NuGet 5.4.0.6315
System.IdentityModel.Tokens.Jwt 5.6.0
#>
# Download latest NuGet tool.
Invoke-WebRequest -Uri 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile "$env:USERPROFILE\Downloads\nuget.exe"
# Download System.IdentityModel.Tokens.Jwt 5.6.0 dependency.
& "$env:USERPROFILE\Downloads\nuget.exe" Install System.IdentityModel.Tokens.Jwt -Version 5.6.0 -OutputDirectory "$env:USERPROFILE\Downloads\.nuget"
# Import Jwt-CreateToken function.
Invoke-Expression -Command (Invoke-WebRequest -Uri 'https://gist.githubusercontent.com/dindoliboon/a4deeaafa9b126026e3139386c245f8e/raw/Jwt-CreateToken.ps1' -UseBasicParsing).Content
# Import dependencies.
if ($PSVersionTable.PSEdition -eq 'Desktop') {
Add-Type -Path "$env:USERPROFILE\.nuget\packages\newtonsoft.json\10.0.1\lib\net45\Newtonsoft.Json.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Tokens\5.6.0\lib\net45\Microsoft.IdentityModel.Tokens.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\System.IdentityModel.Tokens.Jwt\5.6.0\lib\net45\System.IdentityModel.Tokens.Jwt.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Logging\5.6.0\lib\net45\Microsoft.IdentityModel.Logging.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.JsonWebTokens\5.6.0\lib\net45\Microsoft.IdentityModel.JsonWebTokens.dll"
} else {
Add-Type -Path "$env:USERPROFILE\.nuget\packages\newtonsoft.json\10.0.1\lib\netstandard1.3\Newtonsoft.Json.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Tokens\5.6.0\lib\netstandard2.0\Microsoft.IdentityModel.Tokens.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\System.IdentityModel.Tokens.Jwt\5.6.0\lib\netstandard2.0\System.IdentityModel.Tokens.Jwt.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.Logging\5.6.0\lib\netstandard2.0\Microsoft.IdentityModel.Logging.dll"
Add-Type -Path "$env:USERPROFILE\.nuget\packages\Microsoft.IdentityModel.JsonWebTokens\5.6.0\lib\netstandard2.0\Microsoft.IdentityModel.JsonWebTokens.dll"
}
# Get access token.
#
# Using OAuth 2.0 for Server to Server Applications
# https://developers.google.com/identity/protocols/oauth2/service-account
$claims = New-Object -TypeName System.Collections.Generic.List[System.Security.Claims.Claim]
$claims.Add((New-Object -TypeName System.Security.Claims.Claim('scope', 'https://www.googleapis.com/auth/admin.directory.user.readonly')))
$claims.Add((New-Object -TypeName System.Security.Claims.Claim('sub', '20200319-1453-admin@contoso.com')))
$pwd = ConvertTo-SecureString -String 'notasecret' -AsPlainText -Force
$encToken = Jwt-CreateToken -Issuer '20200319-1453-admin@contoso.iam.gserviceaccount.com' -Audience 'https://oauth2.googleapis.com/token' -Claims $claims -Certificate 'C:\Users\Kiosk\Downloads\test-ps-jwt-45605-9466f04606d4.p12' -SigningCertificatePassword $pwd
$payload = @{
grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
assertion = $encToken
}
$googleOAuthResult = Invoke-RestMethod -Method Post -Uri 'https://oauth2.googleapis.com/token' -ContentType 'application/x-www-form-urlencoded' -Body $payload
# Use our access token and retrieve a user.
#
# G Suite Admin SDK Directory API, Users: get
# https://developers.google.com/admin-sdk/directory/v1/reference/users/get
$payload = @{
Authorization = 'Bearer ' + $googleOAuthResult.access_token
}
$apiResult = Invoke-RestMethod -Method Get -Uri 'https://www.googleapis.com/admin/directory/v1/users/20200319-1453-user@contoso.com' -Headers $payload
$apiResult | Format-List
<#
System.IdentityModel.Tokens.Jwt with PowerShell to create JWT using RS256.
http://blog.d-apps.com/2013/08/powershell-and-json-web-token-handler.html
Tested using:
Windows 10.0.18362.628
PowerShell 7.0.0
Windows PowerShell 5.1.18362.628
NuGet 5.4.0.6315
System.IdentityModel.Tokens.Jwt 5.6.0
#>
function Jwt-CreateToken {
Param(
[String] $Issuer,
[String] $Audience,
[string] $Certificate,
[string] $CertificatePassword,
[System.Collections.Generic.List[System.Security.Claims.Claim]] $Claims = $null,
[SecureString] $SigningCertificatePassword,
[DateTime] $NotBefore,
[DateTime] $Expires
)
if ($null -eq $NotBefore) {
$NotBefore = Get-Date
}
if ($null -eq $Expires) {
$Expires = $NotBefore.AddHours(1)
}
$internalClaims = New-Object -TypeName System.Collections.Generic.List[System.Security.Claims.Claim]
# Merge user provided payload claims with internal payload claims.
if ($null -ne $Claims) {
$internalClaims.AddRange($Claims)
}
# Add IssuedAt claim.
$internalClaims.Add((New-Object -TypeName System.Security.Claims.Claim('iat', ([System.DateTimeOffset]$NotBefore).ToUnixTimeSeconds())))
if ((Test-Path -Path $Certificate)) {
# Use P12 or PFX certificate if it exists.
if ($PSBoundParameters.ContainsKey('SigningCertificatePassword')) {
$signingCertificateCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'notinuse', $SigningCertificatePassword
$plaintextSigningCertificatePassword = $signingCertificateCredential.GetNetworkCredential().Password
} else {
$plaintextSigningCertificatePassword = $CertificatePassword
}
$signingX509Certificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2($Certificate, $plaintextSigningCertificatePassword, 'Export')
} else {
# Find certificate by thumbprint ID.
$signingX509Certificate = Get-ChildItem -Path "cert:\$Certificate" -Recurse | Where-Object { $_.HasPrivateKey -eq $true } | Select-Object -First 1
}
$signingCredentials = New-Object -TypeName Microsoft.IdentityModel.Tokens.X509SigningCredentials($signingX509Certificate)
$token = New-Object -TypeName System.IdentityModel.Tokens.Jwt.JwtSecurityToken($Issuer, $Audience, $internalClaims, $NotBefore, $Expires, $signingCredentials)
return (New-Object -TypeName System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler).WriteToken($token)
}
<#
PowerShell to create JWT using RS256.
http://blog.d-apps.com/2013/08/powershell-and-json-web-token-handler.html
Workaround when using System.IdentityModel.Tokens.Jwt 1.0 with certificates smaller than 2048 bits.
Exception calling "WriteToken" with "1" argument(s): "Jwt10530: The 'System.IdentityModel.Tokens.X509AsymmetricSecurityKey' for signing cannot be smaller than '2048' bits. Parameter name: key.KeySize Actual value was 1024."
In 2013, Google's API console would generate P12 keys that was 1024-bits. In 2020, the key size is now 2048-bits.
Instead of using the proof of concept code below, check out libraries listed at https://jwt.io
Tested using:
Windows 10.0.18362.628
PowerShell 7.0.0
Windows PowerShell 5.1.18362.628
#>
function Jwt-CreateToken {
Param(
[String] $Issuer,
[String] $Audience,
[string] $Certificate,
[string] $CertificatePassword,
[System.Collections.Generic.List[System.Security.Claims.Claim]] $Claims = $null,
[SecureString] $SigningCertificatePassword,
[DateTime] $NotBefore,
[DateTime] $Expires
)
function ConvertTo-Base64Url {
Param(
$InputObject
)
$inputObjectBytes = [System.Convert]::ToBase64String($InputObject)
return $inputObjectBytes.Split('=')[0].Replace('+', '-').Replace('/', '_')
}
if ($null -eq $NotBefore) {
$NotBefore = Get-Date
}
if ($null -eq $Expires) {
$Expires = $NotBefore.AddHours(1)
}
$internalHeader = @{
alg = 'RS256'
typ = 'JWT'
}
$internalClaims = @{
iss = $Issuer
aud = $Audience
exp = ([System.DateTimeOffset]$Expires).ToUnixTimeSeconds()
iat = ([System.DateTimeOffset]$NotBefore).ToUnixTimeSeconds()
nbf = ([System.DateTimeOffset]$NotBefore).ToUnixTimeSeconds()
}
# Merge user provided payload claims with internal payload claims.
if ($null -ne $Claims) {
$claims | ForEach-Object -Process {
$internalClaims.Add($_.Type, $_.Value)
}
}
$b64Header = ConvertTo-Base64Url -InputObject ([System.Text.Encoding]::UTF8.GetBytes(($internalHeader | ConvertTo-Json -Compress)))
$b64Claims = ConvertTo-Base64Url -InputObject ([System.Text.Encoding]::UTF8.GetBytes(($internalClaims | ConvertTo-Json -Compress)))
$baseSignature = $b64Header + '.' + $b64Claims
$bytesSignature = [System.Text.Encoding]::UTF8.GetBytes($baseSignature)
if ((Test-Path -Path $Certificate)) {
# Use P12 or PFX certificate if it exists.
if ($PSBoundParameters.ContainsKey('SigningCertificatePassword')) {
$signingCertificateCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'notinuse', $SigningCertificatePassword
$plaintextSigningCertificatePassword = $signingCertificateCredential.GetNetworkCredential().Password
} else {
$plaintextSigningCertificatePassword = $CertificatePassword
}
$signingX509Certificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2($Certificate, $plaintextSigningCertificatePassword, 'Export')
} else {
# Find certificate by thumbprint ID.
$signingX509Certificate = Get-ChildItem -Path "cert:\$Certificate" -Recurse | Where-Object { $_.HasPrivateKey -eq $true } | Select-Object -First 1
}
$bytesSignedSignature = $signingX509Certificate.PrivateKey.SignData($bytesSignature, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
$signedB64Signature = ConvertTo-Base64Url -InputObject $bytesSignedSignature
return $baseSignature + '.' + $signedB64Signature
}
@sean-mcardle
Copy link

sean-mcardle commented Mar 12, 2022

Good to see I'm not the only one doing this. Thanks for the standalone. Saved me some time. Just wish I'd Googled for this before getting the signature part working.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment