HashiCorp Vault - Generate JWT

Octopus.Script exported 2021-08-10 by harrisonmeister belongs to ‘HashiCorp Vault’ category.

This step template generates a Json Web Token (JWT) for use with HashiCorp Vault.

The step is based on the existing JWT - Generate JSON Web Token step template.

However, it differs as it offers less flexibility in choosing the fields to use in the generated JWT and is opinionated towards support for Vault entities and groups.

The resulting JWT will be stored as a sensitive output variable called JWT.

A private key needs to be provided that will sign the combined JWT header and payload.

Currently, the following three signing algorithms are supported:

  1. RS256 - RSASSA-PKCS1-v1_5 using SHA-256
  2. RS384 - RSASSA-PKCS1-v1_5 using SHA-384
  3. RS512 - RSASSA-PKCS1-v1_5 using SHA-512

The default is RS256.

Notes:

  • Tested on Windows and Linux (PowerShell Core)
  • Tested with Octopus 2020.1

Parameters

When steps based on the template are included in a project’s deployment process, the parameters below can be set.

JWT Private signing key

Vault.Generate.JWT.PrivateKey =

Provide the private key in PEM format to be used to sign the JWT.

Accepted headers:

  • ----BEGIN RSA PRIVATE KEY-----
  • -----BEGIN PRIVATE KEY----

Note: It’s recommended to use a sensitive variable to provide this value. If you instead enter the private key directly, the step template will attempt to detect this by replacing spaces with new lines “n`.

JWT signing algorithm

Vault.Generate.JWT.Signing.Algorithm = RS256

The JWA algorithm to use when signing the JWT. The default is RS256 (RSASSA-PKCS1-v1_5 using SHA-256).

This value is also used for the alg algorithm header.

JWT expiry time in minutes

Vault.Generate.JWT.ExpiresAfterMinutes = 20

The number of minutes after the JWT is created that it expires. This value is used for the exp expiration time claim. The default is 20 minutes.

JWT Issuer claim

Vault.Generate.JWT.Issuer =

The iss isuer claim identifies the principal that issued the JWT.

JWT Audience claim

Vault.Generate.JWT.Audience =

The aud audience claim identifies the recipients that the JWT is intended for. This is typically the base address of the resource being accessed, such as https://example-domain.com.

JWT Subject claim

Vault.Generate.JWT.Subject =

Choose from a defined list of values for the sub subject claim of the JWT.

Available options start from least granular to most granular:

  • Octopus Server Uri
  • Octopus Server Uri, and Project Group name
  • Octopus Server Uri, Project Group, and Project name
  • Octopus Server Uri, Project Group, Project and Environment name

Note: If the Server Uri property has not been set for the Octopus instance, the Octopus.Web.ServerUri variable will resolve to an empty string.

JWT Groups claim

Vault.Generate.JWT.Groups =

Choose from a defined list of values for the groups private claim that identifies groups associated with this JWT.

Available options start from least granular to most granular:

  • Octopus Server Uri
  • Octopus Server Uri, and Project Group name
  • Octopus Server Uri, Project Group, and Project name
  • Octopus Server Uri, Project Group, Project and Environment name

Note: If the Server Uri property has not been set for the Octopus instance, the Octopus.Web.ServerUri variable will resolve to an empty string.

JWT Identity Prefix (optional)

Vault.Generate.JWT.IdentityPrefix =

Optional - A prefix to be added to the values used in both the sub and groups claims.

If no value is provided, the prefix is not included.

Script body

Steps based on this template will execute the following PowerShell script.

$ErrorActionPreference = 'Stop'

# Helper functions
###############################################################################

function ConvertTo-JwtBase64 {
    param (
        $Value
    )
    if ($Value -is [string]) {
        $ConvertedValue = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) -replace '\+', '-' -replace '/', '_' -replace '='
    }
    elseif ($Value -is [byte[]]) {
        $ConvertedValue = [Convert]::ToBase64String($Value) -replace '\+', '-' -replace '/', '_' -replace '='
    }

    return $ConvertedValue
}

function ConvertTo-Subject {
    param ([string]$Subject, [string]$IdentityPrefix)
    return ConvertTo-Identity -IdentityValue $Subject -IdentityPrefix $IdentityPrefix
}

function ConvertTo-Groups {
    param ([string]$Groups, [string]$IdentityPrefix)
    return ConvertTo-Identity -IdentityValue $Groups -IdentityPrefix $IdentityPrefix
}

function ConvertTo-Identity {
    param (
        [string]$IdentityValue, 
        [string]$IdentityPrefix
    )
    $ServerUri = $OctopusParameters["Octopus.Web.ServerUri"].ToLower() -Replace "http://" -Replace "https://"
    $ProjectGroup = $OctopusParameters["Octopus.ProjectGroup.Name"].ToLower() -Replace " ", "-"
    $Project = $OctopusParameters["Octopus.Project.Name"].ToLower() -Replace " ", "-"
    $Environment = $OctopusParameters["Octopus.Environment.Name"].ToLower() -Replace " ", "-"
    $identity = ""
    switch ($VAULT_GENERATE_JWT_SUBJECT) {
        "serveruri" {
            $identity = $ServerUri
        }
        "projectgroup" {
            $identity = "$ServerUri/$ProjectGroup"
        }
        "project" {
            $identity = "$ServerUri/$ProjectGroup/$Project"
        }
        "environment" {
            $identity = "$ServerUri/$ProjectGroup/$Project/$Environment"
        }
    }
    if (![string]::IsNullOrWhiteSpace($IdentityPrefix)) {
        $identity = "$IdentityPrefix$identity"
    }
    return $identity
}

###############################################################################

# Variables
$VAULT_GENERATE_JWT_PRIVATE_KEY = $OctopusParameters["Vault.Generate.JWT.PrivateKey"]
$VAULT_GENERATE_JWT_ALGORITHM = $OctopusParameters["Vault.Generate.JWT.Signing.Algorithm"]
$VAULT_GENERATE_JWT_EXPIRES_MINS = $OctopusParameters["Vault.Generate.JWT.ExpiresAfterMinutes"]
$VAULT_GENERATE_JWT_ISSUER = $OctopusParameters["Vault.Generate.JWT.Issuer"]
$VAULT_GENERATE_JWT_SUBJECT = $OctopusParameters["Vault.Generate.JWT.Subject"]
$VAULT_GENERATE_JWT_GROUPS = $OctopusParameters["Vault.Generate.JWT.Groups"]
$VAULT_GENERATE_JWT_AUDIENCE = $OctopusParameters["Vault.Generate.JWT.Audience"]

# Optional 
$VAULT_GENERATE_JWT_IDENTITY_PREFIX = $OctopusParameters["Vault.Generate.JWT.IdentityPrefix"]

$subject = ConvertTo-Subject -Groups $VAULT_GENERATE_JWT_GROUPS -IdentityPrefix $VAULT_GENERATE_JWT_IDENTITY_PREFIX
$groups = ConvertTo-Groups -Groups $VAULT_GENERATE_JWT_GROUPS -IdentityPrefix $VAULT_GENERATE_JWT_IDENTITY_PREFIX

$audiences = @()
if (![string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_AUDIENCE)) {
    @(($VAULT_GENERATE_JWT_AUDIENCE -Split "`n").Trim()) | ForEach-Object {
        if (![string]::IsNullOrWhiteSpace($_)) {
            $audiences += $_
        }
    }
}

