diff --git a/.github/PSModule.yml b/.github/PSModule.yml
index c66c28c..508dc3c 100644
--- a/.github/PSModule.yml
+++ b/.github/PSModule.yml
@@ -5,3 +5,27 @@
Test:
CodeCoverage:
PercentTarget: 0
+# TestResults:
+# Skip: true
+# SourceCode:
+# Skip: true
+# PSModule:
+# Skip: true
+# Module:
+# Windows:
+# Skip: true
+# MacOS:
+# Skip: true
+Build:
+ Docs:
+ Skip: true
+
+Linter:
+ env:
+ VALIDATE_BIOME_FORMAT: false
+ VALIDATE_BIOME_LINT: false
+ VALIDATE_GITHUB_ACTIONS_ZIZMOR: false
+ VALIDATE_JSCPD: false
+ VALIDATE_JSON_PRETTIER: false
+ VALIDATE_MARKDOWN_PRETTIER: false
+ VALIDATE_YAML_PRETTIER: false
diff --git a/.github/linters/.codespellrc b/.github/linters/.codespellrc
new file mode 100644
index 0000000..351e9a0
--- /dev/null
+++ b/.github/linters/.codespellrc
@@ -0,0 +1,3 @@
+[codespell]
+skip = ./.github/linters
+ignore-words-list = afterall
diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json
deleted file mode 100644
index 23970e8..0000000
--- a/.github/linters/.jscpd.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "threshold": 0,
- "reporters": [
- "consoleFull"
- ],
- "ignore": [
- "**/tests/**"
- ],
- "absolute": true
-}
diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml
index b9ecdfa..57db57e 100644
--- a/.github/linters/.markdown-lint.yml
+++ b/.github/linters/.markdown-lint.yml
@@ -8,18 +8,19 @@
###############
# Rules by id #
###############
-MD004: false # Unordered list style
+MD004: false # Unordered list style
MD007:
- indent: 2 # Unordered list indentation
+ indent: 2 # Unordered list indentation
MD013:
- line_length: 808 # Line length
+ line_length: 808 # Line length
+MD024: false # no-duplicate-heading, INPUTS and OUTPUTS _can_ be the same item
MD026:
- punctuation: ".,;:!。,;:" # List of not allowed
-MD029: false # Ordered list item prefix
-MD033: false # Allow inline HTML
-MD036: false # Emphasis used instead of a heading
+ punctuation: '.,;:!。,;:' # List of not allowed
+MD029: false # Ordered list item prefix
+MD033: false # Allow inline HTML
+MD036: false # Emphasis used instead of a heading
#################
# Rules by tags #
#################
-blank_lines: false # Error on blank lines
+blank_lines: false # Error on blank lines
diff --git a/.github/linters/trivy.yaml b/.github/linters/trivy.yaml
new file mode 100644
index 0000000..0646d5d
--- /dev/null
+++ b/.github/linters/trivy.yaml
@@ -0,0 +1,10 @@
+scan:
+ disable-telemetry: true
+ scanners:
+ - vuln
+ - misconfig
+ - secret
+ skip-files:
+ # src/JWT.psm1 is a build artifact generated by Process-PSModule and is
+ # not present in source. Skip it so lint runs against checked-in files only.
+ - src/JWT.psm1
diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml
deleted file mode 100644
index 1f677cb..0000000
--- a/.github/workflows/Linter.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Linter
-
-run-name: "Linter - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}"
-
-on: [pull_request]
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
- packages: read
- statuses: write
-
-jobs:
- Lint:
- name: Lint code base
- runs-on: ubuntu-latest
- steps:
- - name: Checkout repo
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Lint code base
- uses: super-linter/super-linter@latest
- env:
- GITHUB_TOKEN: ${{ github.token }}
- VALIDATE_JSON_PRETTIER: false
- VALIDATE_MARKDOWN_PRETTIER: false
- VALIDATE_YAML_PRETTIER: false
diff --git a/.github/workflows/Nightly-Run.yml b/.github/workflows/Nightly-Run.yml
deleted file mode 100644
index b192d11..0000000
--- a/.github/workflows/Nightly-Run.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: Nightly Run
-
-on:
- workflow_dispatch:
- schedule:
- - cron: '0 0 * * *'
-
-permissions:
- contents: read
- pull-requests: write
- statuses: write
-
-jobs:
- Process-PSModule:
- uses: PSModule/Process-PSModule/.github/workflows/CI.yml@v4
- secrets:
- APIKEY: ${{ secrets.APIKEY }}
diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml
index bd51f85..67f2c25 100644
--- a/.github/workflows/Process-PSModule.yml
+++ b/.github/workflows/Process-PSModule.yml
@@ -1,8 +1,9 @@
name: Process-PSModule
-run-name: "Process-PSModule - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}"
-
on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 0 * * *'
pull_request:
branches:
- main
@@ -26,6 +27,5 @@ permissions:
jobs:
Process-PSModule:
- uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v4
- secrets:
- APIKEY: ${{ secrets.APIKEY }}
+ uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@11117919e65242d3388727819a751f74ad24ea9e # v5.5.0
+ secrets: inherit
diff --git a/README.md b/README.md
index 6319793..e88a481 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,54 @@
-# {{ NAME }}
+# Jwt
-{{ DESCRIPTION }}
-
-## Prerequisites
-
-This uses the following external resources:
-- The [PSModule framework](https://github.com/PSModule/Process-PSModule) for building, testing and publishing the module.
+`Jwt` is a PowerShell module for creating and verifying JSON Web Tokens. This repository maintains the current `Jwt` module command surface under PSModule maintenance so existing users can continue to install and use the package from PowerShell Gallery.
## Installation
-To install the module from the PowerShell Gallery, you can use the following command:
-
```powershell
-Install-PSResource -Name {{ NAME }}
-Import-Module -Name {{ NAME }}
+Install-PSResource -Name Jwt
+Import-Module -Name Jwt
```
-## Usage
+## Commands
-Here is a list of example that are typical use cases for the module.
-
-### Example 1: Greet an entity
-
-Provide examples for typical commands that a user would like to do with the module.
+The maintained module exports the same JWT commands and alias used by the current package:
```powershell
-Greet-Entity -Name 'World'
-Hello, World!
+ConvertFrom-Base64UrlString
+ConvertTo-Base64UrlString
+Get-JwtHeader
+Get-JwtPayload
+New-Jwt
+Test-Jwt
+Verify-JwtSignature
```
-### Example 2
+## Usage
-Provide examples for typical commands that a user would like to do with the module.
+Create and validate an HMAC-signed JWT:
```powershell
-Import-Module -Name PSModuleTemplate
-```
+$header = '{"alg":"HS256","typ":"JWT"}'
+$payload = '{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}'
+$secret = 'a-string-secret-at-least-256-bits-long'
-### Find more examples
+$jwt = New-Jwt -Header $header -PayloadJson $payload -Secret $secret
+Test-Jwt -jwt $jwt -Secret $secret
+```
-To find more examples of how to use the module, please refer to the [examples](examples) folder.
+Read the header and payload from an existing token:
-Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module.
-To find examples of each of the commands you can use Get-Help -Examples 'CommandName'.
+```powershell
+Get-JwtHeader -jwt $jwt
+Get-JwtPayload -jwt $jwt
+```
-## Documentation
+For more information about each command, use PowerShell help:
-Link to further documentation if available, or describe where in the repository users can find more detailed documentation about
-the module's functions and features.
+```powershell
+Get-Command -Module Jwt
+Get-Help New-Jwt -Full
+```
## Contributing
diff --git a/examples/General.ps1 b/examples/General.ps1
deleted file mode 100644
index e193423..0000000
--- a/examples/General.ps1
+++ /dev/null
@@ -1,19 +0,0 @@
-<#
- .SYNOPSIS
- This is a general example of how to use the module.
-#>
-
-# Import the module
-Import-Module -Name 'PSModule'
-
-# Define the path to the font file
-$FontFilePath = 'C:\Fonts\CodeNewRoman\CodeNewRomanNerdFontPropo-Regular.tff'
-
-# Install the font
-Install-Font -Path $FontFilePath -Verbose
-
-# List installed fonts
-Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular'
-
-# Uninstall the font
-Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' | Uninstall-Font -Verbose
diff --git a/src/README.md b/src/README.md
deleted file mode 100644
index af76160..0000000
--- a/src/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Details
-
-For more info about the expected structure of a module repository, please refer to [Build-PSModule](https://github.com/PSModule/Build-PSModule)
diff --git a/src/assemblies/LsonLib.dll b/src/assemblies/LsonLib.dll
deleted file mode 100644
index 3661807..0000000
Binary files a/src/assemblies/LsonLib.dll and /dev/null differ
diff --git a/src/classes/private/SecretWriter.ps1 b/src/classes/private/SecretWriter.ps1
deleted file mode 100644
index 1b1732a..0000000
--- a/src/classes/private/SecretWriter.ps1
+++ /dev/null
@@ -1,15 +0,0 @@
-class SecretWriter {
- [string] $Alias
- [string] $Name
- [string] $Secret
-
- SecretWriter([string] $alias, [string] $name, [string] $secret) {
- $this.Alias = $alias
- $this.Name = $name
- $this.Secret = $secret
- }
-
- [string] GetAlias() {
- return $this.Alias
- }
-}
diff --git a/src/classes/public/Book.ps1 b/src/classes/public/Book.ps1
deleted file mode 100644
index 8917d9a..0000000
--- a/src/classes/public/Book.ps1
+++ /dev/null
@@ -1,147 +0,0 @@
-class Book {
- # Class properties
- [string] $Title
- [string] $Author
- [string] $Synopsis
- [string] $Publisher
- [datetime] $PublishDate
- [int] $PageCount
- [string[]] $Tags
- # Default constructor
- Book() { $this.Init(@{}) }
- # Convenience constructor from hashtable
- Book([hashtable]$Properties) { $this.Init($Properties) }
- # Common constructor for title and author
- Book([string]$Title, [string]$Author) {
- $this.Init(@{Title = $Title; Author = $Author })
- }
- # Shared initializer method
- [void] Init([hashtable]$Properties) {
- foreach ($Property in $Properties.Keys) {
- $this.$Property = $Properties.$Property
- }
- }
- # Method to calculate reading time as 2 minutes per page
- [timespan] GetReadingTime() {
- if ($this.PageCount -le 0) {
- throw 'Unable to determine reading time from page count.'
- }
- $Minutes = $this.PageCount * 2
- return [timespan]::new(0, $Minutes, 0)
- }
- # Method to calculate how long ago a book was published
- [timespan] GetPublishedAge() {
- if (
- $null -eq $this.PublishDate -or
- $this.PublishDate -eq [datetime]::MinValue
- ) { throw 'PublishDate not defined' }
-
- return (Get-Date) - $this.PublishDate
- }
- # Method to return a string representation of the book
- [string] ToString() {
- return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))"
- }
-}
-
-class BookList {
- # Static property to hold the list of books
- static [System.Collections.Generic.List[Book]] $Books
- # Static method to initialize the list of books. Called in the other
- # static methods to avoid needing to explicit initialize the value.
- static [void] Initialize() { [BookList]::Initialize($false) }
- static [bool] Initialize([bool]$force) {
- if ([BookList]::Books.Count -gt 0 -and -not $force) {
- return $false
- }
-
- [BookList]::Books = [System.Collections.Generic.List[Book]]::new()
-
- return $true
- }
- # Ensure a book is valid for the list.
- static [void] Validate([book]$Book) {
- $Prefix = @(
- 'Book validation failed: Book must be defined with the Title,'
- 'Author, and PublishDate properties, but'
- ) -join ' '
- if ($null -eq $Book) { throw "$Prefix was null" }
- if ([string]::IsNullOrEmpty($Book.Title)) {
- throw "$Prefix Title wasn't defined"
- }
- if ([string]::IsNullOrEmpty($Book.Author)) {
- throw "$Prefix Author wasn't defined"
- }
- if ([datetime]::MinValue -eq $Book.PublishDate) {
- throw "$Prefix PublishDate wasn't defined"
- }
- }
- # Static methods to manage the list of books.
- # Add a book if it's not already in the list.
- static [void] Add([Book]$Book) {
- [BookList]::Initialize()
- [BookList]::Validate($Book)
- if ([BookList]::Books.Contains($Book)) {
- throw "Book '$Book' already in list"
- }
-
- $FindPredicate = {
- param([Book]$b)
-
- $b.Title -eq $Book.Title -and
- $b.Author -eq $Book.Author -and
- $b.PublishDate -eq $Book.PublishDate
- }.GetNewClosure()
- if ([BookList]::Books.Find($FindPredicate)) {
- throw "Book '$Book' already in list"
- }
-
- [BookList]::Books.Add($Book)
- }
- # Clear the list of books.
- static [void] Clear() {
- [BookList]::Initialize()
- [BookList]::Books.Clear()
- }
- # Find a specific book using a filtering scriptblock.
- static [Book] Find([scriptblock]$Predicate) {
- [BookList]::Initialize()
- return [BookList]::Books.Find($Predicate)
- }
- # Find every book matching the filtering scriptblock.
- static [Book[]] FindAll([scriptblock]$Predicate) {
- [BookList]::Initialize()
- return [BookList]::Books.FindAll($Predicate)
- }
- # Remove a specific book.
- static [void] Remove([Book]$Book) {
- [BookList]::Initialize()
- [BookList]::Books.Remove($Book)
- }
- # Remove a book by property value.
- static [void] RemoveBy([string]$Property, [string]$Value) {
- [BookList]::Initialize()
- $Index = [BookList]::Books.FindIndex({
- param($b)
- $b.$Property -eq $Value
- }.GetNewClosure())
- if ($Index -ge 0) {
- [BookList]::Books.RemoveAt($Index)
- }
- }
-}
-
-enum Binding {
- Hardcover
- Paperback
- EBook
-}
-
-enum Genre {
- Mystery
- Thriller
- Romance
- ScienceFiction
- Fantasy
- Horror
-}
diff --git a/src/data/Config.psd1 b/src/data/Config.psd1
deleted file mode 100644
index fea4466..0000000
--- a/src/data/Config.psd1
+++ /dev/null
@@ -1,3 +0,0 @@
-@{
- RandomKey = 'RandomValue'
-}
diff --git a/src/data/Settings.psd1 b/src/data/Settings.psd1
deleted file mode 100644
index bcfa7b4..0000000
--- a/src/data/Settings.psd1
+++ /dev/null
@@ -1,3 +0,0 @@
-@{
- RandomSetting = 'RandomSettingValue'
-}
diff --git a/src/finally.ps1 b/src/finally.ps1
deleted file mode 100644
index d8fc207..0000000
--- a/src/finally.ps1
+++ /dev/null
@@ -1,3 +0,0 @@
-Write-Verbose '------------------------------'
-Write-Verbose '--- THIS IS A LAST LOADER ---'
-Write-Verbose '------------------------------'
diff --git a/src/formats/CultureInfo.Format.ps1xml b/src/formats/CultureInfo.Format.ps1xml
deleted file mode 100644
index a715e08..0000000
--- a/src/formats/CultureInfo.Format.ps1xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
- System.Globalization.CultureInfo
-
- System.Globalization.CultureInfo
-
-
-
-
- 16
-
-
- 16
-
-
-
-
-
-
-
- LCID
-
-
- Name
-
-
- DisplayName
-
-
-
-
-
-
-
-
diff --git a/src/formats/Mygciview.Format.ps1xml b/src/formats/Mygciview.Format.ps1xml
deleted file mode 100644
index 4c972c2..0000000
--- a/src/formats/Mygciview.Format.ps1xml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
- mygciview
-
- System.IO.DirectoryInfo
- System.IO.FileInfo
-
-
- PSParentPath
-
-
-
-
-
- 7
- Left
-
-
-
- 26
- Right
-
-
-
- 26
- Right
-
-
-
- 14
- Right
-
-
-
- Left
-
-
-
-
-
-
-
- ModeWithoutHardLink
-
-
- LastWriteTime
-
-
- CreationTime
-
-
- Length
-
-
- Name
-
-
-
-
-
-
-
-
diff --git a/src/functions/private/Get-InternalPSModule.ps1 b/src/functions/private/Get-InternalPSModule.ps1
deleted file mode 100644
index 89f053c..0000000
--- a/src/functions/private/Get-InternalPSModule.ps1
+++ /dev/null
@@ -1,18 +0,0 @@
-function Get-InternalPSModule {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .EXAMPLE
- Test-PSModule -Name 'World'
-
- "Hello, World!"
- #>
- [CmdletBinding()]
- param (
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
diff --git a/src/functions/private/Set-InternalPSModule.ps1 b/src/functions/private/Set-InternalPSModule.ps1
deleted file mode 100644
index cf870ba..0000000
--- a/src/functions/private/Set-InternalPSModule.ps1
+++ /dev/null
@@ -1,22 +0,0 @@
-function Set-InternalPSModule {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .EXAMPLE
- Test-PSModule -Name 'World'
-
- "Hello, World!"
- #>
- [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
- 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function',
- Justification = 'Reason for suppressing'
- )]
- [CmdletBinding()]
- param (
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
diff --git a/src/functions/public/ConvertFrom-Base64UrlString.ps1 b/src/functions/public/ConvertFrom-Base64UrlString.ps1
new file mode 100644
index 0000000..e6ae97e
--- /dev/null
+++ b/src/functions/public/ConvertFrom-Base64UrlString.ps1
@@ -0,0 +1,63 @@
+function ConvertFrom-Base64UrlString {
+ <#
+ .SYNOPSIS
+ Decodes a base64url string.
+
+ .DESCRIPTION
+ Decodes a base64url-encoded string to UTF-8 text by default. Use AsByteArray to return the decoded bytes.
+
+ .EXAMPLE
+ ```powershell
+ 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' | ConvertFrom-Base64UrlString
+ ```
+
+ Decodes the base64url value to `{"alg":"RS256","typ":"JWT"}`.
+
+ .INPUTS
+ System.String
+
+ .OUTPUTS
+ System.String
+ System.Byte[]
+
+ .NOTES
+ Converts JWT-safe base64url text by restoring standard base64 characters and padding before decoding.
+
+ .LINK
+ https://psmodule.io/Jwt/Functions/ConvertFrom-Base64UrlString/
+
+ .LINK
+ https://jwt.io/
+ #>
+ [OutputType([string], [byte[]])]
+ [CmdletBinding()]
+ param(
+ # The base64url-encoded string to decode.
+ [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
+ [ValidateNotNullOrEmpty()]
+ [string] $Base64UrlString,
+
+ # Return decoded bytes instead of UTF-8 text.
+ [Parameter()]
+ [switch] $AsByteArray
+ )
+
+ begin {}
+
+ process {
+ $base64String = $Base64UrlString.Replace('-', '+').Replace('_', '/')
+ switch ($base64String.Length % 4) {
+ 0 { }
+ 1 { throw [System.FormatException]::new('Invalid base64url string length.') }
+ 2 { $base64String = $base64String + '==' }
+ 3 { $base64String = $base64String + '=' }
+ }
+ if ($AsByteArray) {
+ [Convert]::FromBase64String($base64String)
+ } else {
+ [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($base64String))
+ }
+ }
+
+ end {}
+}
diff --git a/src/functions/public/ConvertTo-Base64UrlString.ps1 b/src/functions/public/ConvertTo-Base64UrlString.ps1
new file mode 100644
index 0000000..ee4c9e0
--- /dev/null
+++ b/src/functions/public/ConvertTo-Base64UrlString.ps1
@@ -0,0 +1,56 @@
+function ConvertTo-Base64UrlString {
+ <#
+ .SYNOPSIS
+ Encodes text or bytes as a base64url string.
+
+ .DESCRIPTION
+ Encodes a string or byte array using base64url encoding suitable for JWT headers, payloads, and signatures.
+
+ .EXAMPLE
+ ```powershell
+ '{"alg":"RS256","typ":"JWT"}' | ConvertTo-Base64UrlString
+ ```
+
+ Encodes the JWT header JSON as `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9`.
+
+ .INPUTS
+ System.String
+ System.Byte[]
+
+ .OUTPUTS
+ System.String
+
+ .NOTES
+ Converts standard base64 output to JWT-safe base64url text by replacing URL-sensitive characters and removing padding.
+
+ .LINK
+ https://psmodule.io/Jwt/Functions/ConvertTo-Base64UrlString/
+
+ .LINK
+ https://jwt.io/
+ #>
+ [OutputType([string])]
+ [CmdletBinding()]
+ param(
+ # The string or byte array to encode.
+ [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
+ [ValidateNotNull()]
+ [Alias('in')]
+ [object] $InputObject
+ )
+
+ begin {}
+
+ process {
+ if ($InputObject -is [string]) {
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($InputObject)
+ [Convert]::ToBase64String($bytes) -replace '\+', '-' -replace '/', '_' -replace '='
+ } elseif ($InputObject -is [byte[]]) {
+ [Convert]::ToBase64String($InputObject) -replace '\+', '-' -replace '/', '_' -replace '='
+ } else {
+ throw [System.ArgumentException]::new("ConvertTo-Base64UrlString requires string or byte array input, received $($InputObject.GetType())")
+ }
+ }
+
+ end {}
+}
diff --git a/src/functions/public/Get-JwtHeader.ps1 b/src/functions/public/Get-JwtHeader.ps1
new file mode 100644
index 0000000..c38bf86
--- /dev/null
+++ b/src/functions/public/Get-JwtHeader.ps1
@@ -0,0 +1,56 @@
+function Get-JwtHeader {
+ <#
+ .SYNOPSIS
+ Gets the decoded header from a JWT.
+
+ .DESCRIPTION
+ Decodes and returns the JSON header segment from a JSON Web Token. The payload and signature are ignored.
+
+ .EXAMPLE
+ ```powershell
+ $jwt = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ.' #gitleaks:allow
+ Get-JwtHeader -Jwt $jwt
+ ```
+
+ Gets the decoded header JSON from an unsigned JWT.
+
+ .INPUTS
+ System.String
+
+ .OUTPUTS
+ System.String
+
+ .NOTES
+ This command decodes only the header segment and does not validate the token signature.
+
+ .LINK
+ https://psmodule.io/Jwt/Functions/Get-JwtHeader/
+
+ .LINK
+ https://jwt.io/
+ #>
+ [OutputType([string])]
+ [CmdletBinding()]
+ param(
+ # The JWT to read.
+ [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
+ [ValidateNotNullOrEmpty()]
+ [string] $Jwt
+ )
+
+ begin {}
+
+ process {
+ Write-Verbose "Processing JWT with length $($Jwt.Length) characters"
+ $parts = $Jwt.Split('.')
+ if ($parts.Count -ne 3) {
+ throw [System.ArgumentException]::new('JWT must have exactly 3 segments.')
+ }
+ if (-not $parts[0]) {
+ throw [System.ArgumentException]::new('JWT header segment is missing.')
+ }
+ ConvertFrom-Base64UrlString $parts[0]
+ }
+
+ end {}
+}
diff --git a/src/functions/public/Get-JwtPayload.ps1 b/src/functions/public/Get-JwtPayload.ps1
new file mode 100644
index 0000000..78eb909
--- /dev/null
+++ b/src/functions/public/Get-JwtPayload.ps1
@@ -0,0 +1,55 @@
+function Get-JwtPayload {
+ <#
+ .SYNOPSIS
+ Gets the decoded payload from a JWT.
+
+ .DESCRIPTION
+ Decodes and returns the JSON payload segment from a JSON Web Token. The header and signature are ignored.
+
+ .EXAMPLE
+ ```powershell
+ $jwt | Get-JwtPayload
+ ```
+
+ Gets the decoded payload JSON from a JWT.
+
+ .INPUTS
+ System.String
+
+ .OUTPUTS
+ System.String
+
+ .NOTES
+ This command decodes only the payload segment and does not validate the token signature.
+
+ .LINK
+ https://psmodule.io/Jwt/Functions/Get-JwtPayload/
+
+ .LINK
+ https://jwt.io/
+ #>
+ [OutputType([string])]
+ [CmdletBinding()]
+ param(
+ # The JWT to read.
+ [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
+ [ValidateNotNullOrEmpty()]
+ [string] $Jwt
+ )
+
+ begin {}
+
+ process {
+ Write-Verbose "Processing JWT with length $($Jwt.Length) characters"
+ $parts = $Jwt.Split('.')
+ if ($parts.Count -ne 3) {
+ throw [System.ArgumentException]::new('JWT must have exactly 3 segments.')
+ }
+ if (-not $parts[1]) {
+ throw [System.ArgumentException]::new('JWT payload segment is missing.')
+ }
+ ConvertFrom-Base64UrlString $parts[1]
+ }
+
+ end {}
+}
diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1
deleted file mode 100644
index 0e9aacf..0000000
--- a/src/functions/public/Get-PSModuleTest.ps1
+++ /dev/null
@@ -1,20 +0,0 @@
-#Requires -Modules Utilities
-
-function Get-PSModuleTest {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .EXAMPLE
- Test-PSModule -Name 'World'
-
- "Hello, World!"
- #>
- [CmdletBinding()]
- param (
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
diff --git a/src/functions/public/New-Jwt.ps1 b/src/functions/public/New-Jwt.ps1
new file mode 100644
index 0000000..1d5bcb6
--- /dev/null
+++ b/src/functions/public/New-Jwt.ps1
@@ -0,0 +1,152 @@
+function New-Jwt {
+ <#
+ .SYNOPSIS
+ Creates a JSON Web Token.
+
+ .DESCRIPTION
+ Creates a JWT from JSON header and payload strings. Supports RS256 with a signing certificate, HS256 with a
+ shared secret, and the none algorithm.
+
+ .EXAMPLE
+ ```powershell
+ $payload = '{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}'
+ $secret = 'a-string-secret-at-least-256-bits-long'
+
+ New-Jwt -Header '{"alg":"HS256","typ":"JWT"}' -PayloadJson $payload -Secret $secret
+ ```
+
+ Creates an HS256-signed JWT.
+
+ .EXAMPLE
+ ```powershell
+ $cert = (Get-ChildItem Cert:\CurrentUser\My)[1]
+ $jwt = New-Jwt -Cert $cert -PayloadJson '{"token1":"value1","token2":"value2"}'
+ $jwt.Split('.').Count
+ ```
+
+ Creates an RS256-signed JWT with a certificate private key and returns the number of JWT segments.
+
+ .INPUTS
+ System.String
+
+ .OUTPUTS
+ System.String
+
+ .NOTES
+ RS256 requires a certificate with a private key. HS256 requires a string or byte array secret.
+
+ .LINK
+ https://psmodule.io/Jwt/Functions/New-Jwt/
+
+ .LINK
+ https://jwt.io/
+ #>
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
+ 'PSUseShouldProcessForStateChangingFunctions', '',
+ Justification = 'New-Jwt creates an in-memory token and does not change system state.'
+ )]
+ [OutputType([string])]
+ [CmdletBinding()]
+ param(
+ # The JWT header JSON.
+ [Parameter()]
+ [ValidateNotNullOrEmpty()]
+ [string] $Header = '{"alg":"RS256","typ":"JWT"}',
+
+ # The JWT payload JSON.
+ [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
+ [ValidateNotNullOrEmpty()]
+ [string] $PayloadJson,
+
+ # The signing certificate to use for RS256 tokens.
+ [Parameter()]
+ [ValidateNotNull()]
+ [System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert,
+
+ # The string or byte array secret to use for HS256 tokens.
+ [Parameter()]
+ [ValidateNotNull()]
+ [object] $Secret
+ )
+
+ begin {}
+
+ process {
+ Write-Verbose "Payload to sign length: $($PayloadJson.Length) characters"
+
+ try {
+ $algorithm = (ConvertFrom-Json -InputObject $Header -ErrorAction Stop).alg
+ } catch {
+ throw [System.FormatException]::new("The supplied JWT header is not valid JSON. Header length: $($Header.Length) characters.")
+ }
+ if ([string]::IsNullOrEmpty($algorithm)) {
+ throw [System.FormatException]::new('The JWT header is missing the required "alg" claim.')
+ }
+ Write-Verbose "Algorithm: $algorithm"
+
+ try {
+ $null = ConvertFrom-Json -InputObject $PayloadJson -ErrorAction Stop
+ } catch {
+ throw [System.FormatException]::new("The supplied JWT payload is not valid JSON. Payload length: $($PayloadJson.Length) characters.")
+ }
+
+ $encodedHeader = ConvertTo-Base64UrlString $Header
+ $encodedPayload = ConvertTo-Base64UrlString $PayloadJson
+ $jwtContent = $encodedHeader + '.' + $encodedPayload
+ $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($jwtContent)
+
+ switch ($algorithm) {
+ 'RS256' {
+ if (-not $PSBoundParameters.ContainsKey('Cert')) {
+ throw [System.ArgumentException]::new('RS256 requires a -Cert parameter of type X509Certificate2.', 'Cert')
+ }
+ Write-Verbose "Signing certificate: $($Cert.Subject)"
+ $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Cert)
+ if ($null -eq $rsa) {
+ throw [System.ArgumentException]::new('The supplied certificate has no RSA private key and cannot be used to sign.', 'Cert')
+ } else {
+ try {
+ $signature = $rsa.SignData(
+ $contentBytes,
+ [Security.Cryptography.HashAlgorithmName]::SHA256,
+ [Security.Cryptography.RSASignaturePadding]::Pkcs1
+ )
+ $encodedSignature = ConvertTo-Base64UrlString $signature
+ } catch {
+ $message = "Signing with SHA256 and Pkcs1 padding failed using the certificate private key: $_"
+ throw [System.Exception]::new($message, $_.Exception)
+ } finally {
+ $rsa.Dispose()
+ }
+ }
+ }
+ 'HS256' {
+ if (-not ($PSBoundParameters.ContainsKey('Secret'))) {
+ throw [System.ArgumentException]::new('HS256 requires a -Secret parameter.', 'Secret')
+ }
+ if ($Secret -isnot [byte[]] -and $Secret -isnot [string]) {
+ throw [System.ArgumentException]::new("Expected Secret parameter as byte array or string, instead got $($Secret.GetType())", 'Secret')
+ }
+ $hmacsha256 = [System.Security.Cryptography.HMACSHA256]::new()
+ try {
+ $hmacsha256.Key = if ($Secret -is [byte[]]) { $Secret } else { [System.Text.Encoding]::UTF8.GetBytes($Secret) }
+ $encodedSignature = ConvertTo-Base64UrlString $hmacsha256.ComputeHash($contentBytes)
+ } catch {
+ throw [System.Exception]::new("Signing with HMACSHA256 failed: $_", $_.Exception)
+ } finally {
+ $hmacsha256.Dispose()
+ }
+ }
+ 'none' {
+ $encodedSignature = $null
+ }
+ default {
+ throw [System.NotSupportedException]::new('The algorithm is not one of the supported: "RS256", "HS256", "none".')
+ }
+ }
+
+ $jwtContent + '.' + $encodedSignature
+ }
+
+ end {}
+}
diff --git a/src/functions/public/New-PSModuleTest.ps1 b/src/functions/public/New-PSModuleTest.ps1
deleted file mode 100644
index d4e6e26..0000000
--- a/src/functions/public/New-PSModuleTest.ps1
+++ /dev/null
@@ -1,37 +0,0 @@
-#Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.0'}
-
-function New-PSModuleTest {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .EXAMPLE
- Test-PSModule -Name 'World'
-
- "Hello, World!"
-
- .NOTES
- Testing if a module can have a [Markdown based link](https://example.com).
- !"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.,"
- \[This is a test\]
- #>
- [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
- 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function',
- Justification = 'Reason for suppressing'
- )]
- [Alias('New-PSModuleTestAlias1')]
- [Alias('New-PSModuleTestAlias2')]
- [CmdletBinding()]
- param (
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
-
-New-Alias New-PSModuleTestAlias3 New-PSModuleTest
-New-Alias -Name New-PSModuleTestAlias4 -Value New-PSModuleTest
-
-
-Set-Alias New-PSModuleTestAlias5 New-PSModuleTest
diff --git a/src/functions/public/Set-PSModuleTest.ps1 b/src/functions/public/Set-PSModuleTest.ps1
deleted file mode 100644
index a87ac11..0000000
--- a/src/functions/public/Set-PSModuleTest.ps1
+++ /dev/null
@@ -1,22 +0,0 @@
-function Set-PSModuleTest {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .EXAMPLE
- Test-PSModule -Name 'World'
-
- "Hello, World!"
- #>
- [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
- 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function',
- Justification = 'Reason for suppressing'
- )]
- [CmdletBinding()]
- param (
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
diff --git a/src/functions/public/Test-Jwt.ps1 b/src/functions/public/Test-Jwt.ps1
new file mode 100644
index 0000000..445c474
--- /dev/null
+++ b/src/functions/public/Test-Jwt.ps1
@@ -0,0 +1,160 @@
+function Test-Jwt {
+ <#
+ .SYNOPSIS
+ Tests the cryptographic integrity of a JWT.
+
+ .DESCRIPTION
+ Verifies a JWT signature using the signing certificate for RS256 or a shared secret for HS256. Tokens using the
+ none algorithm are valid only when the signature segment is empty.
+
+ .EXAMPLE
+ ```powershell
+ $jwt | Test-Jwt -Secret 'a-string-secret-at-least-256-bits-long'
+ ```
+
+ Tests an HS256 JWT with a shared secret.
+
+ .EXAMPLE
+ ```powershell
+ $jwt | Test-Jwt -Cert $cert
+ ```
+
+ Tests an RS256 JWT with a public certificate.
+
+ .INPUTS
+ System.String
+
+ .OUTPUTS
+ System.Boolean
+
+ .NOTES
+ The Verify-JwtSignature alias is preserved for compatibility with the original module command surface.
+
+ .LINK
+ https://psmodule.io/Jwt/Functions/Test-Jwt/
+
+ .LINK
+ https://jwt.io/
+ #>
+ [OutputType([bool])]
+ [Alias('Verify-JwtSignature')]
+ [CmdletBinding()]
+ param(
+ # The JWT to test.
+ [Parameter(Mandatory, ValueFromPipeline, Position = 0)]
+ [ValidateNotNullOrEmpty()]
+ [string] $Jwt,
+
+ # The certificate to use for RS256 signature verification.
+ [Parameter()]
+ [ValidateNotNull()]
+ [System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert,
+
+ # The string or byte array secret to use for HS256 signature verification.
+ [Parameter()]
+ [ValidateNotNull()]
+ [object] $Secret
+ )
+
+ begin {}
+
+ process {
+ Write-Verbose "Verifying JWT with length $($Jwt.Length) characters"
+
+ $parts = $Jwt.Split('.')
+ if ($parts.Count -ne 3) {
+ throw [System.ArgumentException]::new('JWT must have exactly 3 segments.')
+ }
+ if (-not $parts[0]) {
+ throw [System.ArgumentException]::new('JWT header segment is missing.')
+ }
+ if (-not $parts[1]) {
+ throw [System.ArgumentException]::new('JWT payload segment is missing.')
+ }
+ $header = ConvertFrom-Base64UrlString $parts[0]
+ try {
+ $algorithm = (ConvertFrom-Json -InputObject $header -ErrorAction Stop).alg
+ } catch {
+ throw [System.FormatException]::new("The supplied JWT header segment is not valid JSON. Header length: $($header.Length) characters.")
+ }
+ if ([string]::IsNullOrEmpty($algorithm)) {
+ throw [System.FormatException]::new('The JWT header is missing the required "alg" claim.')
+ }
+ Write-Verbose "Algorithm: $algorithm"
+
+ switch ($algorithm) {
+ 'RS256' {
+ if (-not $PSBoundParameters.ContainsKey('Cert')) {
+ throw [System.ArgumentException]::new('RS256 requires a -Cert parameter of type X509Certificate2.', 'Cert')
+ }
+ if ([string]::IsNullOrEmpty($parts[2])) {
+ return $false
+ }
+ try {
+ $bytes = ConvertFrom-Base64UrlString $parts[2] -AsByteArray
+ } catch [System.FormatException] {
+ return $false
+ }
+ Write-Verbose "Using certificate with subject: $($Cert.Subject)"
+ $signedContent = [System.Text.Encoding]::UTF8.GetBytes($parts[0] + '.' + $parts[1])
+ $computed = [System.Security.Cryptography.SHA256]::HashData($signedContent)
+ $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($Cert)
+ if ($null -eq $rsa) {
+ throw [System.ArgumentException]::new('The supplied certificate has no RSA public key and cannot be used to verify.', 'Cert')
+ }
+ try {
+ $rsa.VerifyHash(
+ $computed,
+ $bytes,
+ [Security.Cryptography.HashAlgorithmName]::SHA256,
+ [Security.Cryptography.RSASignaturePadding]::Pkcs1
+ )
+ } finally {
+ $rsa.Dispose()
+ }
+ }
+ 'HS256' {
+ if (-not ($PSBoundParameters.ContainsKey('Secret'))) {
+ throw [System.ArgumentException]::new('HS256 requires a -Secret parameter.', 'Secret')
+ }
+ if ($Secret -isnot [byte[]] -and $Secret -isnot [string]) {
+ throw [System.ArgumentException]::new("Expected Secret parameter as byte array or string, instead got $($Secret.GetType())", 'Secret')
+ }
+ $hmacsha256 = [System.Security.Cryptography.HMACSHA256]::new()
+ try {
+ $hmacsha256.Key = if ($Secret -is [byte[]]) { $Secret } else { [System.Text.Encoding]::UTF8.GetBytes($Secret) }
+ $signedContent = [System.Text.Encoding]::UTF8.GetBytes($parts[0] + '.' + $parts[1])
+ $signature = $hmacsha256.ComputeHash($signedContent)
+ if (-not $parts[2]) {
+ $false
+ } else {
+ try {
+ $providedSignature = ConvertFrom-Base64UrlString $parts[2] -AsByteArray
+ } catch [System.FormatException] {
+ $providedSignature = $null
+ }
+ if ($null -eq $providedSignature -or $signature.Length -ne $providedSignature.Length) {
+ $false
+ } else {
+ $difference = 0
+ for ($index = 0; $index -lt $signature.Length; $index++) {
+ $difference = $difference -bor ($signature[$index] -bxor $providedSignature[$index])
+ }
+ $difference -eq 0
+ }
+ }
+ } finally {
+ $hmacsha256.Dispose()
+ }
+ }
+ 'none' {
+ $parts[2] -eq ''
+ }
+ default {
+ throw [System.NotSupportedException]::new('The algorithm is not one of the supported: "RS256", "HS256", "none".')
+ }
+ }
+ }
+
+ end {}
+}
diff --git a/src/functions/public/Test-PSModuleTest.ps1 b/src/functions/public/Test-PSModuleTest.ps1
deleted file mode 100644
index 26be2b9..0000000
--- a/src/functions/public/Test-PSModuleTest.ps1
+++ /dev/null
@@ -1,18 +0,0 @@
-function Test-PSModuleTest {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .EXAMPLE
- Test-PSModule -Name 'World'
-
- "Hello, World!"
- #>
- [CmdletBinding()]
- param (
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
diff --git a/src/header.ps1 b/src/header.ps1
deleted file mode 100644
index cc1fde9..0000000
--- a/src/header.ps1
+++ /dev/null
@@ -1,3 +0,0 @@
-[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')]
-[CmdletBinding()]
-param()
diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1
deleted file mode 100644
index 28396fb..0000000
--- a/src/init/initializer.ps1
+++ /dev/null
@@ -1,3 +0,0 @@
-Write-Verbose '-------------------------------'
-Write-Verbose '--- THIS IS AN INITIALIZER ---'
-Write-Verbose '-------------------------------'
diff --git a/src/manifest.psd1 b/src/manifest.psd1
index ff720bd..40bd8bb 100644
--- a/src/manifest.psd1
+++ b/src/manifest.psd1
@@ -1,5 +1,14 @@
-# This file always wins!
-# Use this file to override any of the framework defaults and generated values.
-@{
- ModuleVersion = '0.0.0'
+@{
+ PrivateData = @{
+ PSData = @{
+ Tags = @(
+ 'JWT'
+ 'JSON'
+ 'Token'
+ 'Authentication'
+ 'Security'
+ 'PSModule'
+ )
+ }
+ }
}
diff --git a/src/modules/OtherPSModule.psm1 b/src/modules/OtherPSModule.psm1
deleted file mode 100644
index 5d6af8e..0000000
--- a/src/modules/OtherPSModule.psm1
+++ /dev/null
@@ -1,19 +0,0 @@
-function Get-OtherPSModule {
- <#
- .SYNOPSIS
- Performs tests on a module.
-
- .DESCRIPTION
- A longer description of the function.
-
- .EXAMPLE
- Get-OtherPSModule -Name 'World'
- #>
- [CmdletBinding()]
- param(
- # Name of the person to greet.
- [Parameter(Mandatory)]
- [string] $Name
- )
- Write-Output "Hello, $Name!"
-}
diff --git a/src/scripts/loader.ps1 b/src/scripts/loader.ps1
deleted file mode 100644
index 973735a..0000000
--- a/src/scripts/loader.ps1
+++ /dev/null
@@ -1,3 +0,0 @@
-Write-Verbose '-------------------------'
-Write-Verbose '--- THIS IS A LOADER ---'
-Write-Verbose '-------------------------'
diff --git a/src/types/DirectoryInfo.Types.ps1xml b/src/types/DirectoryInfo.Types.ps1xml
deleted file mode 100644
index aef538b..0000000
--- a/src/types/DirectoryInfo.Types.ps1xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- System.IO.FileInfo
-
-
- Status
- Success
-
-
-
-
- System.IO.DirectoryInfo
-
-
- Status
- Success
-
-
-
-
diff --git a/src/types/FileInfo.Types.ps1xml b/src/types/FileInfo.Types.ps1xml
deleted file mode 100644
index 4cfaf6b..0000000
--- a/src/types/FileInfo.Types.ps1xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- System.IO.FileInfo
-
-
- Age
-
- ((Get-Date) - ($this.CreationTime)).Days
-
-
-
-
-
diff --git a/src/variables/private/PrivateVariables.ps1 b/src/variables/private/PrivateVariables.ps1
deleted file mode 100644
index f1fc2c3..0000000
--- a/src/variables/private/PrivateVariables.ps1
+++ /dev/null
@@ -1,47 +0,0 @@
-$script:HabitablePlanets = @(
- @{
- Name = 'Earth'
- Mass = 5.97
- Diameter = 12756
- DayLength = 24.0
- },
- @{
- Name = 'Mars'
- Mass = 0.642
- Diameter = 6792
- DayLength = 24.7
- },
- @{
- Name = 'Proxima Centauri b'
- Mass = 1.17
- Diameter = 11449
- DayLength = 5.15
- },
- @{
- Name = 'Kepler-442b'
- Mass = 2.34
- Diameter = 11349
- DayLength = 5.7
- },
- @{
- Name = 'Kepler-452b'
- Mass = 5.0
- Diameter = 17340
- DayLength = 20.0
- }
-)
-
-$script:InhabitedPlanets = @(
- @{
- Name = 'Earth'
- Mass = 5.97
- Diameter = 12756
- DayLength = 24.0
- },
- @{
- Name = 'Mars'
- Mass = 0.642
- Diameter = 6792
- DayLength = 24.7
- }
-)
diff --git a/src/variables/public/Moons.ps1 b/src/variables/public/Moons.ps1
deleted file mode 100644
index dd0f33c..0000000
--- a/src/variables/public/Moons.ps1
+++ /dev/null
@@ -1,6 +0,0 @@
-$script:Moons = @(
- @{
- Planet = 'Earth'
- Name = 'Moon'
- }
-)
diff --git a/src/variables/public/Planets.ps1 b/src/variables/public/Planets.ps1
deleted file mode 100644
index 736584b..0000000
--- a/src/variables/public/Planets.ps1
+++ /dev/null
@@ -1,20 +0,0 @@
-$script:Planets = @(
- @{
- Name = 'Mercury'
- Mass = 0.330
- Diameter = 4879
- DayLength = 4222.6
- },
- @{
- Name = 'Venus'
- Mass = 4.87
- Diameter = 12104
- DayLength = 2802.0
- },
- @{
- Name = 'Earth'
- Mass = 5.97
- Diameter = 12756
- DayLength = 24.0
- }
-)
diff --git a/src/variables/public/SolarSystems.ps1 b/src/variables/public/SolarSystems.ps1
deleted file mode 100644
index acbcedf..0000000
--- a/src/variables/public/SolarSystems.ps1
+++ /dev/null
@@ -1,17 +0,0 @@
-$script:SolarSystems = @(
- @{
- Name = 'Solar System'
- Planets = $script:Planets
- Moons = $script:Moons
- },
- @{
- Name = 'Alpha Centauri'
- Planets = @()
- Moons = @()
- },
- @{
- Name = 'Sirius'
- Planets = @()
- Moons = @()
- }
-)
diff --git a/tests/Data/TestCases.ps1 b/tests/Data/TestCases.ps1
new file mode 100644
index 0000000..f9e08fe
--- /dev/null
+++ b/tests/Data/TestCases.ps1
@@ -0,0 +1,36 @@
+@(
+ @{
+ Name = 'local HS256 token'
+ Header = '{"alg":"HS256","typ":"JWT"}'
+ HeaderEncoded = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
+ Payload = '{"sub":"joe","role":"admin"}'
+ PayloadEncoded = 'eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ'
+ Secret = 'super-secret'
+ ExtractionToken = @(
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
+ 'eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ'
+ 'c2lnbmF0dXJl'
+ ) -join '.'
+ ExpectedToken = $null
+ TamperedPayload = '{"sub":"joe","role":"user"}'
+ }
+ @{
+ Name = 'current jwt.io default HS256 example'
+ Header = '{"alg":"HS256","typ":"JWT"}'
+ HeaderEncoded = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
+ Payload = '{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}'
+ PayloadEncoded = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0'
+ Secret = 'a-string-secret-at-least-256-bits-long'
+ ExtractionToken = @(
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
+ 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0'
+ 'KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30'
+ ) -join '.'
+ ExpectedToken = @(
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
+ 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0'
+ 'KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30'
+ ) -join '.'
+ TamperedPayload = '{"sub":"1234567890","name":"John Doe","admin":false,"iat":1516239022}'
+ }
+)
diff --git a/tests/Jwt.Tests.ps1 b/tests/Jwt.Tests.ps1
new file mode 100644
index 0000000..d32242c
--- /dev/null
+++ b/tests/Jwt.Tests.ps1
@@ -0,0 +1,153 @@
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
+ 'PSUseDeclaredVarsMoreThanAssignments', '',
+ Justification = 'Required for Pester tests'
+)]
+[CmdletBinding()]
+param()
+
+Describe 'Data-driven tests' {
+ $testCases = . "$PSScriptRoot/Data/TestCases.ps1"
+
+ Context '' -ForEach $testCases {
+ It 'ConvertTo-Base64UrlString - encodes the header as base64url' {
+ ConvertTo-Base64UrlString $Header | Should -Be $HeaderEncoded
+ }
+
+ It 'ConvertFrom-Base64UrlString - decodes the header from base64url' {
+ ConvertFrom-Base64UrlString $HeaderEncoded | Should -Be $Header
+ }
+
+ It 'ConvertTo-Base64UrlString - encodes the payload as base64url' {
+ ConvertTo-Base64UrlString $Payload | Should -Be $PayloadEncoded
+ }
+
+ It 'ConvertFrom-Base64UrlString - decodes the payload from base64url' {
+ ConvertFrom-Base64UrlString $PayloadEncoded | Should -Be $Payload
+ }
+
+ It 'Get-JwtHeader - extracts the header' {
+ Get-JwtHeader $ExtractionToken | Should -Be $Header
+ }
+
+ It 'Get-JwtPayload - extracts the payload' {
+ Get-JwtPayload $ExtractionToken | Should -Be $Payload
+ }
+
+ It 'New-Jwt/Test-Jwt - creates and validates the token' {
+ $jwt = New-Jwt -Header $Header -PayloadJson $Payload -Secret $Secret
+
+ $parts = $jwt.Split('.')
+ $parts.Count | Should -Be 3
+ if ($null -ne $ExpectedToken) {
+ $jwt | Should -Be $ExpectedToken
+ }
+ Get-JwtHeader $jwt | Should -Be $Header
+ Get-JwtPayload $jwt | Should -Be $Payload
+ Test-Jwt -jwt $jwt -Secret $Secret | Should -BeTrue
+ }
+
+ It 'Test-Jwt - fails validation for a tampered token' {
+ $jwt = New-Jwt -Header $Header -PayloadJson $Payload -Secret $Secret
+ $parts = $jwt.Split('.')
+ $parts[1] = ConvertTo-Base64UrlString $TamperedPayload
+
+ Test-Jwt -jwt ($parts -join '.') -Secret $Secret | Should -BeFalse
+ }
+
+ It 'New-Jwt - requires a secret' {
+ { New-Jwt -Header $Header -PayloadJson $Payload } | Should -Throw '*HS256 requires*Secret*'
+ }
+ }
+
+ Context 'General behavior' {
+ It 'ConvertFrom-Base64UrlString - returns bytes when requested' {
+ $bytes = ConvertFrom-Base64UrlString 'SGVsbG8' -AsByteArray
+
+ [System.Text.Encoding]::UTF8.GetString($bytes) | Should -Be 'Hello'
+ }
+
+ It 'ConvertFrom-Base64UrlString - rejects invalid base64url length' {
+ { ConvertFrom-Base64UrlString 'A' } | Should -Throw '*Invalid base64url string length*'
+ }
+
+ It 'ConvertTo-Base64UrlString - throws for unsupported input types' {
+ { ConvertTo-Base64UrlString ([pscustomobject]@{ Value = 'invalid' }) } | Should -Throw '*requires string or byte array input*'
+ }
+
+ It 'New-Jwt/Test-Jwt - creates an unsigned token when using the none algorithm' {
+ $jwt = New-Jwt -Header '{"alg":"none","typ":"JWT"}' -PayloadJson '{"sub":"joe","role":"admin"}'
+
+ $jwt | Should -Match '\.$'
+ Test-Jwt -jwt $jwt | Should -BeTrue
+ }
+
+ It 'New-Jwt - requires the payload to be valid JSON' {
+ $header = '{"alg":"HS256","typ":"JWT"}'
+ { New-Jwt -Header $header -PayloadJson 'not-json' -Secret 'super-secret' } |
+ Should -Throw '*payload is not valid JSON*'
+ }
+
+ It 'New-Jwt - rejects a header missing the alg claim' {
+ { New-Jwt -Header '{"typ":"JWT"}' -PayloadJson '{"sub":"joe"}' -Secret 'super-secret' } |
+ Should -Throw '*missing the required "alg" claim*'
+ }
+
+ It 'Test-Jwt - rejects a token with a header missing the alg claim' {
+ $header = ConvertTo-Base64UrlString '{"typ":"JWT"}'
+ $payload = ConvertTo-Base64UrlString '{"sub":"joe"}'
+ $sig = ConvertTo-Base64UrlString 'fakesig'
+ { Test-Jwt "$header.$payload.$sig" -Secret 'super-secret' } |
+ Should -Throw '*missing the required "alg" claim*'
+ }
+
+ It 'Get-JwtHeader - requires exactly three JWT segments' {
+ { Get-JwtHeader 'header.payload' } | Should -Throw '*JWT must have exactly 3 segments*'
+ }
+
+ It 'Get-JwtPayload - requires a payload segment' {
+ { Get-JwtPayload 'header..signature' } | Should -Throw '*JWT payload segment is missing*'
+ }
+
+ It 'Test-Jwt - requires exactly three JWT segments' {
+ { Test-Jwt 'header.payload' } | Should -Throw '*JWT must have exactly 3 segments*'
+ }
+
+ It 'Test-Jwt - rejects unsigned tokens without a third segment' {
+ $header = ConvertTo-Base64UrlString '{"alg":"none","typ":"JWT"}'
+ $payload = ConvertTo-Base64UrlString '{"sub":"joe","role":"admin"}'
+
+ { Test-Jwt "$header.$payload" } | Should -Throw '*JWT must have exactly 3 segments*'
+ }
+
+ It 'Test-Jwt - returns false for an invalid HS256 signature segment' {
+ $jwt = New-Jwt -Header '{"alg":"HS256","typ":"JWT"}' -PayloadJson '{"sub":"joe","role":"admin"}' -Secret 'super-secret'
+ $parts = $jwt.Split('.')
+ $parts[2] = 'A'
+
+ Test-Jwt -jwt ($parts -join '.') -Secret 'super-secret' | Should -BeFalse
+ }
+
+ It 'Verbose output does not include JWT or payload values' {
+ $payload = '{"sub":"joe","role":"admin"}'
+ $jwt = New-Jwt -Header '{"alg":"HS256","typ":"JWT"}' -PayloadJson $payload -Secret 'super-secret'
+
+ $newJwtVerbose = & { New-Jwt -Header '{"alg":"HS256","typ":"JWT"}' -PayloadJson $payload -Secret 'super-secret' -Verbose } 4>&1 |
+ Where-Object { $_.GetType().Name -eq 'VerboseRecord' } |
+ Out-String
+ $getHeaderVerbose = & { Get-JwtHeader $jwt -Verbose } 4>&1 |
+ Where-Object { $_.GetType().Name -eq 'VerboseRecord' } |
+ Out-String
+ $getPayloadVerbose = & { Get-JwtPayload $jwt -Verbose } 4>&1 |
+ Where-Object { $_.GetType().Name -eq 'VerboseRecord' } |
+ Out-String
+ $testJwtVerbose = & { Test-Jwt -jwt $jwt -Secret 'super-secret' -Verbose } 4>&1 |
+ Where-Object { $_.GetType().Name -eq 'VerboseRecord' } |
+ Out-String
+
+ $newJwtVerbose | Should -Not -Match ([regex]::Escape($payload))
+ $getHeaderVerbose | Should -Not -Match ([regex]::Escape($jwt))
+ $getPayloadVerbose | Should -Not -Match ([regex]::Escape($jwt))
+ $testJwtVerbose | Should -Not -Match ([regex]::Escape($jwt))
+ }
+ }
+}
diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1
deleted file mode 100644
index b856855..0000000
--- a/tests/PSModuleTest.Tests.ps1
+++ /dev/null
@@ -1,25 +0,0 @@
-[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
- 'PSReviewUnusedParameter', '',
- Justification = 'Required for Pester tests'
-)]
-[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
- 'PSUseDeclaredVarsMoreThanAssignments', '',
- Justification = 'Required for Pester tests'
-)]
-[CmdletBinding()]
-param()
-
-Describe 'Module' {
- It 'Function: Get-PSModuleTest' {
- Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!'
- }
- It 'Function: New-PSModuleTest' {
- New-PSModuleTest -Name 'World' | Should -Be 'Hello, World!'
- }
- It 'Function: Set-PSModuleTest' {
- Set-PSModuleTest -Name 'World' | Should -Be 'Hello, World!'
- }
- It 'Function: Test-PSModuleTest' {
- Test-PSModuleTest -Name 'World' | Should -Be 'Hello, World!'
- }
-}