From 83f9f2a3a3995c805bfaf99c12b5e826090a1905 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 3 Aug 2026 03:19:35 +0200 Subject: [PATCH 1/3] ci: bump Process-PSModule reusable workflow to v6.1.15 Updates the reusable workflow pin from v6.1.13 to v6.1.15 and keeps the explicit TestData mapping required by the reusable workflow's secrets interface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Process-PSModule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index d24207c..f1f91ba 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,7 +27,7 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@fb1bdb8fefd243292f779d2a856a38db6fe6daf4 # v6.1.13 + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@688896dc3ef70fb35bd74ae5328e76d5e57fe08a # v6.1.15 secrets: APIKey: ${{ secrets.APIKey }} TestData: >- From 982877482c4bf351ffcd3525323716f6870a12aa Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 3 Aug 2026 03:32:05 +0200 Subject: [PATCH 2/3] fix: preserve byte[] return type in New-JwtSigningKey for HS* algorithms PowerShell functions unwrap [byte[]] to [object[]] when returned through an untyped variable, causing Resolve-JwtKey to reject generated HMAC keys with 'Algorithm HS256 does not accept a key of type [System.Object[]]'. Return the byte array with the unary comma operator so the type is preserved and New-Jwt -Algorithm HS256 -GenerateKey works correctly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/functions/public/Keys/New-JwtSigningKey.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/public/Keys/New-JwtSigningKey.ps1 b/src/functions/public/Keys/New-JwtSigningKey.ps1 index ba90b9f..abd97b8 100644 --- a/src/functions/public/Keys/New-JwtSigningKey.ps1 +++ b/src/functions/public/Keys/New-JwtSigningKey.ps1 @@ -86,7 +86,7 @@ function New-JwtSigningKey { } $bytes = [byte[]]::new($keyLength) [System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes) - $key = $bytes + return , $bytes } '^(RS|PS)' { $rsa = [System.Security.Cryptography.RSA]::Create() From d77986864d2377b60d46a98e4c12b1d9a13e47ab Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 3 Aug 2026 03:32:11 +0200 Subject: [PATCH 3/3] test: add production-level edge-case coverage to v2 integration suite Expands Integration.Jwt.Tests.ps1 with a new 'Production-level edge cases' context covering: - Test-Jwt -Detailed reports failed signature and claim checks. - New-Jwt -GenerateKey produces valid tokens for HS256, RS256, and ES256. - ConvertFrom-Jwt accepts a SecureString token. - Test-Jwt returns false for an empty signature segment on signed algs. - New-Jwt parameter validation rejects non-hashtable payloads. - Test-Jwt parameter validation rejects null tokens. - Verbose output does not leak payload secrets or key material. The new HS256 -GenerateKey test exposed the byte[]-to-object[] regression fixed in the preceding commit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/Integration.Jwt.Tests.ps1 | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/Integration.Jwt.Tests.ps1 b/tests/Integration.Jwt.Tests.ps1 index 690d321..8fe6f00 100644 --- a/tests/Integration.Jwt.Tests.ps1 +++ b/tests/Integration.Jwt.Tests.ps1 @@ -671,5 +671,81 @@ Describe 'Jwt module' { } } } + + Context 'Production-level edge cases' { + BeforeAll { + $script:secret = 'a-string-secret-at-least-256-bits-long' + $script:goodJwt = New-Jwt -Payload @{ sub = 'joe' } -Algorithm HS256 -Key $script:secret + } + + It 'Test-Jwt -Detailed reports the failed check when the signature is invalid' { + $compact = $script:goodJwt.ToString() + $parts = $compact.Split('.') + $parts[2] = ConvertTo-Base64UrlString ([byte[]](1..32)) + $tampered = $parts -join '.' + $result = Test-Jwt -Token $tampered -Key $script:secret -RequireExpiration $false -Detailed + + $result.Valid | Should -BeFalse + $result.SignatureValidated | Should -BeFalse + ($result.Checks | Where-Object Name -EQ 'Signature').Passed | Should -BeFalse + } + + It 'Test-Jwt -Detailed reports the failed claim check' { + $nowSec = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $expired = New-Jwt -Payload @{ sub = 'joe'; exp = $nowSec - 60 } -Algorithm HS256 -Key $script:secret + $result = Test-Jwt -Token $expired -Key $script:secret -Detailed + + $result.Valid | Should -BeFalse + ($result.Checks | Where-Object Name -EQ 'Expiration').Passed | Should -BeFalse + } + + It 'New-Jwt -GenerateKey produces valid tokens for all algorithm families' -ForEach @( + @{ Alg = 'HS256' }, + @{ Alg = 'RS256' }, + @{ Alg = 'ES256' } + ) { + $jwt = New-Jwt -Payload @{ sub = 'joe' } -Algorithm $Alg -GenerateKey + + $jwt | Should -BeOfType [Jwt] + $jwt.Header.alg | Should -Be $Alg + $jwt.ToString().Split('.').Count | Should -Be 3 + $jwt.Signature | Should -Not -BeNullOrEmpty + } + + It 'ConvertFrom-Jwt accepts a SecureString token' { + $compact = $script:goodJwt.ToString() + $secure = ConvertTo-SecureString $compact -AsPlainText -Force + $parsed = ConvertFrom-Jwt -Token $secure + + $parsed.Payload.sub | Should -Be 'joe' + } + + It 'Test-Jwt returns false when the signature segment is empty for a signed algorithm' { + $compact = $script:goodJwt.ToString() + $parts = $compact.Split('.') + $emptySig = "$($parts[0]).$($parts[1])." + + Test-Jwt -Token $emptySig -Key $script:secret -RequireExpiration $false | Should -BeFalse + } + + It 'New-Jwt parameter validation rejects a non-hashtable payload' { + { New-Jwt -Payload 'not-a-hashtable' -Algorithm HS256 -Key $script:secret } | + Should -Throw + } + + It 'Test-Jwt parameter validation rejects a null token' { + { Test-Jwt -Token $null -Key $script:secret -RequireExpiration $false } | Should -Throw + } + + It 'Verbose output does not include the payload or key material' { + $payload = @{ sub = 'joe'; secret = 'do-not-leak' } + $verbose = & { New-Jwt -Payload $payload -Algorithm HS256 -Key $script:secret -Verbose } 4>&1 | + Where-Object { $_.GetType().Name -eq 'VerboseRecord' } | + Out-String + + $verbose | Should -Not -Match 'do-not-leak' + $verbose | Should -Not -Match ([regex]::Escape($script:secret)) + } + } }