# Validation
if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_PRIVATE_KEY)) {
    throw "Required parameter Vault.Generate.JWT.PrivateKey not specified."
}
if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_ALGORITHM)) {
    throw "Required parameter Vault.Generate.JWT.Signing.Algorithm not specified."
}
if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_EXPIRES_MINS)) {
    throw "Required parameter Vault.Generate.JWT.ExpiresAfterMinutes not specified."
}
if ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_ISSUER)) {
    throw "Required parameter Vault.Generate.JWT.Issuer not specified."
}
if ($audiences.Length -le 0) {
    throw "Required parameter Vault.Generate.JWT.Audience not specified."
}
if ([string]::IsNullOrWhiteSpace($subject)) {
    throw "Required parameter Vault.Generate.JWT.Subject not specified."
}
if ([string]::IsNullOrWhiteSpace($groups)) {
    throw "Required parameter Vault.Generate.JWT.Groups not specified."
}


# Signing functions
###############################################################################

$RsaPrivateKey_Header = "-----BEGIN RSA PRIVATE KEY-----"
$RsaPrivateKey_Footer = "-----END RSA PRIVATE KEY-----"
$Pkcs8_PrivateKey_Header = "-----BEGIN PRIVATE KEY-----"
$Pkcs8_PrivateKey_Footer = "-----END PRIVATE KEY-----"

function ExtractPemData {
    param (
        [string]$Pem,
        [string]$Header,
        [string]$Footer
    )

    $Start = $Pem.IndexOf($Header) + $Header.Length
    $End = $Pem.IndexOf($Footer, $Start) - $Start
    $EncodedPem = ($Pem.Substring($Start, $End).Trim()) -Replace " ", "`n"
    
    $PemData = [Convert]::FromBase64String($EncodedPem)
    return [byte[]]$PemData
}

function DecodeIntSize {
    param (
        [System.IO.BinaryReader]$BinaryReader
    )
    
    [byte]$byteValue = $BinaryReader.ReadByte()
    
    # If anything other than 0x02, an ASN.1 integer follows.
    if ($byteValue -ne 0x02) {
        return 0;
    }
    
    $byteValue = $BinaryReader.ReadByte()
    # 0x81 == Data size in next byte.
    if ($byteValue -eq 0x81) { 
        $size = $BinaryReader.ReadByte()
    }
    # 0x82 == Data size in next 2 bytes.
    else {
        if ($byteValue -eq 0x82) {
            [byte]$high = $BinaryReader.ReadByte()
            [byte]$low = $BinaryReader.ReadByte()
            $byteValues = [byte[]]@($low, $high, 0x00, 0x00)
            $size = [System.BitConverter]::ToInt32($byteValues, 0)
        }
        else {
            # Otherwise, data size has already been read above.
            $size = $byteValue
        }
    }
    # Remove high-order zeros in data
    $byteValue = $BinaryReader.ReadByte()
    while ($byteValue -eq 0x00) {
        $byteValue = $BinaryReader.ReadByte()
        $size -= 1
    }

    $BinaryReader.BaseStream.Seek(-1, [System.IO.SeekOrigin]::Current) | Out-Null
    return $size
}

function PadByteArray {
    param (
        [byte[]]$Bytes,
        [int]$Size
    )

    if ($Bytes.Length -eq $Size) {
        return $Bytes
    }
    if ($Bytes.Length -gt $Size) {
        throw "Specified size '$Size' to pad is too small for byte array of size '$($Bytes.Length)'."
    }

    [byte[]]$PaddedBytes = New-Object Byte[] $Size
    [System.Array]::Copy($Bytes, 0, $PaddedBytes, $Size - $bytes.Length, $bytes.Length) | Out-Null
    return $PaddedBytes
}

function Compare-ByteArrays {
    param (
        [byte[]]$First,
        [byte[]]$Second
    )
    if ($First.Length -ne $Second.Length) {
        return $False
    }
    [int]$i = 0
    foreach ($byte in $First) {
        if ($byte -ne $Second[$i]) {
            return $False
        }
        $i = $i + 1
    }
    return $True
}

function CreateRSAFromPkcs8 {
    param (
        [byte[]]$KeyBytes
    )
    Write-Verbose "Reading RSA Pkcs8 private key bytes"

    # The encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
    # this byte[] includes the sequence byte and terminal encoded null 
    [byte[]]$SeqOID = 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00
    [byte[]]$Seq = New-Object byte[] 15

    # Have to wrap $KeyBytes in another array :|
    $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) 
    $reader = New-Object System.IO.BinaryReader($MemoryStream)
    $StreamLength = [int]$MemoryStream.Length
    
    try {
        [UInt16]$Bytes = $reader.ReadUInt16()

        if ($Bytes -eq 0x8130) {
            $reader.ReadByte() | Out-Null
        }
        elseif ($Bytes -eq 0x8230) {
            $reader.ReadInt16() | Out-Null
        }
        else {
            return $null
        }
        
        [byte]$byteValue = $reader.ReadByte()

        if ($byteValue -ne 0x02) {
            return $null
        }

        $Bytes = $reader.ReadUInt16()

        if ($Bytes -ne 0x0001) {
            return $null
        }

        # Read the Sequence OID
        $Seq = $reader.ReadBytes(15)
        $SequenceMatches = Compare-ByteArrays -First $Seq -Second $SeqOID
        if ($SequenceMatches -eq $False) {
            Write-Verbose "Sequence OID doesnt match"
            return $null
        }

        $byteValue = $reader.ReadByte()
        # Next byte should be a Octet string
        if ($byteValue -ne 0x04) {
            return $null
        }
        # Read next byte / 2 bytes. 
        # Should be either: 0x81 or 0x82; otherwise it's the byte count.
        $byteValue = $reader.ReadByte()
        if ($byteValue -eq 0x81) {
            $reader.ReadByte() | Out-Null
        }
        else {
            if ($byteValue -eq 0x82) {
                $reader.ReadUInt16() | Out-Null
            }
        }

        # Remaining sequence *should* be the RSA Pkcs1 private Key bytes
        [byte[]]$RsaKeyBytes = $reader.ReadBytes([int]($StreamLength - $MemoryStream.Position))
        Write-Verbose "Attempting to create RSA object from remaining Pkcs1 bytes"
        $rsa = CreateRSAFromPkcs1 -KeyBytes $RsaKeyBytes
        return $rsa
    }
    catch {
        Write-Warning "CreateRSAFromPkcs8: Exception occurred - $($_.Exception.Message)"
        return $null
    }
    finally {
        if ($null -ne $reader) { $reader.Close() }
        if ($null -ne $MemoryStream) { $MemoryStream.Close() }
    }
}

