Public/New-JwtToken.ps1

function New-JwtToken
{
    <#
        .SYNOPSIS
            Creates a JWT token.
        .DESCRIPTION
            Creates a JWT token.
        .PARAMETER Algorithm
            A string containing the algorithm to use.
        .PARAMETER Type
            A string containing the token type.
        .PARAMETER Issuer
            A string containing the issuer to use.
        .PARAMETER Subject
            A string containing the subject to use.
        .PARAMETER Audience
            A string containing the audience to use.
        .PARAMETER Expiration
            An integer representing either the number of seconds for which the token will be valid, or the value of the UNIX epoch time when the token will not be valid anymore.
        .PARAMETER Claims
            An object, hastable or JSON string representing the claims to add or set to the header (based on reserved header claims) or payload.
            If a claim was already set using a parameter, it will be overriden.
            If algorithm (alg) is specified throught a claim, it will be ignored. Algorithm must be set by Algorithm parameter.
        .PARAMETER Secret
            A string, secure string or certificate private key (for RSA) used to sign the token.
        .PARAMETER KeyId
            A string containing the key ID.
        .PARAMETER Certificate
            An x509 certificate object used to sign the token if RSA algorithm is specified. The certificate object must contain the private key.
            Using a certificate implicitely adds the claim 'x5t' (Base64 encoded thumbprint of the certificate) to the header.
        .OUTPUTS
            System.String
            Returns a String containing the raw token.
        .EXAMPLE
            New-JwtToken -Algorithm HS256 -Issuer $TokenIssuer -Audience $TokenAudience -Secret $MySecret
 
            Description
            -----------
            This example will return a string containing the token.
        .EXAMPLE
            $Cert = Get-ChildItem Cert:\LocalMachine\my\CERTIFICATETHUMBPRINT
            $PrivateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Cert)
            New-JwtToken -Algorithm RS256 -Claims @{ iss = $TokenIssuer ; aud = $TokenAudience } -Expiration 3600 -Secret $PrivateKey
 
            Description
            -----------
            This example will return a string containing the token.
        .EXAMPLE
            New-JwtToken -Algorithm RS256 -Claims '{"aud":"audience","iss":"issuer","sub":"subject"}' -Certificate (Get-ChildItem Cert:\LocalMachine\my\CERTIFICATETHUMBPRINT)
 
            Description
            -----------
            This example will return a string containing the token.
        .LINK
            https://tools.ietf.org/html/rfc7519
        .LINK
            https://jwt.io/
    #>

    [CmdLetBinding(DefaultParameterSetName = "FromSecret")]
    param(
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $true)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $true)]
        [ValidateSet("HS256", "HS384", "HS512", "RS256", "RS384", "RS512")]
        [Alias("alg")]
        [string] $Algorithm,
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("typ")]
        [string] $Type = "JWT",
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("iss")]
        [string] $Issuer = "",
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("sub")]
        [string] $Subject = "",
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("aud")]
        [string] $Audience = "",
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("exp", "val", "Validity", "ValidFor")]
        [int] $Expiration = 3600,
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("c")]
        $Claims,
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $true)]
        [ValidateScript( {
                if (($Algorithm -like "RS*") -and ($_.GetType().FullName -notin "System.Security.Cryptography.AsymmetricAlgorithm", "System.Security.Cryptography.RSACng")) { throw $script:LocalizedData.NewJwtToken.Error.NotPrivateKey }
                else { $true }
            })]
        [Alias("sec", "key", "pk")]
        $Secret,
        [Parameter(ParameterSetName = "FromSecret", Mandatory = $false)]
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $false)]
        [Alias("kid")]
        [string] $KeyId = "",
        [Parameter(ParameterSetName = "FromCertificate", Mandatory = $true)]
        [ValidateScript( {
                if ($Algorithm -like "RS*")
                { 
                    if (!([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($_))) { throw $script:LocalizedData.NewJwtToken.Error.NoPrivateKey }
                    else { $true }
                }
                else { throw $script:LocalizedData.NewJwtToken.Error.NotRsa }
            })]
        [Alias("cert")]
        [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate
    )

    begin
    {
        Write-Debug ($script:LocalizedData.Global.Debug.Entering -f $PSCmdlet.MyInvocation.MyCommand)
    }
    process
    {
        try
        {
            Write-Debug ($script:LocalizedData.NewJwtToken.Debug.BuildHeader -f $Algorithm)
            $Header = [ordered ]@{
                alg = $Algorithm;
                typ = $Type
            }
            
            if ($KeyId) { $Header.Add("kid", $KeyId) }
            if ($PSCmdlet.ParameterSetName -eq "FromCertificate")
            {
                $Secret = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
                $Header.Add("x5t", [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Certificate.Thumbprint)).Split('=')[0].Replace('+', '-').Replace('/', '_'))
            }

            if ((Get-Date "1970-01-01").AddSeconds($Expiration) -gt (Get-Date)) { $Expiry = $Expiration }
            else
            {
                Write-Debug ($script:LocalizedData.NewJwtToken.Debug.BuildExpiration -f $Expiration)
                $Expiry = Get-Date "$((Get-Date).AddSeconds($Expiration).ToUniversalTime())" -Uformat %s
                Write-Debug ($script:LocalizedData.NewJwtToken.Debug.Expiry -f $Expiry)
            }
            
            Write-Debug $script:LocalizedData.NewJwtToken.Debug.BuildPayload
            $Payload = @{
                exp = $Expiry
            }
            
            if ($Issuer) { $Payload.Add("iss", $Issuer) }
            if ($Subject) { $Payload.Add("sub", $Subject) }
            if ($Audience) { $Payload.Add("aud", $Audience) }
            
            if ($Claims)
            {
                Write-Debug ($script:LocalizedData.NewJwtToken.Debug.ClaimsType -f $Claims.GetType().Name)
                switch ($Claims.GetType().Name)
                {
                    "String"
                    {
                        try { $Claims = $Claims | ConvertFrom-Json -ErrorAction Stop }
                        catch { Write-Error -Message ($script:LocalizedData.NewJwtToken.Error.NotJson.Message -f $Claims) -Category InvalidData -CategoryActivity $MyInvocation.MyCommand -TargetType $script:LocalizedData.NewJwtToken.Error.NotJson.Target -TargetName $Claims -Exception InvalidDataException }
                    }
                    default { $Claims = $Claims | ConvertTo-Json -Compress | ConvertFrom-Json }
                }
                $Claims | Get-Member -MemberType NoteProperty | Where-Object { $Claims."$($_.Name)" -and ($_.Name -ne "alg") } | ForEach-Object {
                    $CurrentClaim = $_
                    Write-Verbose ($script:LocalizedData.NewJwtToken.Verbose.AddClaim -f $CurrentClaim.Name, $Claims."$($CurrentClaim.Name)")
                    if ($CurrentClaim.Name -in $Script:TokenHeaderClaims) 
                    {
                        if ($Header.Keys -contains $CurrentClaim.Name) { $Header[$CurrentClaim.Name] = $Claims."$($CurrentClaim.Name)" }
                        else { $Header.Add($CurrentClaim.Name, $Claims."$($CurrentClaim.Name)") }
                    }
                    else
                    {
                        if ($Payload.Keys -contains $CurrentClaim.Name) { $Payload[$CurrentClaim.Name] = $Claims."$($CurrentClaim.Name)" }
                        else { $Payload.Add($CurrentClaim.Name, $Claims."$($CurrentClaim.Name)") }
                    }
                }
            }

            $Header = $Header | ConvertTo-Json -Compress
            $Payload = $Payload | ConvertTo-Json -Compress
            Write-Debug ($script:LocalizedData.NewJwtToken.Debug.HeaderToJson -f $Header)
            Write-Debug ($script:LocalizedData.NewJwtToken.Debug.PayloadToJson -f $Payload)
            
            if ($Secret.GetType().Name -eq "SecureString")
            {
                Write-Debug $script:LocalizedData.NewJwtToken.Debug.SecretAsString
                $Credential = New-Object -TypeName pscredential("jwt", $Secret)
                $Secret = $Credential.GetNetworkCredential().Password
            }
            
            $EncodedHeader = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Header)).Split('=')[0].Replace('+', '-').Replace('/', '_')
            $EncodedPayload = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($Payload)).Split('=')[0].Replace('+', '-').Replace('/', '_')
            Write-Debug ($script:LocalizedData.NewJwtToken.Debug.HeaderToBase64 -f $EncodedHeader)
            Write-Debug ($script:LocalizedData.NewJwtToken.Debug.PayloadToBase64 -f $EncodedPayload)

            $ToBeSigned = [System.Text.Encoding]::ASCII.GetBytes("$EncodedHeader.$EncodedPayload")

            $SigningAlgorithm = switch ($Algorithm)
            {
                "HS256" { New-Object System.Security.Cryptography.HMACSHA256 }
                "HS384" { New-Object System.Security.Cryptography.HMACSHA384 }
                "HS512" { New-Object System.Security.Cryptography.HMACSHA512 }
                "RS256" { [Security.Cryptography.HashAlgorithmName]::SHA256 }
                "RS384" { [Security.Cryptography.HashAlgorithmName]::SHA384 }
                "RS512" { [Security.Cryptography.HashAlgorithmName]::SHA512 }
            }

            switch -Regex ($Algorithm)
            {
                "HS"
                {
                    $SigningAlgorithm.Key = [System.Text.Encoding]::ASCII.GetBytes($Secret)
                    $Signature = $SigningAlgorithm.ComputeHash($ToBeSigned)
                }
                "RS"
                {
                    $Signature = $Secret.SignData($ToBeSigned, $SigningAlgorithm, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
                }
            }
            
            $Signature = [Convert]::ToBase64String($Signature).Split('=')[0].Replace('+', '-').Replace('/', '_')
            Write-Debug ($script:LocalizedData.NewJwtToken.Debug.Signature -f $Signature)
            $Token = "$EncodedHeader.$EncodedPayload.$Signature"
            $Token
        }
        catch
        {
            Write-Error $_
        }
    }
    end
    {
        Write-Debug ($script:LocalizedData.Global.Debug.Leaving -f $PSCmdlet.MyInvocation.MyCommand)
    }
}