Repository navigation
Implement JWT creation and signing functionality #3
Description
Automation scripts and PowerShell modules that need to authenticate via JWT (e.g., as a GitHub App) currently depend on the full GitHub module or must implement JWT handling from scratch. The JWT creation and signing logic in the GitHub module is general-purpose and not specific to the GitHub API.
Request
Desired capability
A standalone Jwt module that provides functions and classes for creating, signing, and representing JSON Web Tokens per RFC 7519. The module should support:
- Creating unsigned JWTs from arbitrary headers and claims
- Signing JWTs with a local RSA private key (RS256)
- Signing JWTs via Azure Key Vault REST API
- Representing JWT components (header, payload, signature) as typed PowerShell classes
- Displaying the encoded JWT string when the token object is printed
The GitHub module currently contains this logic in the following files:
Current location in GitHub module |
Purpose |
|---|---|
GitHubJWTComponent |
Base64URL encoding utilities |
New-GitHubUnsignedJWT |
Creates unsigned JWT (header.payload) |
Add-GitHubLocalJWTSignature |
Signs JWT using local RSA private key |
Add-GitHubKeyVaultJWTSignature |
Signs JWT via Azure Key Vault |
Update-GitHubAppJWT |
Orchestrates JWT creation and refresh (GitHub-specific, stays in GitHub module) |
Test-GitHubJWTRefreshRequired |
Checks if JWT needs refresh (GitHub-specific, stays in GitHub module) |
Acceptance criteria
- The module exports a public function for creating signed JWTs with configurable headers and claims
- RSA signing with a local PEM private key is supported (RS256 algorithm)
- Azure Key Vault signing is supported via the Key Vault Sign REST API
- JWT components are represented as PowerShell classes with proper typing
- A JWT object's
ToString()method returns the encodedheader.payload.signaturestring - The skeleton/template code from the PSModule framework scaffolding (Book, Planets, etc.) is removed
- The module can be consumed independently without the
GitHubmodule
Technical decisions
Skeleton cleanup: The module repository currently contains template/placeholder code from the PSModule framework scaffolding (Book class, Planets variables, PSModuleTest functions, etc.). All template content is removed before implementing JWT functionality. The directory structure (src/classes/, src/functions/public/, src/functions/private/) is retained.
Function naming: Functions follow the Verb-Jwt* naming convention, consistent with the PSModule pattern of prefixing nouns with the module name:
| Function | Visibility | Purpose |
|---|---|---|
New-Jwt |
Public | Creates a signed JWT from header, claims, and a signing key |
New-JwtUnsigned |
Private | Builds the unsigned token (header.payload) from header and claims hashtables |
Add-JwtLocalSignature |
Private | Signs using a local RSA private key (PEM format) |
Add-JwtKeyVaultSignature |
Private | Signs using Azure Key Vault REST API |
Class design:
| Class | Purpose |
|---|---|
JwtBase64Url |
Static utility methods for Base64URL encoding/decoding per RFC 4648 §5 |
JwtHeader |
Represents the JWT header (alg, typ, custom fields) |
JwtPayload |
Represents the JWT payload/claims (iat, exp, iss, custom claims) |
Jwt |
Represents a complete JWT token; ToString() returns the encoded header.payload.signature string |
Signing approach: The module supports two signing methods, selected via parameter sets on New-Jwt:
- Local RSA — uses
[System.Security.Cryptography.RSA]::Create()andImportFromPem()for RS256 signing with PKCS1 padding (same approach as the existingAdd-GitHubLocalJWTSignature) - Azure Key Vault — calls the Key Vault Sign REST API (API version 7.4) using a bearer token obtained from Azure CLI or Az PowerShell (same approach as
Add-GitHubKeyVaultJWTSignature)
Return type: New-Jwt returns a [Jwt] object rather than a [securestring]. The Jwt class stores the token components and exposes the full encoded token via ToString(). Consumers that need a securestring can convert explicitly. This departs from the GitHub module's approach (which returns securestring) — the rationale is that a typed object is more useful for inspection, debugging, and pipeline composition.
Azure dependency handling: The Key Vault signing path requires Azure authentication (Azure CLI or Az PowerShell). These are runtime-only dependencies — the module does not declare a module dependency on Az.Accounts. The function checks for availability at call time and provides a clear error if neither is available, matching the existing pattern in the GitHub module.
What stays in the GitHub module: The JWT refresh orchestration (Update-GitHubAppJWT) and refresh check (Test-GitHubJWTRefreshRequired) are GitHub-context-specific and remain in the GitHub module. A follow-up issue in the GitHub repository will update those functions to consume the Jwt module. The GitHub-specific claims (hardcoded iss = ClientID, fixed ±10 minute window) will move to the GitHub module's call site as parameters passed to New-Jwt.
Algorithm support: Initial implementation supports RS256 only, matching the current GitHub module implementation. Additional algorithms (HS256, RS384, RS512, ES256, etc.) can be added in future issues — see issue #2 for inspiration from existing PowerShell JWT modules. The JwtHeader class stores the alg field for forward compatibility.
Test approach: Unit tests with Pester. Test scenarios: creating unsigned JWTs, signing with a test RSA key pair, verifying Base64URL encoding/decoding, and validating the Jwt class ToString() output format.
Implementation plan
Skeleton cleanup
- Remove template classes (
Book.ps1,SecretWriter.ps1) - Remove template functions (
Get-PSModuleTest.ps1,New-PSModuleTest.ps1,Set-PSModuleTest.ps1,Test-PSModuleTest.ps1,Get-InternalPSModule.ps1,Set-InternalPSModule.ps1) - Remove template variables (
Moons.ps1,Planets.ps1,SolarSystems.ps1,PrivateVariables.ps1) - Remove template data files, formats, types, modules, scripts, init, and assemblies
- Remove template test file (
PSModuleTest.Tests.ps1) - Clean up
header.ps1andfinally.ps1
Classes
- Create
JwtBase64Urlclass insrc/classes/public/JwtBase64Url.ps1with staticEncodeandConvertToBase64UrlFormatmethods - Create
JwtHeaderclass insrc/classes/public/JwtHeader.ps1 - Create
JwtPayloadclass insrc/classes/public/JwtPayload.ps1 - Create
Jwtclass insrc/classes/public/Jwt.ps1withHeader,Payload,Signatureproperties andToString()returning the encoded token
Functions
- Create
New-Jwtpublic function insrc/functions/public/New-Jwt.ps1with parameter sets for local RSA and Key Vault signing - Create
New-JwtUnsignedprivate function insrc/functions/private/New-JwtUnsigned.ps1 - Create
Add-JwtLocalSignatureprivate function insrc/functions/private/Add-JwtLocalSignature.ps1 - Create
Add-JwtKeyVaultSignatureprivate function insrc/functions/private/Add-JwtKeyVaultSignature.ps1
Module configuration
- Update
src/manifest.psd1with module metadata (description, tags, project URI) - Update
src/README.mdwith module description - Update root
README.mdwith usage documentation and examples
Tests
- Create
tests/Jwt.Tests.ps1with unit tests for JWT creation, signing, and class behavior
Follow-up (separate issues)
- Update the
GitHubmodule to depend onJwtand replace inline JWT logic (tracked in theGitHubrepository) - Add JWT decoding/parsing functions (
ConvertFrom-Jwt) — see issue #2 for inspiration - Add JWT validation functions (
Test-Jwt) - Add support for additional signing algorithms (HS256, ES256, etc.)
Activity
[-]🩹 [Patch]: Move functionality from the `GitHub` module into this module[/-][+]Implement JWT creation and signing functionality[/+]MariusStorhaug commentedon May 7, 2026
Restructured the issue description into the standard three-section format.
GitHubmoduleGitHubmodule to its purpose, clarifying what moves and what staysVerb-Jwt*convention), class design (Jwt,JwtHeader,JwtPayload,JwtBase64Url), return type ([Jwt]object instead of[securestring]), signing approach (local RSA + Azure Key Vault), Azure dependency handling, and algorithm scope (RS256 only for initial release)Update-GitHubAppJWTandTest-GitHubJWTRefreshRequiredstay in theGitHubmodule — only the generic JWT creation/signing logic movesMinorandFeaturelabels[Init]: Boilerplate scaffold removed (#12)
Add comprehensive JWT module implementation issue
MariusStorhaug commentedon May 8, 2026
Closing — scope absorbed into #13, which now covers
New-Jwt(with an-Unsignedswitch for external signing) alongside the parsing, validation, inspection, and JWK functions. PR #14 is also closed.