function CreateRSAFromPkcs1 {
    param (
        [byte[]]$KeyBytes
    )
    Write-Verbose "Reading RSA Pkcs1 private key bytes"
    # Have to wrap $KeyBytes in another array :|
    $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) 
    $reader = New-Object System.IO.BinaryReader($MemoryStream)
    try {
               
        [UInt16]$Bytes = $reader.ReadUInt16()

        if ($Bytes -eq 0x8130) {
            $reader.ReadByte() | Out-Null
        }
        elseif ($Bytes -eq 0x8230) {
            $reader.ReadInt16() | Out-Null
        }
        else {
            return $null
        }
    
        $Bytes = $reader.ReadUInt16()
        if ($Bytes -ne 0x0102) {
            return $null
        }
    
        [byte]$byteValue = $reader.ReadByte()
        if ($byteValue -ne 0x00) {
            return $null
        }

        # Private key parameters are integer sequences.
        # For a summary of the RSA Parameters fields, 
        # See https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsaparameters#summary-of-fields
        
        $Modulus_Size = DecodeIntSize -BinaryReader $reader
        $Modulus = $reader.ReadBytes($Modulus_Size)

        $E_Size = DecodeIntSize -BinaryReader $reader
        $E = $reader.ReadBytes($E_Size)

        $D_Size = DecodeIntSize -BinaryReader $reader
        $D = $reader.ReadBytes($D_Size)

        $P_Size = DecodeIntSize -BinaryReader $reader
        $P = $reader.ReadBytes($P_Size)

        $Q_Size = DecodeIntSize -BinaryReader $reader
        $Q = $reader.ReadBytes($Q_Size)

        $DP_Size = DecodeIntSize -BinaryReader $reader
        $DP = $reader.ReadBytes($DP_Size)

        $DQ_Size = DecodeIntSize -BinaryReader $reader
        $DQ = $reader.ReadBytes($DQ_Size)

        $IQ_Size = DecodeIntSize -BinaryReader $reader
        $IQ = $reader.ReadBytes($IQ_Size)

        $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
        $rsaParameters = New-Object System.Security.Cryptography.RSAParameters
        $rsaParameters.Modulus = $Modulus
        $rsaParameters.Exponent = $E
        $rsaParameters.P = $P
        $rsaParameters.Q = $Q
        # Some RSAParameter values dont play well with byte buffers having leading zeroes removed.
        $rsaParameters.D = PadByteArray -Bytes $D -Size $Modulus.Length
        $rsaParameters.DP = PadByteArray -Bytes $DP -Size $P.Length
        $rsaParameters.DQ = PadByteArray -Bytes $DQ -Size $Q.Length
        $rsaParameters.InverseQ = PadByteArray -Bytes $IQ -Size $Q.Length
        $rsa.ImportParameters($rsaParameters)

        Write-Verbose "Completed RSA object creation"
        return $rsa
    }
    catch {
        Write-Warning "CreateRSA-FromPkcs1: Exception occurred - $($_.Exception.Message)"
        return $null
    }
    finally {
        if ($null -ne $reader) { $reader.Close() }
        if ($null -ne $MemoryStream) { $MemoryStream.Close() }
    }
}

function CreateSigningKey {
    param (
        [string]$Key
    )
    try {
        $Key = $Key.Trim()
        switch -Wildcard($Key) {
            "$Pkcs8_PrivateKey_Header*" {
                $KeyBytes = ExtractPemData -PEM $Key -Header $Pkcs8_PrivateKey_Header -Footer $Pkcs8_PrivateKey_Footer
                $SigningKey = CreateRSAFromPkcs8 -KeyBytes $KeyBytes
                return $SigningKey
            }
            "$RsaPrivateKey_Header*" {
                $KeyBytes = ExtractPemData -PEM $Key -Header $RsaPrivateKey_Header -Footer $RsaPrivateKey_Footer
                $SigningKey = CreateRSAFromPkcs1 -KeyBytes $KeyBytes
                return $SigningKey
            }
            default {
                Write-Verbose "The PEM header could not be found. Accepted headers: 'BEGIN PRIVATE KEY', 'BEGIN RSA PRIVATE KEY'"
                return $null
            }
        }
    }
    catch {
        Write-Warning "Couldn't create signing key: $($_.Exception.Message)"
        return $null
    }
}

###############################################################################

# Local variables
$StepName = $OctopusParameters["Octopus.Step.Name"]
$OutputVariableName = "JWT"

Write-Verbose "Vault.Generate.JWT.Signing.Algorithm: $VAULT_GENERATE_JWT_ALGORITHM"
Write-Verbose "Vault.Generate.JWT.ExpiresAfterMinutes: $VAULT_GENERATE_JWT_EXPIRES_MINS"
Write-Verbose "Vault.Generate.JWT.Issuer: $VAULT_GENERATE_JWT_ISSUER"
Write-Verbose "Vault.Generate.JWT.Audience(s): $($audiences -Join ",")"
Write-Verbose "Vault.Generate.JWT.Subject: $subject"
Write-Verbose "Vault.Generate.JWT.Groups: $groups"
Write-Verbose "Vault.Generate.JWT.IdentityPrefix: $VAULT_GENERATE_JWT_IDENTITY_PREFIX"
Write-Verbose "Step Name: $StepName"

try {

    # Created + Expires
    $Created = (Get-Date).ToUniversalTime()
    $Expires = $Created.AddMinutes([int]$VAULT_GENERATE_JWT_EXPIRES_MINS)

    $createDate = [Math]::Floor([decimal](Get-Date($Created) -UFormat "%s"))
    $expiryDate = [Math]::Floor([decimal](Get-Date($Expires) -UFormat "%s"))

    $JwtHeader = @{
        alg = $VAULT_GENERATE_JWT_ALGORITHM;
        typ = "JWT";
    } | ConvertTo-Json -Compress
    
    $JwtPayload = [Ordered]@{
        iat = [long]$createDate;
        exp = [long]$expiryDate;
        iss = $VAULT_GENERATE_JWT_ISSUER;
        aud = $audiences;
        sub = $subject;
        groups = $groups;
    }

    $JwtPayload = $JwtPayload | ConvertTo-Json -Compress

    $base64Header = ConvertTo-JwtBase64 -Value $JwtHeader
    $base64Payload = ConvertTo-JwtBase64 -Value $JwtPayload

    $Jwt = $base64Header + '.' + $base64Payload

    $JwtBytes = [System.Text.Encoding]::UTF8.GetBytes($Jwt)
    $JwtSignature = $null
    
    switch ($VAULT_GENERATE_JWT_ALGORITHM) {
        "RS256" {
            try { 

                $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY
                if ($null -eq $rsa) {
                    throw "Couldn't create RSA object"
                }
                $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) 
                $JwtSignature = ConvertTo-JwtBase64 -Value $Signature
            }
            catch { throw "Signing with SHA256 and Pkcs1 padding failed using private key: $($_.Exception.Message)" }
            finally { if ($null -ne $rsa) { $rsa.Dispose() } }
            
        }
        "RS384" {
            try { 
                $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY
                if ($null -eq $rsa) {
                    throw "Couldn't create RSA object"
                }
                $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA384, [Security.Cryptography.RSASignaturePadding]::Pkcs1) 
                $JwtSignature = ConvertTo-JwtBase64 -Value $Signature
            }
            catch { throw "Signing with SHA384 and Pkcs1 padding failed using private key: $($_.Exception.Message)" }
            finally { if ($null -ne $rsa) { $rsa.Dispose() } }
        }
        "RS512" {
            try { 
                $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY
                if ($null -eq $rsa) {
                    throw "Couldn't create RSA object"
                }
                $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA512, [Security.Cryptography.RSASignaturePadding]::Pkcs1) 
                $JwtSignature = ConvertTo-JwtBase64 -Value $Signature
            }
            catch { throw "Signing with SHA512 and Pkcs1 padding failed using private key: $($_.Exception.Message)" }
            finally { if ($null -ne $rsa) { $rsa.Dispose() } }
        }
        default {
            throw "The algorithm is not one of the supported: 'RS256', 'RS384', 'RS512'"
        }
    }
    if ([string]::IsNullOrWhiteSpace($JwtSignature) -eq $True) {
        throw "JWT signature empty."
    }

    $Jwt = "$Jwt.$JwtSignature"
    Set-OctopusVariable -Name $OutputVariableName -Value $Jwt -Sensitive
    Write-Host "Created output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}"
}
catch {
    $ExceptionMessage = $_.Exception.Message
    $Message = "An error occurred generating a JWT: $ExceptionMessage"
    Write-Error $Message -Category InvalidResult
}

Provided under the Apache License version 2.0.

Report an issue

To use this template in Octopus Deploy, copy the JSON below and paste it into the Library → Step templates → Import dialog.

{
  "Id": "e72fd23a-3bfd-4758-a720-2462d5206f65",
  "Name": "HashiCorp Vault - Generate JWT",
  "Description": "This step template generates a [Json Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) for use with HashiCorp Vault.\n\nThe step is based on the existing [JWT - Generate JSON Web Token](https://library.octopus.com/step-templates/1ca0401c-dfca-420e-81ca-1f4b7cf02d2d/actiontemplate-jwt-generate-json-web-token) step template.\n\nHowever, it differs as it offers less flexibility in choosing the fields to use in the generated JWT and is opinionated towards support for Vault [entities and groups](https://learn.hashicorp.com/tutorials/vault/identity).\n\nThe resulting JWT will be stored as a [sensitive output variable](https://octopus.com/docs/projects/variables/output-variables#sensitive-output-variables) called **JWT**.\n\nA private key needs to be provided that will sign the combined JWT header and payload.\n\nCurrently, the following three signing algorithms are supported:\n\n1. `RS256` - RSASSA-PKCS1-v1_5 using SHA-256\n2. `RS384` - RSASSA-PKCS1-v1_5 using SHA-384\n3. `RS512` - RSASSA-PKCS1-v1_5 using SHA-512\n\nThe default is `RS256`.\n\n**Notes:**\n- Tested on Windows and Linux (PowerShell Core)\n- Tested with Octopus **2020.1**",
  "Version": 1,
  "ExportedAt": "2021-08-10T12:55:30.088Z",
  "ActionType": "Octopus.Script",
  "Author": "harrisonmeister",
  "Packages": [],
  "Parameters": [
    {
      "Id": "71bacc82-21d7-4d08-a944-15dacd0a3405",
      "Name": "Vault.Generate.JWT.PrivateKey",
      "Label": "JWT Private signing key",
      "HelpText": "Provide the private key in PEM format to be used to sign the JWT.\n\nAccepted headers: \n\n- `----BEGIN RSA PRIVATE KEY-----`\n- `-----BEGIN PRIVATE KEY----`\n\n**Note:** It's recommended to use a sensitive variable to provide this value. If you instead enter the private key directly, the step template will attempt to detect this by replacing spaces with new lines ``n`.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Sensitive"
      }
    },
    {
      "Id": "84759ecc-a3f6-4822-a678-833269a8bf0d",
      "Name": "Vault.Generate.JWT.Signing.Algorithm",
      "Label": "JWT signing algorithm",
      "HelpText": "The JWA [algorithm](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1) to use when signing the JWT. The default is `RS256` (RSASSA-PKCS1-v1_5 using SHA-256).\n\nThis value is also used for the `alg` [algorithm header](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1).",
      "DefaultValue": "RS256",
      "DisplaySettings": {
        "Octopus.ControlType": "Select",
        "Octopus.SelectOptions": "RS256|RS256: RSASSA-PKCS1-v1_5 using SHA-256\nRS384|RS384: RSASSA-PKCS1-v1_5 using SHA-384\nRS512|RS512: RSASSA-PKCS1-v1_5 using SHA-512"
      }
    },
    {
      "Id": "76d289c5-f6f9-4cfb-b876-13515745f03f",
      "Name": "Vault.Generate.JWT.ExpiresAfterMinutes",
      "Label": "JWT expiry time in minutes",
      "HelpText": "The number of minutes after the JWT is created that it expires. This value is used for the `exp` [expiration time claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4). The default is `20` minutes.",
      "DefaultValue": "20",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "20d57320-15e4-4777-916b-8563192a326d",
      "Name": "Vault.Generate.JWT.Issuer",
      "Label": "JWT Issuer claim",
      "HelpText": "The `iss` [isuer claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) identifies the principal that issued the JWT.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    },
    {
      "Id": "12a68971-8e0b-4b90-9a27-4c81413d0fb7",
      "Name": "Vault.Generate.JWT.Audience",
      "Label": "JWT Audience claim",
      "HelpText": "The `aud` [audience claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3) identifies the recipients that the JWT is intended for. This is typically the base address of the resource being accessed, such as `https://example-domain.com`.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "MultiLineText"
      }
    },
    {
      "Id": "e7026958-60cb-4652-9fe0-2ef2965de951",
      "Name": "Vault.Generate.JWT.Subject",
      "Label": "JWT Subject claim",
      "HelpText": "Choose from a defined list of values for the `sub` [subject claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2) of the JWT.\n\nAvailable options start from least granular to most granular: \n\n- Octopus Server Uri\n- Octopus Server Uri, and Project Group name\n- Octopus Server Uri, Project Group, and Project name\n- Octopus Server Uri, Project Group, Project and Environment name\n\n*Note:* If the **Server Uri** property has not been set for the Octopus instance, the `Octopus.Web.ServerUri` variable will resolve to an empty string.\n",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Select",
        "Octopus.SelectOptions": "serveruri|Octopus Server Uri\nprojectgroup|Octopus Server Uri, and Project Group\nproject|Octopus Server Uri, Project Group, and Project\nenvironment|Octopus Server Uri, Project Group, Project, and Environment"
      }
    },
    {
      "Id": "1bd2294c-64a3-4dad-bb9f-1a4cce5eaff5",
      "Name": "Vault.Generate.JWT.Groups",
      "Label": "JWT Groups claim",
      "HelpText": "Choose from a defined list of values for the `groups` [private claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.3) that identifies groups associated with this JWT. \n\nAvailable options start from least granular to most granular: \n\n- Octopus Server Uri\n- Octopus Server Uri, and Project Group name\n- Octopus Server Uri, Project Group, and Project name\n- Octopus Server Uri, Project Group, Project and Environment name\n\n*Note:* If the **Server Uri** property has not been set for the Octopus instance, the `Octopus.Web.ServerUri` variable will resolve to an empty string.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "Select",
        "Octopus.SelectOptions": "serveruri|Octopus Server Uri\nprojectgroup|Octopus Server Uri, and Project Group\nproject|Octopus Server Uri, Project Group, and Project\nenvironment|Octopus Server Uri, Project Group, Project, and Environment"
      }
    },
    {
      "Id": "88298719-27ac-48c6-8c13-c40a3aaceb6c",
      "Name": "Vault.Generate.JWT.IdentityPrefix",
      "Label": "JWT Identity Prefix (optional)",
      "HelpText": "*Optional* - A prefix to be added to the values used in **both** the `sub` and `groups` claims.\n\nIf no value is provided, the prefix is not included.",
      "DefaultValue": "",
      "DisplaySettings": {
        "Octopus.ControlType": "SingleLineText"
      }
    }
  ],
  "Properties": {
    "Octopus.Action.Script.ScriptSource": "Inline",
    "Octopus.Action.Script.Syntax": "PowerShell",
    "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = 'Stop'\n\n# Helper functions\n###############################################################################\n\nfunction ConvertTo-JwtBase64 {\n    param (\n        $Value\n    )\n    if ($Value -is [string]) {\n        $ConvertedValue = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) -replace '\\+', '-' -replace '/', '_' -replace '='\n    }\n    elseif ($Value -is [byte[]]) {\n        $ConvertedValue = [Convert]::ToBase64String($Value) -replace '\\+', '-' -replace '/', '_' -replace '='\n    }\n\n    return $ConvertedValue\n}\n\nfunction ConvertTo-Subject {\n    param ([string]$Subject, [string]$IdentityPrefix)\n    return ConvertTo-Identity -IdentityValue $Subject -IdentityPrefix $IdentityPrefix\n}\n\nfunction ConvertTo-Groups {\n    param ([string]$Groups, [string]$IdentityPrefix)\n    return ConvertTo-Identity -IdentityValue $Groups -IdentityPrefix $IdentityPrefix\n}\n\nfunction ConvertTo-Identity {\n    param (\n        [string]$IdentityValue, \n        [string]$IdentityPrefix\n    )\n    $ServerUri = $OctopusParameters[\"Octopus.Web.ServerUri\"].ToLower() -Replace \"http://\" -Replace \"https://\"\n    $ProjectGroup = $OctopusParameters[\"Octopus.ProjectGroup.Name\"].ToLower() -Replace \" \", \"-\"\n    $Project = $OctopusParameters[\"Octopus.Project.Name\"].ToLower() -Replace \" \", \"-\"\n    $Environment = $OctopusParameters[\"Octopus.Environment.Name\"].ToLower() -Replace \" \", \"-\"\n    $identity = \"\"\n    switch ($VAULT_GENERATE_JWT_SUBJECT) {\n        \"serveruri\" {\n            $identity = $ServerUri\n        }\n        \"projectgroup\" {\n            $identity = \"$ServerUri/$ProjectGroup\"\n        }\n        \"project\" {\n            $identity = \"$ServerUri/$ProjectGroup/$Project\"\n        }\n        \"environment\" {\n            $identity = \"$ServerUri/$ProjectGroup/$Project/$Environment\"\n        }\n    }\n    if (![string]::IsNullOrWhiteSpace($IdentityPrefix)) {\n        $identity = \"$IdentityPrefix$identity\"\n    }\n    return $identity\n}\n\n###############################################################################\n\n# Variables\n$VAULT_GENERATE_JWT_PRIVATE_KEY = $OctopusParameters[\"Vault.Generate.JWT.PrivateKey\"]\n$VAULT_GENERATE_JWT_ALGORITHM = $OctopusParameters[\"Vault.Generate.JWT.Signing.Algorithm\"]\n$VAULT_GENERATE_JWT_EXPIRES_MINS = $OctopusParameters[\"Vault.Generate.JWT.ExpiresAfterMinutes\"]\n$VAULT_GENERATE_JWT_ISSUER = $OctopusParameters[\"Vault.Generate.JWT.Issuer\"]\n$VAULT_GENERATE_JWT_SUBJECT = $OctopusParameters[\"Vault.Generate.JWT.Subject\"]\n$VAULT_GENERATE_JWT_GROUPS = $OctopusParameters[\"Vault.Generate.JWT.Groups\"]\n$VAULT_GENERATE_JWT_AUDIENCE = $OctopusParameters[\"Vault.Generate.JWT.Audience\"]\n\n# Optional \n$VAULT_GENERATE_JWT_IDENTITY_PREFIX = $OctopusParameters[\"Vault.Generate.JWT.IdentityPrefix\"]\n\n$subject = ConvertTo-Subject -Groups $VAULT_GENERATE_JWT_GROUPS -IdentityPrefix $VAULT_GENERATE_JWT_IDENTITY_PREFIX\n$groups = ConvertTo-Groups -Groups $VAULT_GENERATE_JWT_GROUPS -IdentityPrefix $VAULT_GENERATE_JWT_IDENTITY_PREFIX\n\n$audiences = @()\nif (![string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_AUDIENCE)) {\n    @(($VAULT_GENERATE_JWT_AUDIENCE -Split \"`n\").Trim()) | ForEach-Object {\n        if (![string]::IsNullOrWhiteSpace($_)) {\n            $audiences += $_\n        }\n    }\n}\n\n# Validation\nif ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_PRIVATE_KEY)) {\n    throw \"Required parameter Vault.Generate.JWT.PrivateKey not specified.\"\n}\nif ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_ALGORITHM)) {\n    throw \"Required parameter Vault.Generate.JWT.Signing.Algorithm not specified.\"\n}\nif ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_EXPIRES_MINS)) {\n    throw \"Required parameter Vault.Generate.JWT.ExpiresAfterMinutes not specified.\"\n}\nif ([string]::IsNullOrWhiteSpace($VAULT_GENERATE_JWT_ISSUER)) {\n    throw \"Required parameter Vault.Generate.JWT.Issuer not specified.\"\n}\nif ($audiences.Length -le 0) {\n    throw \"Required parameter Vault.Generate.JWT.Audience not specified.\"\n}\nif ([string]::IsNullOrWhiteSpace($subject)) {\n    throw \"Required parameter Vault.Generate.JWT.Subject not specified.\"\n}\nif ([string]::IsNullOrWhiteSpace($groups)) {\n    throw \"Required parameter Vault.Generate.JWT.Groups not specified.\"\n}\n\n\n# Signing functions\n###############################################################################\n\n$RsaPrivateKey_Header = \"-----BEGIN RSA PRIVATE KEY-----\"\n$RsaPrivateKey_Footer = \"-----END RSA PRIVATE KEY-----\"\n$Pkcs8_PrivateKey_Header = \"-----BEGIN PRIVATE KEY-----\"\n$Pkcs8_PrivateKey_Footer = \"-----END PRIVATE KEY-----\"\n\nfunction ExtractPemData {\n    param (\n        [string]$Pem,\n        [string]$Header,\n        [string]$Footer\n    )\n\n    $Start = $Pem.IndexOf($Header) + $Header.Length\n    $End = $Pem.IndexOf($Footer, $Start) - $Start\n    $EncodedPem = ($Pem.Substring($Start, $End).Trim()) -Replace \" \", \"`n\"\n    \n    $PemData = [Convert]::FromBase64String($EncodedPem)\n    return [byte[]]$PemData\n}\n\nfunction DecodeIntSize {\n    param (\n        [System.IO.BinaryReader]$BinaryReader\n    )\n    \n    [byte]$byteValue = $BinaryReader.ReadByte()\n    \n    # If anything other than 0x02, an ASN.1 integer follows.\n    if ($byteValue -ne 0x02) {\n        return 0;\n    }\n    \n    $byteValue = $BinaryReader.ReadByte()\n    # 0x81 == Data size in next byte.\n    if ($byteValue -eq 0x81) { \n        $size = $BinaryReader.ReadByte()\n    }\n    # 0x82 == Data size in next 2 bytes.\n    else {\n        if ($byteValue -eq 0x82) {\n            [byte]$high = $BinaryReader.ReadByte()\n            [byte]$low = $BinaryReader.ReadByte()\n            $byteValues = [byte[]]@($low, $high, 0x00, 0x00)\n            $size = [System.BitConverter]::ToInt32($byteValues, 0)\n        }\n        else {\n            # Otherwise, data size has already been read above.\n            $size = $byteValue\n        }\n    }\n    # Remove high-order zeros in data\n    $byteValue = $BinaryReader.ReadByte()\n    while ($byteValue -eq 0x00) {\n        $byteValue = $BinaryReader.ReadByte()\n        $size -= 1\n    }\n\n    $BinaryReader.BaseStream.Seek(-1, [System.IO.SeekOrigin]::Current) | Out-Null\n    return $size\n}\n\nfunction PadByteArray {\n    param (\n        [byte[]]$Bytes,\n        [int]$Size\n    )\n\n    if ($Bytes.Length -eq $Size) {\n        return $Bytes\n    }\n    if ($Bytes.Length -gt $Size) {\n        throw \"Specified size '$Size' to pad is too small for byte array of size '$($Bytes.Length)'.\"\n    }\n\n    [byte[]]$PaddedBytes = New-Object Byte[] $Size\n    [System.Array]::Copy($Bytes, 0, $PaddedBytes, $Size - $bytes.Length, $bytes.Length) | Out-Null\n    return $PaddedBytes\n}\n\nfunction Compare-ByteArrays {\n    param (\n        [byte[]]$First,\n        [byte[]]$Second\n    )\n    if ($First.Length -ne $Second.Length) {\n        return $False\n    }\n    [int]$i = 0\n    foreach ($byte in $First) {\n        if ($byte -ne $Second[$i]) {\n            return $False\n        }\n        $i = $i + 1\n    }\n    return $True\n}\n\nfunction CreateRSAFromPkcs8 {\n    param (\n        [byte[]]$KeyBytes\n    )\n    Write-Verbose \"Reading RSA Pkcs8 private key bytes\"\n\n    # The encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = \"1.2.840.113549.1.1.1\"\n    # this byte[] includes the sequence byte and terminal encoded null \n    [byte[]]$SeqOID = 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00\n    [byte[]]$Seq = New-Object byte[] 15\n\n    # Have to wrap $KeyBytes in another array :|\n    $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) \n    $reader = New-Object System.IO.BinaryReader($MemoryStream)\n    $StreamLength = [int]$MemoryStream.Length\n    \n    try {\n        [UInt16]$Bytes = $reader.ReadUInt16()\n\n        if ($Bytes -eq 0x8130) {\n            $reader.ReadByte() | Out-Null\n        }\n        elseif ($Bytes -eq 0x8230) {\n            $reader.ReadInt16() | Out-Null\n        }\n        else {\n            return $null\n        }\n        \n        [byte]$byteValue = $reader.ReadByte()\n\n        if ($byteValue -ne 0x02) {\n            return $null\n        }\n\n        $Bytes = $reader.ReadUInt16()\n\n        if ($Bytes -ne 0x0001) {\n            return $null\n        }\n\n        # Read the Sequence OID\n        $Seq = $reader.ReadBytes(15)\n        $SequenceMatches = Compare-ByteArrays -First $Seq -Second $SeqOID\n        if ($SequenceMatches -eq $False) {\n            Write-Verbose \"Sequence OID doesnt match\"\n            return $null\n        }\n\n        $byteValue = $reader.ReadByte()\n        # Next byte should be a Octet string\n        if ($byteValue -ne 0x04) {\n            return $null\n        }\n        # Read next byte / 2 bytes. \n        # Should be either: 0x81 or 0x82; otherwise it's the byte count.\n        $byteValue = $reader.ReadByte()\n        if ($byteValue -eq 0x81) {\n            $reader.ReadByte() | Out-Null\n        }\n        else {\n            if ($byteValue -eq 0x82) {\n                $reader.ReadUInt16() | Out-Null\n            }\n        }\n\n        # Remaining sequence *should* be the RSA Pkcs1 private Key bytes\n        [byte[]]$RsaKeyBytes = $reader.ReadBytes([int]($StreamLength - $MemoryStream.Position))\n        Write-Verbose \"Attempting to create RSA object from remaining Pkcs1 bytes\"\n        $rsa = CreateRSAFromPkcs1 -KeyBytes $RsaKeyBytes\n        return $rsa\n    }\n    catch {\n        Write-Warning \"CreateRSAFromPkcs8: Exception occurred - $($_.Exception.Message)\"\n        return $null\n    }\n    finally {\n        if ($null -ne $reader) { $reader.Close() }\n        if ($null -ne $MemoryStream) { $MemoryStream.Close() }\n    }\n}\n\nfunction CreateRSAFromPkcs1 {\n    param (\n        [byte[]]$KeyBytes\n    )\n    Write-Verbose \"Reading RSA Pkcs1 private key bytes\"\n    # Have to wrap $KeyBytes in another array :|\n    $MemoryStream = New-Object System.IO.MemoryStream(, $KeyBytes) \n    $reader = New-Object System.IO.BinaryReader($MemoryStream)\n    try {\n               \n        [UInt16]$Bytes = $reader.ReadUInt16()\n\n        if ($Bytes -eq 0x8130) {\n            $reader.ReadByte() | Out-Null\n        }\n        elseif ($Bytes -eq 0x8230) {\n            $reader.ReadInt16() | Out-Null\n        }\n        else {\n            return $null\n        }\n    \n        $Bytes = $reader.ReadUInt16()\n        if ($Bytes -ne 0x0102) {\n            return $null\n        }\n    \n        [byte]$byteValue = $reader.ReadByte()\n        if ($byteValue -ne 0x00) {\n            return $null\n        }\n\n        # Private key parameters are integer sequences.\n        # For a summary of the RSA Parameters fields, \n        # See https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsaparameters#summary-of-fields\n        \n        $Modulus_Size = DecodeIntSize -BinaryReader $reader\n        $Modulus = $reader.ReadBytes($Modulus_Size)\n\n        $E_Size = DecodeIntSize -BinaryReader $reader\n        $E = $reader.ReadBytes($E_Size)\n\n        $D_Size = DecodeIntSize -BinaryReader $reader\n        $D = $reader.ReadBytes($D_Size)\n\n        $P_Size = DecodeIntSize -BinaryReader $reader\n        $P = $reader.ReadBytes($P_Size)\n\n        $Q_Size = DecodeIntSize -BinaryReader $reader\n        $Q = $reader.ReadBytes($Q_Size)\n\n        $DP_Size = DecodeIntSize -BinaryReader $reader\n        $DP = $reader.ReadBytes($DP_Size)\n\n        $DQ_Size = DecodeIntSize -BinaryReader $reader\n        $DQ = $reader.ReadBytes($DQ_Size)\n\n        $IQ_Size = DecodeIntSize -BinaryReader $reader\n        $IQ = $reader.ReadBytes($IQ_Size)\n\n        $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider\n        $rsaParameters = New-Object System.Security.Cryptography.RSAParameters\n        $rsaParameters.Modulus = $Modulus\n        $rsaParameters.Exponent = $E\n        $rsaParameters.P = $P\n        $rsaParameters.Q = $Q\n        # Some RSAParameter values dont play well with byte buffers having leading zeroes removed.\n        $rsaParameters.D = PadByteArray -Bytes $D -Size $Modulus.Length\n        $rsaParameters.DP = PadByteArray -Bytes $DP -Size $P.Length\n        $rsaParameters.DQ = PadByteArray -Bytes $DQ -Size $Q.Length\n        $rsaParameters.InverseQ = PadByteArray -Bytes $IQ -Size $Q.Length\n        $rsa.ImportParameters($rsaParameters)\n\n        Write-Verbose \"Completed RSA object creation\"\n        return $rsa\n    }\n    catch {\n        Write-Warning \"CreateRSA-FromPkcs1: Exception occurred - $($_.Exception.Message)\"\n        return $null\n    }\n    finally {\n        if ($null -ne $reader) { $reader.Close() }\n        if ($null -ne $MemoryStream) { $MemoryStream.Close() }\n    }\n}\n\nfunction CreateSigningKey {\n    param (\n        [string]$Key\n    )\n    try {\n        $Key = $Key.Trim()\n        switch -Wildcard($Key) {\n            \"$Pkcs8_PrivateKey_Header*\" {\n                $KeyBytes = ExtractPemData -PEM $Key -Header $Pkcs8_PrivateKey_Header -Footer $Pkcs8_PrivateKey_Footer\n                $SigningKey = CreateRSAFromPkcs8 -KeyBytes $KeyBytes\n                return $SigningKey\n            }\n            \"$RsaPrivateKey_Header*\" {\n                $KeyBytes = ExtractPemData -PEM $Key -Header $RsaPrivateKey_Header -Footer $RsaPrivateKey_Footer\n                $SigningKey = CreateRSAFromPkcs1 -KeyBytes $KeyBytes\n                return $SigningKey\n            }\n            default {\n                Write-Verbose \"The PEM header could not be found. Accepted headers: 'BEGIN PRIVATE KEY', 'BEGIN RSA PRIVATE KEY'\"\n                return $null\n            }\n        }\n    }\n    catch {\n        Write-Warning \"Couldn't create signing key: $($_.Exception.Message)\"\n        return $null\n    }\n}\n\n###############################################################################\n\n# Local variables\n$StepName = $OctopusParameters[\"Octopus.Step.Name\"]\n$OutputVariableName = \"JWT\"\n\nWrite-Verbose \"Vault.Generate.JWT.Signing.Algorithm: $VAULT_GENERATE_JWT_ALGORITHM\"\nWrite-Verbose \"Vault.Generate.JWT.ExpiresAfterMinutes: $VAULT_GENERATE_JWT_EXPIRES_MINS\"\nWrite-Verbose \"Vault.Generate.JWT.Issuer: $VAULT_GENERATE_JWT_ISSUER\"\nWrite-Verbose \"Vault.Generate.JWT.Audience(s): $($audiences -Join \",\")\"\nWrite-Verbose \"Vault.Generate.JWT.Subject: $subject\"\nWrite-Verbose \"Vault.Generate.JWT.Groups: $groups\"\nWrite-Verbose \"Vault.Generate.JWT.IdentityPrefix: $VAULT_GENERATE_JWT_IDENTITY_PREFIX\"\nWrite-Verbose \"Step Name: $StepName\"\n\ntry {\n\n    # Created + Expires\n    $Created = (Get-Date).ToUniversalTime()\n    $Expires = $Created.AddMinutes([int]$VAULT_GENERATE_JWT_EXPIRES_MINS)\n\n    $createDate = [Math]::Floor([decimal](Get-Date($Created) -UFormat \"%s\"))\n    $expiryDate = [Math]::Floor([decimal](Get-Date($Expires) -UFormat \"%s\"))\n\n    $JwtHeader = @{\n        alg = $VAULT_GENERATE_JWT_ALGORITHM;\n        typ = \"JWT\";\n    } | ConvertTo-Json -Compress\n    \n    $JwtPayload = [Ordered]@{\n        iat = [long]$createDate;\n        exp = [long]$expiryDate;\n        iss = $VAULT_GENERATE_JWT_ISSUER;\n        aud = $audiences;\n        sub = $subject;\n        groups = $groups;\n    }\n\n    $JwtPayload = $JwtPayload | ConvertTo-Json -Compress\n\n    $base64Header = ConvertTo-JwtBase64 -Value $JwtHeader\n    $base64Payload = ConvertTo-JwtBase64 -Value $JwtPayload\n\n    $Jwt = $base64Header + '.' + $base64Payload\n\n    $JwtBytes = [System.Text.Encoding]::UTF8.GetBytes($Jwt)\n    $JwtSignature = $null\n    \n    switch ($VAULT_GENERATE_JWT_ALGORITHM) {\n        \"RS256\" {\n            try { \n\n                $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY\n                if ($null -eq $rsa) {\n                    throw \"Couldn't create RSA object\"\n                }\n                $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1) \n                $JwtSignature = ConvertTo-JwtBase64 -Value $Signature\n            }\n            catch { throw \"Signing with SHA256 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" }\n            finally { if ($null -ne $rsa) { $rsa.Dispose() } }\n            \n        }\n        \"RS384\" {\n            try { \n                $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY\n                if ($null -eq $rsa) {\n                    throw \"Couldn't create RSA object\"\n                }\n                $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA384, [Security.Cryptography.RSASignaturePadding]::Pkcs1) \n                $JwtSignature = ConvertTo-JwtBase64 -Value $Signature\n            }\n            catch { throw \"Signing with SHA384 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" }\n            finally { if ($null -ne $rsa) { $rsa.Dispose() } }\n        }\n        \"RS512\" {\n            try { \n                $rsa = CreateSigningKey -Key $VAULT_GENERATE_JWT_PRIVATE_KEY\n                if ($null -eq $rsa) {\n                    throw \"Couldn't create RSA object\"\n                }\n                $Signature = $rsa.SignData($JwtBytes, [Security.Cryptography.HashAlgorithmName]::SHA512, [Security.Cryptography.RSASignaturePadding]::Pkcs1) \n                $JwtSignature = ConvertTo-JwtBase64 -Value $Signature\n            }\n            catch { throw \"Signing with SHA512 and Pkcs1 padding failed using private key: $($_.Exception.Message)\" }\n            finally { if ($null -ne $rsa) { $rsa.Dispose() } }\n        }\n        default {\n            throw \"The algorithm is not one of the supported: 'RS256', 'RS384', 'RS512'\"\n        }\n    }\n    if ([string]::IsNullOrWhiteSpace($JwtSignature) -eq $True) {\n        throw \"JWT signature empty.\"\n    }\n\n    $Jwt = \"$Jwt.$JwtSignature\"\n    Set-OctopusVariable -Name $OutputVariableName -Value $Jwt -Sensitive\n    Write-Host \"Created output variable: ##{Octopus.Action[$StepName].Output.$OutputVariableName}\"\n}\ncatch {\n    $ExceptionMessage = $_.Exception.Message\n    $Message = \"An error occurred generating a JWT: $ExceptionMessage\"\n    Write-Error $Message -Category InvalidResult\n}"
  },
  "Category": "HashiCorp Vault",
  "HistoryUrl": "https://github.com/OctopusDeploy/Library/commits/master/step-templates//opt/buildagent/work/75443764cd38076d/step-templates/hashicorp-vault-generate-jwt.json",
  "Website": "/step-templates/e72fd23a-3bfd-4758-a720-2462d5206f65",
  "Logo": "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",
  "$Meta": {
    "Type": "ActionTemplate"
  }
}

History

Page updated on Tuesday, August 10, 2021