diff --git a/.gitignore b/.gitignore
index 6938c5f15..c594a1ce2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -269,3 +269,8 @@ docs/Getting-Started/Samples.md
# Dump Folder
Dump
+examples/certs/*-public.pem
+examples/certs/*-private.pem
+tests/certs/*
+/examples/certs
+examples/Authentication/certs/*
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b5be5ef8e..4dd3cd280 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -38,5 +38,13 @@
"javascript.format.insertSpaceBeforeFunctionParenthesis": false,
"[yaml]": {
"editor.tabSize": 2
+ },
+ "markdownlint.config": {
+ "default": true,
+ "MD045": false,
+ "MD033": false,
+ "MD026": {
+ "punctuation": ".,;:"
+ }
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3ffdf9f6f..30dcb52fe 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
-
+
[](https://raw.githubusercontent.com/Badgerati/Pode/master/LICENSE.txt)
[](https://badgerati.github.io/Pode)
@@ -53,36 +53,37 @@ Then navigate to `http://127.0.0.1:8000` in your browser.
## 🚀 Features
-* Cross-platform using PowerShell Core (with support for PS5)
-* Docker support, including images for ARM/Raspberry Pi
-* Azure Functions, AWS Lambda, and IIS support
-* OpenAPI specification version 3.0.x and 3.1.0
-* OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
-* Listen on a single or multiple IP(v4/v6) address/hostnames
-* Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
-* Host REST APIs, Web Pages, and Static Content (with caching)
-* Support for custom error pages
-* Request and Response compression using GZip/Deflate
-* Multi-thread support for incoming requests
-* Inbuilt template engine, with support for third-parties
-* Async timers for short-running repeatable processes
-* Async scheduled tasks using cron expressions for short/long-running processes
-* Supports logging to CLI, Files, and custom logic for other services like LogStash
-* Cross-state variable access across multiple runspaces
-* Restart the server via file monitoring, or defined periods/times
-* Ability to allow/deny requests from certain IP addresses and subnets
-* Basic rate limiting for IP addresses and subnets
-* Middleware and Sessions on web servers, with Flash message and CSRF support
-* Authentication on requests, such as Basic, Windows and Azure AD
-* Authorisation support on requests, using Roles, Groups, Scopes, etc.
-* Support for dynamically building Routes from Functions and Modules
-* Generate/bind self-signed certificates
-* Secret management support to load secrets from vaults
-* Support for File Watchers
-* In-memory caching, with optional support for external providers (such as Redis)
-* (Windows) Open the hosted server as a desktop application
-* FileBrowsing support
-* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese
+- ✅ Cross-platform using PowerShell Core (with support for PS5)
+- ✅ Docker support, including images for ARM/Raspberry Pi
+- ✅ Azure Functions, AWS Lambda, and IIS support
+- ✅ OpenAPI specification version 3.0.x and 3.1.0
+- ✅ OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
+- ✅ Listen on a single or multiple IP(v4/v6) addresses/hostnames
+- ✅ Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
+- ✅ Host REST APIs, Web Pages, and Static Content (with caching)
+- ✅ Support for custom error pages
+- ✅ Request and Response compression using GZip/Deflate
+- ✅ Multi-thread support for incoming requests
+- ✅ Inbuilt template engine, with support for third-parties
+- ✅ Async timers for short-running repeatable processes
+- ✅ Async scheduled tasks using cron expressions for short/long-running processes
+- ✅ Supports logging to CLI, Files, and custom logic for other services like LogStash
+- ✅ Cross-state variable access across multiple runspaces
+- ✅ Restart the server via file monitoring, or defined periods/times
+- ✅ Ability to allow/deny requests from certain IP addresses and subnets
+- ✅ Basic rate limiting for IP addresses and subnets
+- ✅ Middleware and Sessions on web servers, with Flash message and CSRF support
+- ✅ Authentication on requests, such as Basic, Windows and Azure AD
+- ✅ Authorisation support on requests, using Roles, Groups, Scopes, etc.
+- ✅ Enhanced authentication support, including Basic, Bearer (with JWT), Certificate, Digest, Form, OAuth2, and ApiKey (with JWT).
+- ✅ Support for dynamically building Routes from Functions and Modules
+- ✅ Generate/bind self-signed certificates
+- ✅ Secret management support to load secrets from vaults
+- ✅ Support for File Watchers
+- ✅ In-memory caching, with optional support for external providers (such as Redis)
+- ✅ (Windows) Open the hosted server as a desktop application
+- ✅ FileBrowsing support
+- ✅ Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese,Dutch and Chinese
## 📦 Install
diff --git a/docs/Tutorials/Authentication/Methods/ApiKey.md b/docs/Tutorials/Authentication/Methods/ApiKey.md
index 2167867cf..6d754daa0 100644
--- a/docs/Tutorials/Authentication/Methods/ApiKey.md
+++ b/docs/Tutorials/Authentication/Methods/ApiKey.md
@@ -26,13 +26,13 @@ Start-PodeServer {
}
```
-By default, Pode will look for an `X-API-KEY` header in the request. You can change this to Cookie or Query by using the `-Location` parameter. To change the name of what Pode looks for, you can use `-LocationName`.
+By default, Pode will look for an `X-API-KEY` header in the request. You can change this to Cookie or Query by using the `-ApiKeyLocation` parameter. To change the name of what Pode looks for, you can use `-LocationName`.
For example, to look for an `appId` query value:
```powershell
Start-PodeServer {
- New-PodeAuthScheme -ApiKey -Location Query -LocationName 'appId' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthScheme -ApiKey -ApiKeyLocation Query -LocationName 'appId' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($key)
# check if the key is valid, and get user
diff --git a/docs/Tutorials/Authentication/Methods/Bearer.md b/docs/Tutorials/Authentication/Methods/Bearer.md
index 77f92abbe..be388f8ae 100644
--- a/docs/Tutorials/Authentication/Methods/Bearer.md
+++ b/docs/Tutorials/Authentication/Methods/Bearer.md
@@ -6,13 +6,30 @@ Bearer authentication lets you authenticate a user based on a token, with option
Authorization: Bearer
```
+!!! note
+ **`New-PodeAuthScheme -Bearer` is deprecated.** Please use **`New-PodeAuthBearerScheme`**.
+
## Setup
-To start using Bearer authentication in Pode you can use `New-PodeAuthScheme -Bearer`, and then pipe the returned object into [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth). The parameter supplied to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock is the `$token` from the Authorization token:
+To start using Bearer authentication in Pode, call **`New-PodeAuthBearerScheme`**, and then pipe the returned object into [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth). The parameter supplied to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's **ScriptBlock** is the `$token` from the Authorization header.
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # check if the token is valid, and get user
+
+ return @{ User = $user }
+ }
+}
+```
+
+By default, Pode will look for a token in the **`Authorization`** header, verifying that it starts with the **`Bearer`** tag. You can customize this tag via **`-HeaderTag`**. You can also change the token extraction location to the **query string** using **`-Location Query`**. For the **`-Location query`** the standard tag is **`access_token`**:
```powershell
Start-PodeServer {
- New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthBearerScheme -Location Query | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($token)
# check if the token is valid, and get user
@@ -22,13 +39,129 @@ Start-PodeServer {
}
```
-By default, Pode will check if the request's header contains an `Authorization` key, and whether the value of that key starts with `Bearer` tag. The `New-PodeAuthScheme -Bearer` function can be supplied parameters to customise the tag using `-HeaderTag`.
+**Note:** Per [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750), using the Authorization header is recommended for sending bearer tokens. Query parameters should only be used when headers are not feasible, as query strings may be logged in URLs, potentially exposing sensitive information.
+
+## JWT Support
+
+`New-PodeAuthBearerScheme` supports **JWT authentication** with various security levels and algorithms. Set **`-AsJWT`** to enable JWT validation. Depending on the chosen algorithm, you can specify:
+
+- **HMAC**-based secret keys (`-Secret`)
+- **Certificate**-based parameters (`-Certificate`, `-CertificateThumbprint`, `-CertificateName`, `-X509Certificate`, `-SelfSigned`)
+- The **RSA padding scheme** (`-RsaPaddingScheme`)
+- The **JWT verification mode** (`-JwtVerificationMode`)
+
+### JwtVerificationMode
+
+Defines how aggressively JWT claims should be checked:
+
+- **Strict**: Requires all standard claims:
+ - `exp` (Expiration Time)
+ - `nbf` (Not Before)
+ - `iat` (Issued At)
+ - `iss` (Issuer)
+ - `aud` (Audience)
+ - `jti` (JWT ID)
+
+- **Moderate**: Allows missing `iss` (Issuer) and `aud` (Audience) but still checks expiration (`exp`).
+- **Lenient**: Ignores missing `iss` and `aud`, only verifies expiration (`exp`), not-before (`nbf`), and issued-at (`iat`).
+
+### HMAC Example
+
+Here’s an example using **HMAC** (HS256) JWT validation:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -Algorithm 'HS256' `
+ -Secret (ConvertTo-SecureString "MySecretKey" -AsPlainText -Force) `
+ -JwtVerificationMode 'Strict' |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate and decode JWT, then extract user details
+
+ return @{ User = $user }
+ }
+}
+```
+
+### Certificate-Based Example
+
+For **RSA/ECDSA** JWT validation, you can specify a **certificate** or **thumbprint** instead of a secret key. Pode will infer the appropriate signing algorithms (e.g., RS256, ES256) from the certificate. For instance, using a local **PFX** certificate file:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -Algorithm 'RS256' `
+ -Certificate "C:\path\to\cert.pfx" `
+ -CertificatePassword (ConvertTo-SecureString "CertPass" -AsPlainText -Force) `
+ -JwtVerificationMode 'Moderate' |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate JWT and extract user
+
+ return @{ User = $user }
+ }
+}
+```
+
+### Self-Signed Certificate Example
+
+For testing purposes or internal deployments, you can use the **`-SelfSigned`** parameter, which automatically generates an **ephemeral self-signed ECDSA certificate** (ES384) for JWT signing. This avoids the need to manually create and manage certificate files.
+
+#### Example:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -SelfSigned |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate JWT and extract user
+
+ return @{ User = $user }
+ }
+}
+```
+
+This is equivalent to manually generating a self-signed ECDSA certificate and passing it via `-X509Certificate`:
+
+```powershell
+Start-PodeServer {
+ $x509Certificate = New-PodeSelfSignedCertificate `
+ -CommonName 'JWT Signing Certificate' `
+ -KeyType ECDSA `
+ -KeyLength 384 `
+ -CertificatePurpose CodeSigning `
+ -Ephemeral
+
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -X509Certificate $x509Certificate |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # validate JWT and extract user
+
+ return @{ User = $user }
+ }
+}
+```
+
+Using `-SelfSigned` simplifies setup by automatically handling certificate creation and disposal, making it a convenient choice for local development and testing scenarios.
+
+## Scope Validation
-You can also optionally return a `Scope` property alongside the `User`. If you specify any scopes with [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme) then it will be validated in the Bearer's post validator - a 403 will be returned if the scope is invalid.
+You can optionally include `-Scope` when creating the scheme. Pode will validate any returned `Scope` from your auth **ScriptBlock** against the scheme’s required scopes. If the scope is invalid, Pode will return 403 (Forbidden).
```powershell
Start-PodeServer {
- New-PodeAuthScheme -Bearer -Scope 'write' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthBearerScheme -Scope 'write' | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($token)
# check if the token is valid, and get user
@@ -38,11 +171,13 @@ Start-PodeServer {
}
```
+
+
## Middleware
-Once configured you can start using Bearer authentication to validate incoming requests. You can either configure the validation to happen on every Route as global Middleware, or as custom Route Middleware.
+Once configured, you can instruct Pode to validate every request with Bearer authentication by using **Global Middleware**, or you can require it on individual Routes.
-The following will use Bearer authentication to validate every request on every Route:
+**Global Middleware Example** – Validate **every** incoming request:
```powershell
Start-PodeServer {
@@ -50,7 +185,7 @@ Start-PodeServer {
}
```
-Whereas the following example will use Bearer authentication to only validate requests on specific a Route:
+**Route-Specific Example** – Validate only on a certain Route:
```powershell
Start-PodeServer {
@@ -60,46 +195,42 @@ Start-PodeServer {
}
```
-## JWT
-
-You can supply a JWT using Bearer authentication, for more details [see here](../JWT).
-
## Full Example
-The following full example of Bearer authentication will setup and configure authentication, validate the token, and then validate on a specific Route:
+Below is a complete example demonstrating Bearer authentication with JWT. It configures a server, sets up JWT validation with a shared secret, and validates requests on one route (`/cpu`) while leaving another (`/memory`) open:
```powershell
Start-PodeServer {
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
- # setup bearer authentication to validate a user
- New-PodeAuthScheme -Bearer | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
- param($token)
-
- # here you'd check a real storage, this is just for example
- if ($token -eq 'test-token') {
- return @{
- User = @{
- 'ID' ='M0R7Y302'
- 'Name' = 'Morty'
- 'Type' = 'Human'
+ # Setup Bearer authentication to validate a user via JWT
+ New-PodeAuthBearerScheme -AsJWT -Algorithm 'HS256' -Secret (ConvertTo-SecureString "MySecretKey" -AsPlainText -Force) -JwtVerificationMode 'Lenient' |
+ Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ param($token)
+
+ # Example: in real usage, you would decode/verify the JWT fully
+ if ($token -eq 'test-token') {
+ return @{
+ User = @{
+ 'ID' = 'M0R7Y302'
+ 'Name' = 'Morty'
+ 'Type' = 'Human'
+ }
+ # Scope = 'read'
}
- # Scope = 'read'
}
- }
- # authentication failed
- return $null
- }
+ # authentication failed
+ return $null
+ }
- # check the request on this route against the authentication
+ # Validate against the authentication on this route
Add-PodeRoute -Method Get -Path '/cpu' -Authentication 'Authenticate' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'cpu' = 82 }
}
- # this route will not be validated against the authentication
+ # Open route, no auth required
Add-PodeRoute -Method Get -Path '/memory' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'memory' = 14 }
}
}
-```
diff --git a/docs/Tutorials/Authentication/Methods/Digest.md b/docs/Tutorials/Authentication/Methods/Digest.md
index 95e1e9570..c9cfca84e 100644
--- a/docs/Tutorials/Authentication/Methods/Digest.md
+++ b/docs/Tutorials/Authentication/Methods/Digest.md
@@ -1,14 +1,16 @@
# Digest
-Digest authentication lets you authenticate a user without actually sending the password to the server. Instead the a request is made to the server, and a challenge issued back for credentials. The authentication is then done by comparing hashes generated by the client and server using the user's password as a secret key.
+Digest authentication allows secure user authentication without sending the password to the server. Instead, the client receives a challenge from the server and responds with a hash-based authentication response. The server then verifies the hash using the stored password as a secret key.
+
+**Pode's Digest Authentication is compliant with [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616)**, ensuring compatibility with standard authentication mechanisms.
## Setup
-To setup and start using Digest authentication in Pode you use the `New-PodeAuthScheme -Digest` function, and then pipe this into the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function. The parameters supplied to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock are the `$username`, and a HashTable containing the parameters from the Authorization header:
+To configure Digest authentication in Pode, use the `New-PodeAuthScheme -Digest` function and pass it to [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth). The parameters supplied to the `Add-PodeAuth` function's ScriptBlock include the `$username` and a hashtable containing the authentication parameters extracted from the `Authorization` header:
```powershell
Start-PodeServer {
- New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ New-PodeAuthScheme -Digest -Algorithm "SHA-256" -QualityOfProtection "auth-int" | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($username, $params)
# check if the user is valid
@@ -18,28 +20,70 @@ Start-PodeServer {
}
```
-Unlike other forms of authentication where you only need return the User on success. Digest requires you to also return the Password of the user as a separate property. This password is what is used as the secret key to generate the client's response hash, and allows the server to re-generate the hash for validation. (Not returning the password will result in an HTTP 401 challenge response).
+Unlike other authentication methods, where only a user object is returned on success, Digest authentication **requires returning the password** (or hash) as a separate property. The password acts as the secret key to regenerate the client’s hash response for verification. Not returning the password results in an HTTP `401 Unauthorized` challenge response.
+
+### RFC 7616 Compliance
+
+Pode’s Digest authentication implementation adheres to **RFC 7616**, ensuring:
+
+- Use of **nonce-based challenge-response authentication**
+- Support for **multiple hashing algorithms** beyond MD5
+- Support for **Quality of Protection (QoP)**, including `auth` and `auth-int`
+- Correct formatting of **WWW-Authenticate** headers on authentication failure
+
+!!! note
+ SHA-384 is **not** part of RFC 7616 but has been added for consistency with other modern cryptographic algorithms and to provide additional security options.
+
+### Supported Algorithms
+
+Pode now supports multiple algorithms for Digest authentication. The `-Algorithm` parameter allows selecting one or more of the following:
+
+- `MD5`
+- `SHA-1`
+- `SHA-256`
+- `SHA-384`
+- `SHA-512`
+- `SHA-512/256`
+
+Pode automatically includes all supported algorithms in the `WWW-Authenticate` challenge header, allowing clients to select the strongest available option.
+
+### Quality of Protection (QoP)
+
+The `-QualityOfProtection` parameter (`-qop`) allows choosing between:
+
+- `"auth"` (authentication only)
+- `"auth-int"` (authentication with message integrity protection)
+
+If `auth-int` is used, the client includes a hash of the request body in the authentication response, ensuring the request content has not been altered.
-By default, Pode will check if the Request's header contains an `Authorization` key, and whether the value of that key starts with `Digest` tag. The `New-PodeAuthScheme -Digest` function can be supplied parameters to customise the tag using `-HeaderTag`. Pode will also gather the rest of the parameters in the header such as the Nonce, NonceCount, etc. An HTTP 401 challenge will be sent back if the Authorization header is invalid.
+## Handling Authentication Requests
-The HashTable of parameters sent to the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock are the following:
+By default, Pode checks if the request contains an `Authorization` header with the `Digest` scheme. The `New-PodeAuthScheme -Digest` function can be customized using the `-HeaderTag` parameter to modify the tag used in the request header. Pode also extracts all required parameters from the header, including the nonce, nonce count, and QoP options.
-| Parameter | Description |
-| --------- | ----------- |
-| cnonce | A nonce value generated by the client |
-| nc | The count of time the client has used the server nonce |
-| nonce | A nonce value generated by the server |
-| qop | Fixed to 'auth' |
-| realm | The realm description from the server's HTTP 401 challenge |
-| response | The generated hash value of all these parameters from the client |
-| uri | The URI path that needs authentication |
-| username | The username of the user that needs authenticating |
+If the `Authorization` header is missing or invalid, Pode returns an HTTP `401 Unauthorized` response with a `WWW-Authenticate` challenge.
+
+### Digest Authentication Parameters
+
+The hashtable of parameters passed to the `Add-PodeAuth` function’s ScriptBlock includes the following:
+
+| Parameter | Description |
+|------------|--------------|
+| `cnonce` | A nonce value generated by the client. |
+| `nc` | The count of times the client has used the server nonce. |
+| `nonce` | A nonce value generated by the server. |
+| `qop` | The quality of protection requested (`auth` or `auth-int`). |
+| `realm` | The authentication realm from the server's challenge. |
+| `response` | The hash generated by the client for authentication. |
+| `uri` | The URI path that requires authentication. |
+| `username` | The username provided for authentication. |
## Middleware
-Once configured you can start using Digest authentication to validate incoming Requests. You can either configure the validation to happen on every Route as global Middleware, or as custom Route Middleware.
+Digest authentication can be applied globally to all requests using `Add-PodeAuthMiddleware` or to specific routes via the `-Authentication` parameter.
-The following will use Digest authentication to validate every request on every Route:
+### Global Middleware
+
+To apply Digest authentication globally to all routes:
```powershell
Start-PodeServer {
@@ -47,7 +91,9 @@ Start-PodeServer {
}
```
-Whereas the following example will use Digest authentication to only validate requests on specific a Route:
+### Per-Route Middleware
+
+To enforce Digest authentication only on specific routes:
```powershell
Start-PodeServer {
@@ -59,40 +105,116 @@ Start-PodeServer {
## Full Example
-The following full example of Digest authentication will setup and configure authentication, validate that a user's username is valid, and then validate on a specific Route:
+The following example sets up Digest authentication with SHA-256 and `auth-int`, validates a user, and applies authentication to a specific route:
```powershell
Start-PodeServer {
Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
- # setup digest authentication to validate a user
- New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
+ # Setup Digest authentication with SHA-256 and auth-int
+ New-PodeAuthScheme -Digest -Algorithm "SHA-256" -QualityOfProtection "auth-int" | Add-PodeAuth -Name 'Authenticate' -Sessionless -ScriptBlock {
param($username, $params)
- # here you'd check a real user storage, this is just for example
+ # Example user validation
if ($username -eq 'morty') {
return @{
User = @{
- 'ID' ='M0R7Y302'
- 'Name' = 'Morty';
- 'Type' = 'Human';
+ 'ID' = 'M0R7Y302'
+ 'Name' = 'Morty'
+ 'Type' = 'Human'
}
Password = 'pickle'
}
}
- # authentication failed
+ # Authentication failed
return $null
}
- # check the request on this route against the authentication
+ # Protect the /cpu route with Digest authentication
Add-PodeRoute -Method Get -Path '/cpu' -Authentication 'Authenticate' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'cpu' = 82 }
}
- # this route will not be validated against the authentication
+ # The /memory route is accessible without authentication
Add-PodeRoute -Method Get -Path '/memory' -ScriptBlock {
Write-PodeJsonResponse -Value @{ 'memory' = 14 }
}
}
```
+
+### **Windows-Specific Limitations and the Pode Client Module**
+
+Windows' built-in Digest authentication has several **critical limitations** that restrict its compatibility with modern security practices:
+
+- **Limited to MD5:** Windows does not support stronger hashing algorithms like SHA-256 or SHA-512.
+- **No Support for `auth-int`:** Integrity protection (`auth-int`) is not available, making it less secure.
+- **Fails with Multiple Algorithms:** If the `WWW-Authenticate` header lists multiple algorithms, Windows' built-in implementation fails to negotiate properly.
+- **Lack of Algorithm Negotiation:** Windows cannot automatically select the strongest supported algorithm from a list.
+
+### **Overcoming Windows Limitations with the Pode Client Module**
+
+To bypass these **Windows client limitations**, Pode provides a custom **client module** that supports full RFC 7616-compliant Digest authentication. This module allows PowerShell scripts to authenticate using modern algorithms, multiple QoP modes, and cross-platform compatibility.
+
+The **client module** is available under:
+
+```powershell
+Import-Module ./examples/Authentication/Modules/Invoke-Digest.psm1
+```
+
+By using this module, you can perform **secure Digest authentication** in PowerShell, even on Windows, without being restricted to **MD5-only authentication**.
+
+The module includes the following functions:
+
+### **Invoke-WebRequestDigest**
+
+A replacement for `Invoke-WebRequest` that supports Digest authentication.
+
+#### **Example Usage**
+
+```powershell
+Import-Module './examples/Authentication/Modules/Invoke-Digest.psm1'
+
+# Define the URI and credentials
+$uri = 'http://localhost:8081/users'
+$username = 'morty'
+$password = 'pickle'
+
+# Convert the password to a SecureString and create a credential object
+$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+$credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+# Make a GET request using Digest authentication
+$response = Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential
+
+# Display response headers and content
+$response.Headers | Format-List
+Write-Output $response.Content
+```
+
+---
+
+### **Invoke-RestMethodDigest**
+
+A replacement for `Invoke-RestMethod` that supports Digest authentication.
+
+#### **Example Usage**
+
+```powershell
+Import-Module './examples/Authentication/Modules/Invoke-Digest.psm1'
+
+# Define the URI and credentials
+$uri = 'http://localhost:8081/users'
+$username = 'morty'
+$password = 'pickle'
+
+# Convert the password to a SecureString and create a credential object
+$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+$credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+# Make a GET request and automatically parse JSON response
+$response = Invoke-RestMethodDigest -Uri $uri -Method 'GET' -Credential $credential
+
+# Output the parsed response
+$response
+```
\ No newline at end of file
diff --git a/docs/Tutorials/Authentication/Methods/JWT.md b/docs/Tutorials/Authentication/Methods/JWT.md
index 9a24416ff..3e2453921 100644
--- a/docs/Tutorials/Authentication/Methods/JWT.md
+++ b/docs/Tutorials/Authentication/Methods/JWT.md
@@ -1,112 +1,299 @@
-# JWT
+# Create a JWT
-Pode has inbuilt JWT parsing for either [Bearer](../Bearer) or [API Key](../ApiKey) authentications. Pode will attempt to validate and parse the token/key as a JWT, and if successful the JWT's payload will be passed as the parameter to [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth), instead of the token/key.
+Pode provides a [`ConvertTo-PodeJwt`](../../../../Functions/Authentication/ConvertTo-PodeJwt) command that builds and signs a JWT for you. You can provide:
-For more information on JWTs, see the [official website](https://jwt.io).
+- **`-Header`**: A hashtable defining fields like `alg`, `typ`, etc.
+- **`-Payload`**: A hashtable for JWT claims (e.g., `sub`, `exp`, `nbf`, and other custom claims).
+- **`-Secret`**/**`-Certificate`**/**`-CertificateThumbprint`**, etc.: If you want to sign the JWT (for HS*, RS*, ES*, PS* algorithms).
+- **`-IgnoreSignature`**: If you want a token with no signature (alg = none).
+- **`-Authentication`**: To reference an existing named authentication scheme, automatically pulling its parameters (algorithm, secret, certificate, etc.) so the generated JWT is recognized by that scheme.
-## Setup
+## Customizing the Header/Payload
-To start using JWT authentication, you can supply the `-AsJWT` switch with either the `-Bearer` or `-ApiKey` switch on [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme). You can also supply an optional `-Secret` that the JWT signature uses so Pode can validate the JWT:
+When generating a JWT using **`ConvertTo-PodeJwt`**, you can specify parameters that either:
+
+1. **Manually** define the header/payload using `-Header` and `-Payload`, or
+2. **Automatically** set standard claims via shortcut parameters like `-Expiration`, `-Issuer`, `-Audience`, etc.
+
+You can also combine these approaches—Pode merges everything into the final token unless you use **`-NoStandardClaims`** to disable automatic claims.
+
+Below are the **primary parameters** you can pass to **`ConvertTo-PodeJwt`**:
+
+### Header and Payload
+
+- **`-Header`**: A hashtable for JWT header fields (e.g., `alg`, `typ`).
+- **`-Payload`**: A hashtable for arbitrary/custom claims (e.g., `role`, `scope`, etc.).
+- **`-NoStandardClaims`**: If specified, **no** standard claims are auto-generated (e.g., no `exp`, `nbf`, `iat`, etc.). This is useful if you want full control over claims in `-Payload`.
+
+#### Standard Claims Parameters
+
+These automatically populate or override common JWT claims:
+
+- **`-Expiration`** (`int`, default 3600)
+ - Sets the `exp` (expiration time) to the current time + `Expiration` (in seconds).
+ - For example, **3600** means `exp` = now + 1 hour.
+
+- **`-NotBefore`** (`int`, default 0)
+ - Sets the `nbf` (not-before) to current time + `NotBefore` (in seconds).
+ - **0** = immediate validity; **60** = valid 1 minute from now, etc.
+
+- **`-IssuedAt`** (`int`, default 0)
+ - Sets the `iat` (issued-at) time.
+ - **0** means “use current time.” Any other integer is added to the current time as seconds.
+
+- **`-Issuer`** (`string`)
+ - Sets the `iss` (issuer) claim, e.g. `"auth.example.com"`.
+
+- **`-Subject`** (`string`)
+ - Sets the `sub` (subject) claim, e.g. `"user123"`.
+
+- **`-Audience`** (`string`)
+ - Sets the `aud` (audience) claim, e.g. `"myapi.example.com"`.
+
+- **`-JwtId`** (`string`)
+ - Sets the `jti` (JWT ID) claim, a unique identifier for the token.
+
+If you **also** supply the same claims in your `-Payload` hashtable, Pode typically defers to your explicit claim unless **`-NoStandardClaims`** is omitted, in which case these parameters can overwrite the payload-based claims.
+
+---
+
+### Example Usage
+
+Below is an example that **automatically** sets standard claims for expiration (1 hour from now), not-before (starts immediately), and an issuer, while also providing a custom header/payload:
```powershell
-# jwt with no signature:
-New-PodeAuthScheme -Bearer -AsJWT | Add-PodeAuth -Name 'Example' -Sessionless -ScriptBlock {
- param($payload)
+$header = @{
+ alg = 'HS256'
+ typ = 'JWT'
}
-# jwt with signature, signed with secret "abc":
-New-PodeAuthScheme -ApiKey -AsJWT -Secret 'abc' | Add-PodeAuth -Name 'Example' -Sessionless -ScriptBlock {
- param($payload)
+$payload = @{
+ role = 'admin'
+ customClaim = 'someValue'
}
+
+$jwt = ConvertTo-PodeJwt `
+ -Header $header `
+ -Payload $payload `
+ -Secret 'SuperSecretKey' `
+ -Expiration 3600 `
+ -NotBefore 0 `
+ -Issuer 'auth.example.com' `
+ -Subject 'user123' `
+ -Audience 'myapi.example.com' `
+ -JwtId 'unique-token-id'
+
+Write-PodeJsonResponse -Value @{ token = $jwt }
```
-The `$payload` will be a PSCustomObject of the converted JSON payload. For example, sending the following unsigned JWT in a request:
+This produces a JWT that includes:
-```plain
-eyJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6Im1vcnR5Iiwic3ViIjoiMTIzIn0.
-```
+- A header with `alg = HS256`, `typ = JWT`.
+- Standard claims: `exp`, `nbf`, `iat`, `iss`, `sub`, `aud`, `jti`.
+- Custom claims: `role`, `customClaim`.
-would produce a payload of:
+If you **don’t** want Pode to generate any standard claims at all (perhaps you want to define everything in `-Payload` yourself), include **`-NoStandardClaims`**:
-```plain
-sub: 123
-username: morty
+```powershell
+$jwt = ConvertTo-PodeJwt -NoStandardClaims -Payload @{ sub='user123'; customKey='abc' } -Secret 'SuperSecretKey'
```
-### Algorithms
+No `exp`, `nbf`, or `iat` will be automatically added.
-Pode supports the following algorithms for JWT signatures:
+Similarly, if you have a named scheme:
-* None
-* HS256
-* HS384
-* HS512
+```powershell
+New-PodeAuthBearerScheme -AsJWT -Algorithm 'RS256' -Certificate 'C:\cert.pfx' -CertificatePassword (ConvertTo-SecureString "CertPass" -AsPlainText -Force) |
+ Add-PodeAuth -Name 'ExampleApiKeyCert'
-For `none`, Pode expects there to be no signature with the JWT. For other algorithms, a `-Secret` is required, and a signature must be supplied with the JWT in requests.
+Add-PodeRoute -Method Post -Path '/login' -ScriptBlock {
+ $jwt = ConvertTo-PodeJwt `
+ -Authentication 'ExampleApiKeyCert' `
+ -Issuer 'auth.example.com' `
+ -Expiration 3600 `
+ -Subject 'user123'
-### Payload
+ Write-PodeJsonResponse -Value @{ token = $jwt }
+}
+```
-If the payload of the JWT contains a expiry (`exp`) or a not before (`nbf`) timestamp, Pode will validate it and return a 400 if the JWT is expired/not started.
+Here, Pode automatically applies the RS256 certificate from **`ExampleApiKeyCert`** and merges your standard-claims parameters, producing a token recognized by that same scheme upon verification.
-## Usage
+## Using `-Authentication`
-To send the JWT in a request, the JWT should be sent in place of where the usual bearer token/API key would have been. For example, for bearer it would be in the Authorization header:
+If you have already set up an authentication scheme, for instance:
-```plain
-Authorization: Bearer
+```powershell
+New-PodeAuthBearerScheme -AsJWT -Algorithm 'RS256' -Certificate 'C:\path\to\cert.pfx' -CertificatePassword (ConvertTo-SecureString "CertPass" -AsPlainText -Force) |
+ Add-PodeAuth -Name 'ExampleApiKeyCert'
```
-and for API keys, it would be in the location defined (header, cookie, or query string). For example, in the X-API-KEY header:
+then you can **reuse** this scheme’s configuration when creating a token by calling:
-```plain
-X-API-KEY:
-```
+```powershell
+$jwt = ConvertTo-PodeJwt -Authentication 'ExampleApiKeyCert'
-## Create JWT
+# e.g., return the new JWT to a client
+Write-PodeJsonResponse -StatusCode 200 -Value @{ jwt_token = $jwt }
+```
-Pode has a simple [`ConvertTo-PodeJwt`](../../../../Functions/Authentication/ConvertTo-PodeJwt) that will build a JWT for you. It accepts a hashtable for `-Header` and `-Payload`, as well as an optional `-Secret`.
+Pode automatically looks up the **`ExampleApiKeyCert`** auth scheme, retrieves its signing algorithm and key/certificate, and uses those to generate a valid JWT. This ensures that the JWT you create can later be **decoded and verified** by the same auth scheme without having to re-specify all parameters (secret, certificate, etc.).
-The function will run some simple validation, and them build the JWT for you.
+### Example
-For example:
+Below is a short example of how you might implement a **login** route that returns a signed JWT:
```powershell
-$header = @{
- alg = 'hs256'
- typ = 'JWT'
+Add-PodeRoute -Method Post -Path '/user/login' -ScriptBlock {
+ param()
+
+ # In a real scenario, you'd validate the incoming credentials from $WebEvent.data
+ $username = $WebEvent.Data['username']
+ $password = $WebEvent.Data['password']
+
+ # If valid, generate a JWT that matches the 'ExampleApiKeyCert' scheme
+ $jwt = ConvertTo-PodeJwt -Authentication 'ExampleApiKeyCert'
+
+ Write-PodeJsonResponse -StatusCode 200 -Value @{ jwt_token = $jwt }
}
+```
-$payload = @{
- sub = '123'
- name = 'John Doe'
- exp = ([System.DateTimeOffset]::Now.AddDays(1).ToUnixTimeSeconds())
+In this example, the **`-Authentication`** parameter ensures Pode uses the RS256 certificate-based configuration already defined by the `ExampleApiKeyCert` auth scheme, producing a token that is verifiable by that same scheme on future requests.
+
+
+Below is an **updated JWT Lifecycle guide** for Pode, clarifying that **Pode automatically validates the token** when you attach `-Authentication` to a route, and that **`ConvertFrom-PodeJwt`** is generally used for **inspecting** or **debugging** token contents.
+
+
+## Managing the JWT Lifecycle in Pode
+
+In many scenarios, you need more than just generating JWTs—you also need endpoints or logic for **renewing** and **inspecting** tokens. Pode’s built-in commands and authentication features enable these patterns quickly:
+
+1. **Creating a JWT**: Use [`ConvertTo-PodeJwt`](https://github.com/Badgerati/Pode/blob/develop/Functions/Authentication/ConvertTo-PodeJwt.ps1) to build and sign a JWT.
+2. **Automatic Validation**: Rely on Pode’s bearer auth if a route uses `-Authentication 'YourBearerScheme'`.
+3. **Decoding/Inspecting a JWT**: Use `ConvertFrom-PodeJwt` if you want to explicitly decode the JWT for debugging or extracting claims.
+4. **Renewing/Extending a JWT**: Use `Update-PodeJwt` to reissue a token with a new expiration.
+
+## 1. Creating a JWT
+
+See the [“Create a JWT” guide](#create-a-jwt) for details on using `ConvertTo-PodeJwt`. You can:
+
+- Define a scheme in Pode (e.g., `Bearer_JWT_ES512`) that holds your algorithm and certificates/secrets.
+- Generate tokens by referencing `-Authentication 'Bearer_JWT_ES512'`.
+- Optionally set custom claims, expiration, issuer, etc.
+
+This creation step often happens inside a **login** route, as shown in the example below:
+
+```powershell
+function Test-User {
+ param($username, $password)
+ if ($username -eq 'morty' -and $password -eq 'pickle') {
+ return @{
+ Id = 'M0R7Y302'
+ Username = 'morty.smith'
+ Name = 'Morty Smith'
+ Groups = 'Domain Users'
+ }
+ }
+ throw 'Invalid credentials'
}
-ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'abc'
+Add-PodeRoute -Method Post -Path '/auth/login' -ScriptBlock {
+ try {
+ $username = $WebEvent.Data.username
+ $password = $WebEvent.Data.password
+ $user = Test-User $username $password # Validate credentials in some real store
+
+ $payload = @{
+ sub = $user.Id
+ name = $user.Name
+ # ... more custom claims ...
+ }
+
+ # Generate JWT recognized by the scheme 'Bearer_JWT_ES512'
+ $jwt = ConvertTo-PodeJwt -Payload $payload -Authentication 'Bearer_JWT_ES512' -Expiration 600
+
+ Write-PodeJsonResponse -StatusCode 200 -Value @{
+ success = $true
+ user = $user
+ jwt = $jwt
+ }
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid credentials' }
+ }
+}
```
-This return the following JWT:
+## 2. Automatic Validation
+
+Once you have a named bearer scheme (e.g., `Bearer_JWT_ES512`), **any** route that includes `-Authentication 'Bearer_JWT_ES512'` is automatically protected. Pode will:
+
+- Extract the JWT from the HTTP `Authorization` header (or another location if specified).
+- Decode and verify the signature based on the scheme’s configuration.
+- Reject the request if invalid; otherwise, set `$WebEvent.Auth.User` with any relevant user/claims data.
-```plain
-eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY
+```powershell
+Add-PodeRoute -Method Get -Path '/secure' -Authentication 'Bearer_JWT_ES512' -ScriptBlock {
+ # If we get here, the token is valid
+ $user = $WebEvent.Auth.User
+ Write-PodeJsonResponse -Value @{ user = $user; message = 'Welcome!' }
+}
```
-## Parse JWT
+No need to manually call `ConvertFrom-PodeJwt`—Pode handles validation behind the scenes.
-Pode has a [`ConvertFrom-PodeJwt`](../../../../Functions/Authentication/ConvertFrom-PodeJwt) that can be used to parse a valid JWT. Only the algorithms at the top of this page are supported for verifying the signature. You can skip signature verification by passing `-IgnoreSignature`. On success, the payload of the JWT is returned.
+## 3. Decoding/Inspecting a JWT
-For example, if the created JWT was supplied:
+Sometimes you want to **inspect** a token or decode it for debugging. That’s where `ConvertFrom-PodeJwt` is handy. For example, you might have a route that **also** includes `-Authentication 'Bearer_JWT_ES512'` (so the user needs a valid token to get in), but within the route you call `ConvertFrom-PodeJwt` to see the raw contents or claims:
```powershell
-ConvertFrom-PodeJwt -Token 'eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY' -Secret 'abc'
+Add-PodeRoute -Method Post -Path '/auth/bearer/jwt/info' -Authentication 'Bearer_JWT_ES512' -ScriptBlock {
+ try {
+ # Although Pode already validated the token, we can decode it ourselves for debugging
+ $decoded = ConvertFrom-PodeJwt -Outputs 'Header,Payload,Signature' -HumanReadable
+ Write-PodeJsonResponse -Value $decoded
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
+}
```
-then the following would be returned:
+This route returns the **header, payload, and signature** in JSON, with timestamps (like `exp`, `nbf`, `iat`) converted to human-readable dates.
+
+## 4. Renewing/Extending a JWT with `Update-PodeJwt`
+
+Use `Update-PodeJwt` to **extend** an existing token’s lifetime. Typically, you create a `/renew` endpoint:
```powershell
-@{
- sub = '123'
- name = 'John Doe'
- exp = 1636657408
+Add-PodeRoute -Method Post -Path '/auth/bearer/jwt/renew' -Authentication 'Bearer_JWT_ES512' -ScriptBlock {
+ try {
+ # Reads the current valid JWT, reissues it with a fresh 'exp' claim
+ $newToken = Update-PodeJwt
+ Write-PodeJsonResponse -StatusCode 200 -Value @{ success = $true; jwt = $newToken }
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
}
```
+
+Pode fetches the token from `$WebEvent`, checks the original scheme (here, `Bearer_JWT_ES512`), and re-signs with updated expiration. The rest of the claims stay the same. The client can then discard the old token and use the newly returned token moving forward.
+
+---
+
+## Full Lifecycle Example
+
+**1.** **Login** (create token)
+**2.** **Make Authenticated Requests** (Pode automatically validates)
+**3.** **Renew** (use `Update-PodeJwt` if needed)
+**4.** **Debug** (optionally decode token with `ConvertFrom-PodeJwt`)
+
+This covers a typical JWT flow in Pode:
+
+- The user logs in at `/auth/login`, gets a JWT.
+- They pass that JWT in subsequent requests, which are auto-validated by `-Authentication 'Bearer_JWT_ES512'`.
+- If the token is about to expire, they can call `/auth/bearer/jwt/renew` to get a fresh one.
+- If you need to debug claims, you can build an endpoint that calls `ConvertFrom-PodeJwt` or look at `$WebEvent.Auth.User`.
+
+For more details, see the [Pode GitHub examples](https://github.com/Badgerati/Pode/tree/develop/examples/Authentication) or the relevant [`ConvertTo-PodeJwt`](https://github.com/Badgerati/Pode/blob/develop/Functions/Authentication/ConvertTo-PodeJwt.ps1) and [`Update-PodeJwt`](https://github.com/Badgerati/Pode/blob/develop/Functions/Authentication/Update-PodeJwt.ps1) source files.
\ No newline at end of file
diff --git a/docs/Tutorials/Basics.md b/docs/Tutorials/Basics.md
index 137aa8b29..f887da2e5 100644
--- a/docs/Tutorials/Basics.md
+++ b/docs/Tutorials/Basics.md
@@ -1,4 +1,5 @@
# Basics
+
!!! Warning
You can initiate only one server per PowerShell instance. To run multiple servers, start additional PowerShell, or pwsh, sessions. Each session can run its own server. This is fundamental to how Pode operates, so consider it when designing your scripts and infrastructure.
@@ -59,6 +60,7 @@ When you call [`Start-PodeServer`](../../Functions/Core/Start-PodeServer) direct
For example, the following is a file that contains the same scriptblock for the server at the top of this page. Following that are the two ways to run the server - the first is via another script, and the second is directly from the CLI:
* File.ps1
+
```powershell
{
# attach to port 8080 for http
@@ -70,12 +72,15 @@ For example, the following is a file that contains the same scriptblock for the
```
* Server.ps1 (start via script)
+
```powershell
Start-PodeServer -FilePath './File.ps1'
```
+
then use `./Server.ps1` on the CLI.
* CLI (start from CLI)
+
```powershell
PS> Start-PodeServer -FilePath './File.ps1'
```
diff --git a/docs/Tutorials/Certificates.md b/docs/Tutorials/Certificates.md
index be3a9e606..2aff12dae 100644
--- a/docs/Tutorials/Certificates.md
+++ b/docs/Tutorials/Certificates.md
@@ -1,121 +1,313 @@
# Certificates
-Pode has the ability to generate and bind self-signed certificates (for dev/testing), as well as the ability to bind existing certificates for HTTPS.
+Pode has the ability to generate and bind self-signed certificates (for dev/testing), as well as the ability to bind existing certificates for HTTPS or JWT.
-There are 8 ways to setup HTTPS on [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint):
+## Setting Up HTTPS in Pode
-1. Supplying just the `-Certificate`, which is the path to files such as a `.cer` or `.pem` file.
-2. Supplying both the `-Certificate` and `-CertificatePassword`, which is the path to a `.pfx` file and its password.
-3. Supplying both the `-Certificate` and `-CertificateKey`, which is the paths to certificate/key PEM file pairs.
-4. Supplying all of `-Certificate`, `-CertificateKey`, and `-CertificatePassword`, which is the paths to certificate/key PEM file pairs and the password for an encrypted key.
-5. Supplying a `-CertificateThumbprint` for a certificate installed at `Cert:\CurrentUser\My` on Windows.
-6. Supplying a `-CertificateName` for a certificate installed at `Cert:\CurrentUser\My` on Windows.
-7. Supplying `-X509Certificate` of type `X509Certificate`.
-8. Supplying the `-SelfSigned` switch, to generate a quick self-signed `X509Certificate`.
+Pode provides multiple ways to configure HTTPS on [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint):
-Note: for 5. and 6. you can change the certificate store used by supplying `-CertificateStoreName` and/or `-CertificateStoreLocation`.
+- **File-based certificates:**
+ - `-Certificate`: Path to a `.cer` or `.pem` file.
+ - `-Certificate` with `-CertificatePassword`: Path to a `.pfx` file and its password.
+ - `-Certificate` with `-CertificateKey`: Paths to a certificate/key PEM file pair.
+ - `-Certificate`, `-CertificateKey`, and `-CertificatePassword`: Paths to an encrypted PEM file pair and its password.
-## Usage
+- **Windows Certificate Store:**
+ - `-CertificateThumbprint`: Uses a certificate installed at `Cert:\CurrentUser\My`.
+ - `-CertificateName`: Uses a certificate installed at `Cert:\CurrentUser\My` by name.
-### File
+- **X.509 Certificates:**
+ - `-X509Certificate`: Provides a certificate object of type `X509Certificate2`.
+ - `-SelfSigned`: Generates a quick self-signed `X509Certificate` for development.
-#### PFX
+- **Custom Certificate Management:**
+ - Pode’s built-in functions allow better control over certificate creation, import, and export.
-To bind a certificate PFX file, you use the `-Certificate` parameter, along with the `-CertificatePassword` parameter for the PFX certificate. The following example supplies the path to some `.pfx` to enable HTTPS support:
+## Generating a Self-Signed Certificate
+
+Pode provides the **`New-PodeSelfSignedCertificate`** function for creating self-signed X.509 certificates for development and testing purposes.
+
+### Features of `New-PodeSelfSignedCertificate`
+
+- ✅ Creates a **self-signed certificate** for HTTPS, JWT, or other use cases.
+- ✅ Supports **RSA** and **ECDSA** keys with configurable key sizes.
+- ✅ Can include **multiple Subject Alternative Names (SANs)** (e.g., `localhost`, IP addresses).
+- ✅ Allows setting **certificate purposes (ServerAuth, ClientAuth, etc.).**
+- ✅ Provides **ephemeral certificates** (in-memory only, not stored on disk).
+- ✅ Supports **exportable certificates** that can be saved for later use.
+
+### Usage Examples
+
+#### 1️⃣ Generate a Self-Signed Certificate for HTTPS
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -Certificate './cert.pfx' -CertificatePassword 'Hunter2'
-}
+$cert = New-PodeSelfSignedCertificate -DnsName "example.com" -CertificatePurpose ServerAuth
```
-#### PEM
+- Creates a **self-signed RSA certificate** for `example.com`.
+- The certificate is valid for HTTPS (`ServerAuth`).
-Pode has support for binding certificate/key PEM file pairs, on PowerShell 7+ this works out-of-the-box. However, for PowerShell 5/6 you are required to have OpenSSL installed.
+#### 2️⃣ Generate a Self-Signed Certificate for Local Development
-To bind a certificate/key PEM file pairs generated via LetsEncrypt or OpenSSL, you supply their paths to the `-Certificate` and `-CertificateKey` parameters.
+```powershell
+$cert = New-PodeSelfSignedCertificate -Loopback
+```
+
+- Automatically includes common loopback addresses:
+ - `127.0.0.1`
+ - `::1`
+ - `localhost`
+ - The machine’s hostname
+
+#### 3️⃣ Generate an ECDSA Certificate
-For example, if you generate the certificate/key using the following:
-```bash
-openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "test.local" -KeyType "ECDSA" -KeyLength 384
```
-Then your endpoint would be created as:
+- Creates a **self-signed ECDSA certificate** with a **384-bit** key.
+
+#### 4️⃣ Generate a Certificate That Exists Only in Memory (Ephemeral)
+
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "temp.local" -Ephemeral
+```
+
+- The private key is **not stored on disk**, and the certificate only exists **in-memory**.
+
+#### 5️⃣ Generate an Exportable Certificate
+
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "secureapp.local" -Exportable
+```
+
+- The certificate is **exportable** and can be saved as a `.pfx` or `.pem` file later.
+
+#### 6️⃣ Bind a Self-Signed Certificate to an HTTPS Endpoint
+
```powershell
Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -Certificate './cert.pem' -CertificateKey './key.pem'
+ $cert = New-PodeSelfSignedCertificate -DnsName "example.com" -CertificatePurpose ServerAuth
+ Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -X509Certificate $cert
}
```
-However, if you generate the certificate/key and encrypt the key with a passphrase:
-```bash
-openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
+- Creates an HTTPS endpoint using a self-signed certificate.
+
+---
+
+## Generating a Certificate Signing Request (CSR)
+
+To generate a Certificate Signing Request (CSR) along with a private key, use the **`New-PodeCertificateRequest`** function:
+
+```powershell
+$csr = New-PodeCertificateRequest -DnsName "example.com" -CommonName "example.com" -KeyType "RSA" -KeyLength 2048
```
-Then the endpoint is created as follows:
+This will create a CSR file and a private key file in the current directory. You can specify additional parameters such as organization details and certificate purposes.
+
+### Using a CSR to Obtain a Certificate
+
+Once you have generated a CSR, you need to submit it to a **Certificate Authority (CA)** (such as Let's Encrypt, DigiCert, or a private CA) to receive a signed certificate. The process typically involves:
+
+1. Uploading or providing the `.csr` file to the CA.
+2. Completing domain validation steps (if required).
+3. Receiving the signed certificate (`.cer`, `.pem`, or `.pfx`) from the CA.
+4. Importing the signed certificate into Pode for use.
+
+Example: Importing the signed certificate after receiving it from the CA:
+
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -Certificate './cert.pem' -CertificateKey './key.pem' -CertificatePassword ''
+$cert = Import-PodeCertificate -Path "C:\Certs\signed-cert.pfx" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+if (-not (Test-PodeCertificate -Certificate $cert -ErrorAction Stop)) {
+ throw 'Certificate not valid'
}
```
-Depending on how you generated the certificate, especially if you used the above openssl, you might have to install the certificate to your local certificate store for it to be trusted. If you're using `Invoke-WebRequest` or `Invoke-RestMethod` on PowerShell 6+ you can supply the `-SkipCertificateCheck` switch.
+Alternatively, you can use:
+
+```powershell
+Test-PodeCertificate -Certificate $cert -ErrorAction Stop | Out-Null
+```
+
+to force an exception if the certificate fails validation.
+**Refer to the `Test-PodeCertificate` documentation for any parameter details.**
+
+---
+
+## Exporting a Certificate
+
+Pode allows exporting certificates in various formats such as PFX and PEM. To export a certificate:
+
+```powershell
+Export-PodeCertificate -Certificate $cert -FilePath "C:\Certs\mycert" -Format "PFX" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+```
+
+or as a PEM file with a separate private key:
-### Thumbprint
+```powershell
+Export-PodeCertificate -Certificate $cert -FilePath "C:\Certs\mycert" -Format "PEM" -IncludePrivateKey
+```
+
+## Checking a Certificate’s Purpose
+
+A certificate's **purpose** is defined by its **Enhanced Key Usage (EKU)** attributes, which specify what the certificate is allowed to be used for. Common EKU values include:
-On Windows only, you can use a certificate that is installed at `Cert:\CurrentUser\My` using its thumbprint:
+- `ServerAuth` – Used for server authentication in HTTPS.
+- `ClientAuth` – Used for client authentication in mutual TLS setups.
+- `CodeSigning` – Used for digitally signing software and scripts.
+- `EmailSecurity` – Used for securing email communication.
+
+Pode can extract the EKU of a certificate to determine its intended purposes:
```powershell
+$purposes = Get-PodeCertificatePurpose -Certificate $cert
+$purposes
+```
+
+### Enforcing Certificate Purpose
+
+When Pode validates a certificate, it ensures that the certificate’s EKU matches the expected usage. If a certificate is used for an endpoint but lacks the required EKU (e.g., using a `CodeSigning` certificate for `ServerAuth`), Pode will reject the certificate and fail to bind it to the endpoint.
+
+For example, if an HTTPS endpoint is created, the certificate **must** include `ServerAuth`:
+
+```powershell
+$cert = New-PodeSelfSignedCertificate -DnsName "example.com" -CertificatePurpose ServerAuth
+
Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -CertificateThumbprint '2A623A8DC46ED42A13B27DD045BFC91FDDAEB957'
+ Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -X509Certificate $cert
}
```
-Note: You can change the certificate store used by supplying `-CertificateStoreName` and/or `-CertificateStoreLocation`.
+If the certificate lacks the correct EKU, Pode will return an error when attempting to bind it.
-### Name
+## Importing an Existing Certificate
-On Windows only, you can use a certificate that is installed at `Cert:\CurrentUser\My` using its subject name:
+To import a certificate from a file or the Windows certificate store:
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8090 -Protocol Https -CertificateName '*.example.com'
+$cert = Import-PodeCertificate -Path "C:\Certs\mycert.pfx" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+```
+
+If you import a certificate without validating it, you should then call **`Test-PodeCertificate`** to verify that the certificate is valid:
+
+```powershell
+if (-not (Test-PodeCertificate -Certificate $cert -ErrorAction Stop)) {
+ throw 'Certificate not valid'
}
```
-Note: You can change the certificate store used by supplying `-CertificateStoreName` and/or `-CertificateStoreLocation`.
+Alternatively, you can force an exception by piping the output:
+
+```powershell
+Test-PodeCertificate -Certificate $cert -ErrorAction Stop | Out-Null
+```
+
+Refer to the **Test-PodeCertificate** section below for more details on all available parameters.
+
+---
+
+## Testing a Certificate’s Validity
+
+Pode provides the **`Test-PodeCertificate`** function to validate an **X.509 certificate** and ensure it meets security and usage requirements.
-### X509
+### Features of `Test-PodeCertificate`
-The following will instead create an X509Certificate, and pass that to the endpoint instead:
+- ✅ Checks if the certificate is **within its validity period** (`NotBefore` and `NotAfter`).
+- ✅ **Builds the certificate chain** to verify its trust.
+- ✅ Supports **online and offline revocation checking** (OCSP/CRL).
+- ✅ Allows **optional enforcement of strong cryptographic algorithms**.
+- ✅ Provides an option to **reject self-signed certificates**.
+- ✅ Optionally checks that the certificate’s Enhanced Key Usage (EKU) matches an **ExpectedPurpose** (with an optional **Strict** mode).
+
+### Usage Examples
+
+#### Basic Certificate Validation
```powershell
-$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new('./certs/example.cer')
-Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -X509Certificate $cert
+Test-PodeCertificate -Certificate $cert
```
-### Self-Signed
+- Checks if the certificate is currently valid.
+- Does **not** check revocation status.
-If you are developing/testing a site on HTTPS then Pode can generate and bind quick self-signed certificates. To do this you can pass the `-SelfSigned` switch:
+#### Validate Certificate with Online Revocation Checking
```powershell
-Start-PodeServer {
- Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned
-}
+Test-PodeCertificate -Certificate $cert -CheckRevocation
+```
+
+- Uses **OCSP/CRL lookup** to check if the certificate is revoked.
+
+#### Validate Certificate with Offline (Cached CRL) Revocation Check
+
+```powershell
+Test-PodeCertificate -Certificate $cert -CheckRevocation -OfflineRevocation
```
-You might get a warning in the browser about the certificate, and this is fine. If you're using `Invoke-WebRequest` or `Invoke-RestMethod` on PowerShell 6+ you can supply the `-SkipCertificateCheck` switch.
+- Uses **only locally cached CRLs**, making it suitable for air-gapped environments.
-## SSL Protocols
+#### Allow Certificates with Weak Algorithms
-The default allowed SSL protocols are SSL3 and TLS1.2 (or just TLS1.2 on MacOS), but you can change these to any of: SSL2, SSL3, TLS, TLS11, TLS12, TLS13. This is specified in your `server.psd1` configuration file:
+```powershell
+Test-PodeCertificate -Certificate $cert -AllowWeakAlgorithms
+```
+
+- Allows the use of certificates with **SHA1, MD5, or RSA-1024**.
+
+#### Reject Self-Signed Certificates
+
+```powershell
+Test-PodeCertificate -Certificate $cert -DenySelfSigned
+```
+
+- Fails validation if the certificate **is self-signed**.
+
+#### Enforce Expected Certificate Purpose
+
+```powershell
+Test-PodeCertificate -Certificate $cert -ExpectedPurpose CodeSigning -Strict
+```
+
+- Validates that the certificate is explicitly authorized for **CodeSigning**.
+- In strict mode, if any unknown EKUs are present, the validation fails.
+
+---
+
+## Using Certificates for JWT Authentication
+
+Pode supports using X.509 certificates for JWT authentication. You can specify a certificate for signing and verifying JWTs by providing `-X509Certificate` when creating a bearer authentication scheme:
```powershell
-@{
- Server = @{
- Ssl= @{
- Protocols = @('TLS', 'TLS11', 'TLS12')
- }
+$cert = Import-PodeCertificate -Path "C:\Certs\jwt-signing-cert.pfx" -CertificatePassword (ConvertTo-SecureString "MyPass" -AsPlainText -Force)
+
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -X509Certificate $cert |
+ Add-PodeAuth -Name 'JWTAuth' -Sessionless -ScriptBlock {
+ param($token)
+
+ # Validate and extract user details
+ return @{ User = $user }
}
}
```
+
+Alternatively, you can use a self-signed certificate for development and testing:
+
+```powershell
+Start-PodeServer {
+ New-PodeAuthBearerScheme `
+ -AsJWT `
+ -SelfSigned |
+ Add-PodeAuth -Name 'JWTAuth' -Sessionless -ScriptBlock {
+ param($token)
+
+ # Validate and extract user details
+ return @{ User = $user }
+ }
+}
+```
+
+Using certificates for JWT authentication provides enhanced security by enabling asymmetric signing (RSA/ECDSA) rather than using a shared secret.
diff --git a/docs/Tutorials/Ssl.md b/docs/Tutorials/Ssl.md
new file mode 100644
index 000000000..1d65ad098
--- /dev/null
+++ b/docs/Tutorials/Ssl.md
@@ -0,0 +1,85 @@
+# SSL Protocols
+
+By default, the server chooses the allowed SSL/TLS protocols based on the operating system’s native support.
+
+For example, on Windows 11 and Windows Server 2022 only TLS 1.2 and TLS 1.3 are enabled, while older systems (such as Windows Vista/Server 2008) allow SSL 2.0 and SSL 3.0. This behavior follows the table below:
+
+| Operating System | SSL 2.0 | SSL 3.0 | TLS 1.0 | TLS 1.1 | TLS 1.2 | TLS 1.3 |
+|------------------------------------|---------------------|---------------------|---------------------|---------------------|--------------------|--------------------|
+| Windows Vista / Server 2008 | Enabled | Enabled | Not Supported | Not Supported | Not Supported | Not Supported |
+| Windows 7 / Server 2008 R2 | Enabled | Enabled | Disabled | Disabled | Disabled | Not Supported |
+| Windows 8 / Server 2012 | Disabled by Default | Enabled by Default | Enabled by Default | Enabled by Default | Enabled by Default | Not Supported |
+| Windows 10 (Build 20170 and later) | No | Disabled by Default | Enabled by Default | Enabled by Default | Enabled by Default | Enabled by Default |
+| Windows 11 / Server 2022 | No | Disabled by Default | Disabled by Default | Disabled by Default | Enabled by Default | Enabled by Default |
+| macOS 10.8 - 10.10 | No | Yes | Yes | Yes | Yes | No |
+| macOS 10.11 | No | No | Yes | Yes | Yes | No |
+| macOS 10.13 and later | No | No | Yes | Yes | Yes | Yes |
+| Linux (OpenSSL 1.0.1 - 1.0.1f) | No | Yes | Yes | Yes | Yes | No |
+| Linux (OpenSSL 1.0.1g and later) | No | No | Yes | Yes | Yes | No |
+| Linux (OpenSSL 1.1.1 and later) | No | No | Yes | Yes | Yes | Yes |
+
+**Notes:**
+
+- **Windows Operating Systems:**
+ - TLS 1.3 is supported starting from Windows 10 Build 20170 and Windows Server 2022.
+ - Earlier versions (like Windows 7 and Windows Server 2008 R2) support up to TLS 1.2, but may require manual configuration to enable it.
+
+- **macOS:**
+ - TLS 1.3 support begins with macOS 10.13.
+
+- **Linux:**
+ - The supported SSL/TLS protocols on Linux systems depend on the version of OpenSSL installed:
+ - OpenSSL versions 1.0.1 to 1.0.1f support up to TLS 1.2, with SSL 3.0 enabled by default.
+ - OpenSSL version 1.0.1g and later disable SSL 3.0 by default.
+ - OpenSSL version 1.1.1 and later add support for TLS 1.3.
+
+## Override the Default Values
+
+If you wish to override the defaults, you can customize the allowed protocols in your `server.psd1` configuration file. For example, if you want to allow only TLS protocols (excluding the deprecated SSL versions), you can configure it as follows:
+
+```powershell
+@{
+ Server = @{
+ Ssl = @{
+ Protocols = @('Tls', 'Tls11', 'Tls12')
+ }
+ }
+}
+```
+
+Or, to include TLS 1.3 where supported:
+
+```powershell
+@{
+ Server = @{
+ Ssl = @{
+ Protocols = @('Tls', 'Tls11', 'Tls12', 'Tls13')
+ }
+ }
+}
+```
+
+This configuration allows you to explicitly set the protocols from the following list of supported values: `'Ssl2'`, `'Ssl3'`, `'Tls'`, `'Tls11'`, `'Tls12'`, and `'Tls13'`.
+
+> **Important:** Overriding these default values in your configuration file does **not** automatically enable the corresponding protocols at the operating system level. The OS may block a protocol unless its native settings are also changed. In other words, even if you add `'Ssl3'` to your allowed protocols, Windows 11 will still reject SSLv3 connections unless you modify the OS settings.
+
+### Example: Enabling SSLv3 on Windows 11
+
+By default, Windows 11 disables SSLv3 in its Schannel settings. To enable SSLv3, you need to change the registry settings. **Proceed with caution** as enabling SSLv3 can expose your system to known vulnerabilities (such as the POODLE attack).
+
+You can enable SSLv3 on Windows 11 using PowerShell as follows:
+
+```powershell
+# Create the registry keys for SSL 3.0 if they don't already exist
+New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0" -Force | Out-Null
+New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client" -Force | Out-Null
+New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server" -Force | Out-Null
+
+# Enable SSLv3 for both client and server by setting the Enabled DWORD to 1
+Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client" -Name "Enabled" -Value 1 -Type DWord
+Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server" -Name "Enabled" -Value 1 -Type DWord
+
+Write-Output "SSLv3 has been enabled. A system restart may be required for the changes to take effect."
+```
+
+After making these changes, your Windows 11 system will accept SSLv3 connections. Remember that this registry modification is an OS-level change, and overriding the configuration in `server.psd1` alone will not suffice.
diff --git a/docs/index.md b/docs/index.md
index 25d0d9dd9..22b75db5a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -18,36 +18,37 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We
## 🚀 Features
-* Cross-platform using PowerShell Core (with support for PS5)
-* Docker support, including images for ARM/Raspberry Pi
-* Azure Functions, AWS Lambda, and IIS support
-* OpenAPI specification version 3.0.x and 3.1.0
-* OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
-* Listen on a single or multiple IP(v4/v6) addresses/hostnames
-* Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
-* Host REST APIs, Web Pages, and Static Content (with caching)
-* Support for custom error pages
-* Request and Response compression using GZip/Deflate
-* Multi-thread support for incoming requests
-* Inbuilt template engine, with support for third-parties
-* Async timers for short-running repeatable processes
-* Async scheduled tasks using cron expressions for short/long-running processes
-* Supports logging to CLI, Files, and custom logic for other services like LogStash
-* Cross-state variable access across multiple runspaces
-* Restart the server via file monitoring, or defined periods/times
-* Ability to allow/deny requests from certain IP addresses and subnets
-* Basic rate limiting for IP addresses and subnets
-* Middleware and Sessions on web servers, with Flash message and CSRF support
-* Authentication on requests, such as Basic, Windows and Azure AD
-* Authorisation support on requests, using Roles, Groups, Scopes, etc.
-* Support for dynamically building Routes from Functions and Modules
-* Generate/bind self-signed certificates
-* Secret management support to load secrets from vaults
-* Support for File Watchers
-* In-memory caching, with optional support for external providers (such as Redis)
-* (Windows) Open the hosted server as a desktop application
-* FileBrowsing support
-* Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese, and Chinese
+* ✅ Cross-platform using PowerShell Core (with support for PS5)
+* ✅ Docker support, including images for ARM/Raspberry Pi
+* ✅ Azure Functions, AWS Lambda, and IIS support
+* ✅ OpenAPI specification version 3.0.x and 3.1.0
+* ✅ OpenAPI documentation with Swagger, Redoc, RapidDoc, StopLight, OpenAPI-Explorer and RapiPdf
+* ✅ Listen on a single or multiple IP(v4/v6) addresses/hostnames
+* ✅ Cross-platform support for HTTP(S), WS(S), SSE, SMTP(S), and TCP(S)
+* ✅ Host REST APIs, Web Pages, and Static Content (with caching)
+* ✅ Support for custom error pages
+* ✅ Request and Response compression using GZip/Deflate
+* ✅ Multi-thread support for incoming requests
+* ✅ Inbuilt template engine, with support for third-parties
+* ✅ Async timers for short-running repeatable processes
+* ✅ Async scheduled tasks using cron expressions for short/long-running processes
+* ✅ Supports logging to CLI, Files, and custom logic for other services like LogStash
+* ✅ Cross-state variable access across multiple runspaces
+* ✅ Restart the server via file monitoring, or defined periods/times
+* ✅ Ability to allow/deny requests from certain IP addresses and subnets
+* ✅ Basic rate limiting for IP addresses and subnets
+* ✅ Middleware and Sessions on web servers, with Flash message and CSRF support
+* ✅ Authentication on requests, such as Basic, Windows and Azure AD
+* ✅ Authorisation support on requests, using Roles, Groups, Scopes, etc.
+* ✅ Enhanced authentication support, including Basic, Bearer (with JWT), Certificate, Digest, Form, OAuth2, and ApiKey (with JWT).
+* ✅ Support for dynamically building Routes from Functions and Modules
+* ✅ Generate/bind self-signed certificates
+* ✅ Secret management support to load secrets from vaults
+* ✅ Support for File Watchers
+* ✅ In-memory caching, with optional support for external providers (such as Redis)
+* ✅ (Windows) Open the hosted server as a desktop application
+* ✅ FileBrowsing support
+* ✅ Localization (i18n) in Arabic, German, Spanish, France, Italian, Japanese, Korean, Polish, Portuguese,Dutch and Chinese
## 🏢 Companies using Pode
diff --git a/examples/Authentication/Modules/Invoke-Digest.psm1 b/examples/Authentication/Modules/Invoke-Digest.psm1
new file mode 100644
index 000000000..07672527f
--- /dev/null
+++ b/examples/Authentication/Modules/Invoke-Digest.psm1
@@ -0,0 +1,662 @@
+function ConvertTo-Hash {
+ param (
+ [string]$Value,
+ [string]$Algorithm
+ )
+
+ $crypto = switch ($Algorithm) {
+ 'MD5' { [System.Security.Cryptography.MD5]::Create() }
+ 'SHA-1' { [System.Security.Cryptography.SHA1]::Create() }
+ 'SHA-256' { [System.Security.Cryptography.SHA256]::Create() }
+ 'SHA-384' { [System.Security.Cryptography.SHA384]::Create() }
+ 'SHA-512' { [System.Security.Cryptography.SHA512]::Create() }
+ 'SHA-512/256' {
+ # Compute SHA-512 and take first 32 bytes (256 bits)
+ $sha512 = [System.Security.Cryptography.SHA512]::Create()
+ $fullHash = $sha512.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))
+ return [System.BitConverter]::ToString($fullHash[0..31]).Replace('-', '').ToLowerInvariant()
+ }
+ }
+
+ return [System.BitConverter]::ToString($crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value))).Replace('-', '').ToLowerInvariant()
+}
+
+function ChallengeDigest {
+ param(
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
+ [string]$Method,
+
+ [Parameter(Mandatory = $true)]
+ [string]$Uri
+ )
+ # Create an HTTP client
+ $handler = [System.Net.Http.HttpClientHandler]::new()
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
+
+ # Step 1: Send an initial request to get the challenge
+ $initialRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::$Method, $Uri)
+ $initialResponse = $httpClient.SendAsync($initialRequest).Result
+ if ($null -eq $initialResponse) {
+ Throw "Server $Uri is not responding"
+ }
+
+ # Extract WWW-Authenticate headers safely
+ $wwwAuthHeaders = $initialResponse.Headers.GetValues('WWW-Authenticate')
+ # Filter to get only the Digest authentication scheme
+ $wwwAuthHeader = $wwwAuthHeaders | Where-Object { $_ -match '^Digest' }
+
+ Write-Verbose 'Extracted WWW-Authenticate headers:'
+ $wwwAuthHeaders | ForEach-Object { Write-Verbose " - $_" }
+
+ if (-not $wwwAuthHeader) {
+ Throw 'Digest authentication not supported by server!'
+ }
+
+ # Extract Digest Authentication challenge values
+ $challenge = @{}
+
+ if ($wwwAuthHeader -match '^Digest ') {
+ $headerContent = $wwwAuthHeader -replace '^Digest ', ''
+ Write-Verbose "RAW HEADER: $headerContent"
+
+ # 1) CAPTURE supported algorithms
+ if ($headerContent -match 'algorithm=((?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5)(?:,\s*(?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5))*)') {
+ $algorithms = ($matches[1] -split '\s*,\s*')
+ Write-Verbose "Supported Algorithms: $algorithms"
+ $challenge['algorithm'] = $algorithms
+ }
+
+ # 2) REMOVE algorithm parameter
+ $headerContent = $headerContent -replace 'algorithm=(?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5)(?:,\s*(?:SHA-1|SHA-256|SHA-384|SHA-512(?:/256)?|MD5))*\s*,?', ''
+ # 3) CLEAN UP extra commas/whitespace
+ $headerContent = $headerContent -replace ',\s*,', ','
+ $headerContent = $headerContent -replace '^\s*,', ''
+
+ # Split remaining parameters safely
+ $headerContent -split ', ' | ForEach-Object {
+ $key, $value = $_ -split '=', 2
+ if ($key -and $value) {
+ $challenge[$key.Trim()] = $value.Trim('"')
+ }
+ }
+ }
+
+ Write-Verbose 'Extracted Digest Authentication Challenge:'
+ $challenge.GetEnumerator() | ForEach-Object { Write-Verbose "$($_.Key) = $($_.Value)" }
+
+ $realm = $challenge['realm']
+ $nonce = $challenge['nonce']
+ $qop = $challenge['qop']
+ $algorithm = $challenge['algorithm']
+
+ if (('Post', 'Put', 'Patch') -contains $Method) {
+ if ($qop -eq 'auth-int' -or $qop -eq 'auth,auth-int') {
+ $qop = 'auth-int'
+ }
+ else {
+ $qop = 'auth'
+ }
+ }
+ else {
+ if ($qop -eq 'auth' -or $qop -eq 'auth,auth-int') {
+ $qop = 'auth'
+ }
+ else {
+ throw "$Method doesn't support QualityOfProtection 'auth-int'"
+ }
+ }
+
+ Write-Verbose "Selected QOP: $qop"
+
+ $preferredAlgorithms = @('SHA-512/256', 'SHA-512', 'SHA-384', 'SHA-256', 'SHA-1', 'MD5')
+ if ($algorithm -isnot [System.Array]) {
+ $algorithm = @($algorithm)
+ }
+ $algorithm = ($preferredAlgorithms | Where-Object { $algorithm -contains $_ } | Select-Object -First 1)
+ if (-not $algorithm) {
+ Throw "No supported algorithms found! Server supports: $algorithm"
+ }
+ return [PSCustomObject]@{
+ realm = $realm
+ nonce = $nonce
+ qop = $qop
+ algorithm = $algorithm
+ wwwAuthHeader = $wwwAuthHeader
+ uri = $Uri
+ httpClient = $httpClient
+ method = $Method
+ }
+}
+
+<#
+.SYNOPSIS
+ Sends an HTTP request using Digest authentication and returns a web response.
+
+.DESCRIPTION
+ The Invoke-WebRequestDigest function performs an HTTP request with Digest authentication,
+ handling HTTP headers, authentication challenges, retries, and timeouts.
+ It returns a BasicHtmlWebResponseObject similar to Invoke-WebRequest.
+
+.PARAMETER Uri
+ The target URI for the request.
+
+.PARAMETER Method
+ The HTTP method to use for the request. Default is 'GET'.
+
+.PARAMETER Body
+ The request body, required for methods like POST, PUT, and PATCH.
+
+.PARAMETER Credential
+ The PSCredential object containing the username and password for Digest authentication.
+
+.PARAMETER Headers
+ A hashtable of additional headers to include in the request.
+
+.PARAMETER ContentType
+ The Content-Type of the request body. Default is 'application/json'.
+
+.PARAMETER OperationTimeoutSeconds
+ The maximum time in seconds before the request times out. Default is 100.
+
+.PARAMETER ConnectionTimeoutSeconds
+ The timeout in seconds for establishing a connection. Default is 100.
+
+.PARAMETER DisableKeepAlive
+ If specified, disables persistent connections by adding the 'Connection: close' header.
+
+.PARAMETER HttpVersion
+ The HTTP version to use, such as '1.1' or '2.0'. Default is '1.1'.
+
+.PARAMETER MaximumRetryCount
+ The number of times to retry the request in case of failure. Default is 1.
+
+.PARAMETER RetryIntervalSec
+ The interval in seconds between retry attempts. Default is 1.
+
+.PARAMETER OutFile
+ If specified, writes the response body to the specified file instead of returning content.
+
+.PARAMETER PassThru
+ If specified, returns the response object even if OutFile is used.
+
+.PARAMETER SkipCertificateCheck
+ If specified, disables SSL certificate validation (useful for self-signed certificates).
+
+.PARAMETER SslProtocol
+ Specifies the allowed SSL/TLS protocol(s) to use (e.g., 'Tls12').
+
+.PARAMETER TransferEncoding
+ The value for the 'Transfer-Encoding' header.
+
+.PARAMETER UserAgent
+ The User-Agent string to use in the request.
+
+.OUTPUTS
+ - Returns a [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject].
+ - If OutFile is specified, writes response data to the specified file.
+
+.EXAMPLE
+ $cred = Get-Credential
+ $response = Invoke-WebRequestDigest -Uri 'https://example.com/data' -Method 'GET' -Credential $cred
+ Write-Output $response.Content
+
+.EXAMPLE
+ $body = @{ "name" = "John Doe"; "email" = "john@example.com" }
+ $cred = Get-Credential
+ $response = Invoke-WebRequestDigest -Uri 'https://example.com/users' -Method 'POST' -Credential $cred -Body $body -ContentType 'application/json'
+ Write-Output $response.Content
+
+.EXAMPLE
+ # Download file
+ Invoke-WebRequestDigest -Uri 'https://example.com/file.zip' -Method 'GET' -Credential $cred -OutFile 'C:\Downloads\file.zip'
+
+.NOTES
+ - This function provides full control over HTTP requests with Digest authentication.
+ - Supports custom headers, connection options, timeouts, and retries.
+ - Unlike Invoke-RestMethodDigest, this function does not automatically parse JSON/XML.
+#>
+function Invoke-WebRequestDigest {
+ [CmdletBinding(DefaultParameterSetName = 'Uri')]
+ [OutputType([Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject])]
+ param(
+ # URI of the request (required)
+ [Parameter(Mandatory = $true, Position = 0)]
+ [Uri]$Uri,
+
+ # HTTP method (default GET)
+ [Parameter(Position = 1)]
+ [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'PATCH', 'MERGE', 'CONNECT')]
+ [string]$Method = 'GET',
+
+ # Request body (for POST/PUT/PATCH, etc.)
+ [Parameter()]
+ $Body,
+
+ # Credential for Digest authentication (required)
+ [Parameter(Mandatory = $true)]
+ [System.Management.Automation.PSCredential]$Credential,
+
+ # Additional headers (as a hashtable)
+ [Parameter()]
+ [hashtable]$Headers,
+
+ # Content type for the request body (default application/json)
+ [Parameter()]
+ [string]$ContentType = 'application/json',
+
+ # Timeout (for the overall operation) in seconds
+ [Parameter()]
+ [int]$OperationTimeoutSeconds = 100,
+
+ # Connection timeout in seconds
+ [Parameter()]
+ [int]$ConnectionTimeoutSeconds = 100,
+
+ # Disable persistent connections (KeepAlive)
+ [Parameter()]
+ [switch]$DisableKeepAlive,
+
+ # Specify the HTTP version (e.g. '1.1' or '2.0')
+ [Parameter()]
+ [string]$HttpVersion = '1.1',
+
+ # Maximum number of retries (if request fails)
+ [Parameter()]
+ [int]$MaximumRetryCount = 1,
+
+ # Interval between retries (seconds)
+ [Parameter()]
+ [int]$RetryIntervalSec = 1,
+
+ # If provided, write response body to this file
+ [Parameter()]
+ [string]$OutFile,
+
+ # If specified, output the response object even if OutFile is used
+ [Parameter()]
+ [switch]$PassThru,
+
+ # Skip certificate validation (useful for self-signed certs)
+ [Parameter()]
+ [switch]$SkipCertificateCheck,
+
+ # Specify allowed SSL/TLS protocol(s) (e.g. 'Tls12')
+ [Parameter()]
+ [string]$SslProtocol,
+
+ # Transfer-Encoding header value to set on the request
+ [Parameter()]
+ [string]$TransferEncoding,
+
+ # User-Agent string to use on the request
+ [Parameter()]
+ [string]$UserAgent
+ )
+
+ # Validate that we have a credential
+ if (-not $Credential) {
+ Throw 'A credential is required for Digest authentication.'
+ }
+
+ # Use HttpClientHandler
+ $handler = [System.Net.Http.HttpClientHandler]::new()
+ if ($SkipCertificateCheck) {
+ $handler.ServerCertificateCustomValidationCallback = { return $true }
+ }
+ if ($SslProtocol) {
+ $handler.SslProtocols = [System.Enum]::Parse(
+ [System.Security.Authentication.SslProtocols], $SslProtocol)
+ }
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
+
+ $httpClient.Timeout = [TimeSpan]::FromSeconds($ConnectionTimeoutSeconds)
+
+ # If DisableKeepAlive is specified, add a header to close the connection.
+ if ($DisableKeepAlive) {
+ if (-not $Headers) { $Headers = @{} }
+ $Headers['Connection'] = 'close'
+ }
+
+ # Use the challenge function to get the digest details.
+ try {
+ $challenge = ChallengeDigest -Uri $Uri -Method $Method
+ }
+ catch {
+ Throw "Error retrieving Digest authentication challenge: $_"
+ }
+
+ try {
+ # If a body is provided and content type is JSON, convert it if necessary.
+ if ($Body -and ($ContentType -match 'application/json')) {
+ if ($Body -isnot [string]) {
+ $Body = $Body | ConvertTo-Json -Compress
+ }
+ }
+
+ # Build the digest response parameters.
+ $nc = '00000001'
+ $cnonce = (New-Guid).Guid.Substring(0, 8)
+ $Method = $challenge.Method.ToUpper()
+ Write-Verbose "Using method: $Method"
+ $uriPath = ([System.Uri]$challenge.uri).AbsolutePath
+
+ # Compute HA1
+ $HA1 = ConvertTo-Hash -Value "$($Credential.UserName):$($challenge.realm):$($Credential.GetNetworkCredential().Password)" -Algorithm $challenge.algorithm
+
+ if ($challenge.qop -eq 'auth-int') {
+ if (('Post', 'Put', 'Patch') -notcontains $Method) {
+ Throw "'auth-int' doesn't support $Method"
+ }
+ $requestBody = $Body | ConvertTo-Json
+ $entityBodyHash = ConvertTo-Hash -Value $requestBody -Algorithm $challenge.algorithm
+ $HA2 = ConvertTo-Hash -Value "$($Method):$($uriPath):$($entityBodyHash)" -Algorithm $challenge.algorithm
+ }
+ else {
+ $HA2 = ConvertTo-Hash -Value "$($Method):$($uriPath)" -Algorithm $challenge.algorithm
+ }
+
+ $responseHash = ConvertTo-Hash -Value "$($HA1):$($challenge.nonce):$($nc):$($cnonce):$($challenge.qop):$HA2" -Algorithm $challenge.algorithm
+
+ # Build the Authorization header using StringBuilder.
+ $sb = [System.Text.StringBuilder]::new()
+ [void]$sb.Append('Digest username="').Append($Credential.UserName).Append('"')
+ [void]$sb.Append(', realm="').Append($challenge.realm).Append('"')
+ [void]$sb.Append(', nonce="').Append($challenge.nonce).Append('"')
+ [void]$sb.Append(', uri="').Append($uriPath).Append('"')
+ [void]$sb.Append(', algorithm=').Append($challenge.algorithm)
+ [void]$sb.Append(', response="').Append($responseHash).Append('"')
+ [void]$sb.Append(', qop="').Append($challenge.qop).Append('"')
+ [void]$sb.Append(', nc=').Append($nc)
+ [void]$sb.Append(', cnonce="').Append($cnonce).Append('"')
+
+ # Create the HttpRequestMessage.
+ $authRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::$Method, $challenge.uri)
+ $authRequest.Headers.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Digest', $sb.ToString())
+
+ # Set the HTTP version if provided.
+ if ($HttpVersion) {
+ $authRequest.Version = [System.Version]$HttpVersion
+ }
+
+ # Add additional headers (if any) to the request.
+ if ($Headers) {
+ foreach ($key in $Headers.Keys) {
+ $authRequest.Headers.TryAddWithoutValidation($key, $Headers[$key]) | Out-Null
+ }
+ }
+
+ # Set Transfer-Encoding if provided.
+ if ($TransferEncoding) {
+ $authRequest.Headers.TryAddWithoutValidation('Transfer-Encoding', $TransferEncoding) | Out-Null
+ }
+
+ # Set User-Agent if provided.
+ if ($UserAgent) {
+ $authRequest.Headers.UserAgent.Clear()
+ $authRequest.Headers.UserAgent.ParseAdd($UserAgent)
+ }
+
+ if ($challenge.qop -eq 'auth-int') {
+ $authRequest.Content = [System.Net.Http.StringContent]::new($requestBody, [System.Text.Encoding]::UTF8, $ContentType)
+ }
+
+ # Implement a simple retry loop.
+ $retryCount = 0
+ do {
+ try {
+ $rawResponse = $challenge.httpClient.SendAsync($authRequest).Result
+ break
+ }
+ catch {
+ if (++$retryCount -ge $MaximumRetryCount) {
+ Throw "Error sending the authenticated request after $MaximumRetryCount attempts: $_"
+ }
+ else {
+ Write-Verbose "Retrying in $RetryIntervalSec seconds..."
+ Start-Sleep -Seconds $RetryIntervalSec
+ }
+ }
+ } while ($true)
+
+ # Optionally write response to file.
+ if ($OutFile) {
+ $mediaType = $rawResponse.Content.Headers.ContentType.MediaType
+ if ($mediaType -match '^(text|application/json|application/xml)') {
+ $contentString = $rawResponse.Content.ReadAsStringAsync().Result
+ Set-Content -Path $OutFile -Value $contentString -Encoding UTF8
+ }
+ else {
+ $rawResponse.Content.ReadAsByteArrayAsync().Result | Set-Content -Path $OutFile -Encoding Byte
+ }
+ if (-not $PassThru) { return }
+ }
+
+ # Wrap the response in a BasicHtmlWebResponseObject using the OperationTimeoutSeconds value.
+ $contentStream = $rawResponse.Content.ReadAsStream()
+ $timeout = [TimeSpan]::FromSeconds($OperationTimeoutSeconds)
+ $cancellationToken = [System.Threading.CancellationToken]::None
+ return [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]::new($rawResponse, $contentStream, $timeout, $cancellationToken)
+ }
+ catch {
+ Throw "Error sending Digest authenticated request: $_"
+ }
+}
+
+
+<#
+.SYNOPSIS
+ Sends an HTTP or REST request using Digest authentication and returns parsed data.
+
+.DESCRIPTION
+ The Invoke-RestMethodDigest function performs an HTTP request with Digest authentication,
+ leveraging Invoke-WebRequestDigest under the hood. It automatically parses the response
+ content into an object, supporting JSON and XML formats.
+
+.PARAMETER Uri
+ The target URI for the request.
+
+.PARAMETER Method
+ The HTTP method to use for the request. Default is 'GET'.
+
+.PARAMETER Body
+ The request body, required for methods like POST, PUT, and PATCH.
+
+.PARAMETER Credential
+ The PSCredential object containing the username and password for Digest authentication.
+
+.PARAMETER Headers
+ A hashtable of additional headers to include in the request.
+
+.PARAMETER ContentType
+ The Content-Type of the request body. Default is 'application/json'.
+
+.PARAMETER OperationTimeoutSeconds
+ The maximum time in seconds before the request times out. Default is 100.
+
+.PARAMETER ConnectionTimeoutSeconds
+ The timeout in seconds for establishing a connection. Default is 100.
+
+.PARAMETER DisableKeepAlive
+ If specified, disables persistent connections by adding the 'Connection: close' header.
+
+.PARAMETER HttpVersion
+ The HTTP version to use, such as '1.1' or '2.0'. Default is '1.1'.
+
+.PARAMETER MaximumRetryCount
+ The number of times to retry the request in case of failure. Default is 1.
+
+.PARAMETER RetryIntervalSec
+ The interval in seconds between retry attempts. Default is 1.
+
+.PARAMETER OutFile
+ If specified, writes the response body to the specified file instead of returning content.
+
+.PARAMETER PassThru
+ If specified, returns the response object even if OutFile is used.
+
+.PARAMETER SkipCertificateCheck
+ If specified, disables SSL certificate validation (useful for self-signed certificates).
+
+.PARAMETER SslProtocol
+ Specifies the allowed SSL/TLS protocol(s) to use (e.g., 'Tls12').
+
+.PARAMETER TransferEncoding
+ The value for the 'Transfer-Encoding' header.
+
+.PARAMETER UserAgent
+ The User-Agent string to use in the request.
+
+.OUTPUTS
+ - JSON responses are converted to PowerShell objects.
+ - XML responses are parsed into XML objects.
+ - Plain text or other data is returned as-is.
+
+.EXAMPLE
+ $cred = Get-Credential
+ $response = Invoke-RestMethodDigest -Uri 'https://example.com/api/data' -Method 'GET' -Credential $cred
+ Write-Output $response
+
+.EXAMPLE
+ $body = @{ "name" = "John Doe"; "email" = "john@example.com" }
+ $cred = Get-Credential
+ $response = Invoke-RestMethodDigest -Uri 'https://example.com/api/users' -Method 'POST' -Credential $cred -Body $body -ContentType 'application/json'
+ Write-Output $response
+
+.NOTES
+ - This function is a wrapper around Invoke-WebRequestDigest and provides an easier way
+ to work with REST APIs by automatically parsing the response content.
+ - Use Invoke-WebRequestDigest if you need full access to response headers and raw content.
+#>
+function Invoke-RestMethodDigest {
+ [CmdletBinding(DefaultParameterSetName = 'Uri')]
+ [OutputType([xml])]
+ [OutputType([psobject])]
+ param(
+ # URI of the request (required)
+ [Parameter(Mandatory = $true, Position = 0)]
+ [Uri]$Uri,
+
+ # HTTP method (default GET)
+ [Parameter(Position = 1)]
+ [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'PATCH', 'MERGE', 'CONNECT')]
+ [string]$Method = 'GET',
+
+ # Request body (for POST/PUT/PATCH, etc.)
+ [Parameter()]
+ $Body,
+
+ # Credential for Digest authentication (required)
+ [Parameter(Mandatory = $true)]
+ [System.Management.Automation.PSCredential]$Credential,
+
+ # Additional headers (as a hashtable)
+ [Parameter()]
+ [hashtable]$Headers,
+
+ # Content type for the request body (default application/json)
+ [Parameter()]
+ [string]$ContentType = 'application/json',
+
+ # Timeout (for the overall operation) in seconds
+ [Parameter()]
+ [int]$OperationTimeoutSeconds = 100,
+
+ # Connection timeout in seconds
+ [Parameter()]
+ [int]$ConnectionTimeoutSeconds = 100,
+
+ # Disable persistent connections (KeepAlive)
+ [Parameter()]
+ [switch]$DisableKeepAlive,
+
+ # Specify the HTTP version (e.g. '1.1' or '2.0')
+ [Parameter()]
+ [string]$HttpVersion = '1.1',
+
+ # Maximum number of retries (if request fails)
+ [Parameter()]
+ [int]$MaximumRetryCount = 1,
+
+ # Interval between retries (seconds)
+ [Parameter()]
+ [int]$RetryIntervalSec = 1,
+
+ # If provided, write response body to this file
+ [Parameter()]
+ [string]$OutFile,
+
+ # If specified, output the response object even if OutFile is used
+ [Parameter()]
+ [switch]$PassThru,
+
+ # Skip certificate validation (useful for self-signed certs)
+ [Parameter()]
+ [switch]$SkipCertificateCheck,
+
+ # Specify allowed SSL/TLS protocol(s) (e.g. 'Tls12')
+ [Parameter()]
+ [string]$SslProtocol,
+
+
+ # Transfer-Encoding header value
+ [Parameter()]
+ [string]$TransferEncoding,
+
+ # User-Agent string to use on the request
+ [Parameter()]
+ [string]$UserAgent
+ )
+
+ # Build a parameter hashtable for Invoke-WebRequestDigest
+ $params = @{
+ Uri = $Uri
+ Method = $Method
+ Body = $Body
+ Credential = $Credential
+ Headers = $Headers
+ ContentType = $ContentType
+ OperationTimeoutSeconds = $OperationTimeoutSeconds
+ ConnectionTimeoutSeconds = $ConnectionTimeoutSeconds
+ DisableKeepAlive = $DisableKeepAlive
+ HttpVersion = $HttpVersion
+ MaximumRetryCount = $MaximumRetryCount
+ RetryIntervalSec = $RetryIntervalSec
+ OutFile = $OutFile
+ PassThru = $PassThru
+ SkipCertificateCheck = $SkipCertificateCheck
+ SslProtocol = $SslProtocol
+ TransferEncoding = $TransferEncoding
+ UserAgent = $UserAgent
+ }
+
+ # Call the digest-enabled web request function
+ $webResponse = Invoke-WebRequestDigest @params
+
+ if ($null -eq $webResponse) {
+ return $null
+ }
+
+ # Parse the response content based on its media type
+ $content = $webResponse.Content
+ if ($content) {
+ # Get Content-Type header if available
+ $mediaType = $webResponse.Headers.'Content-Type'
+ if ($mediaType -match 'application/json') {
+ return $content | ConvertFrom-Json
+ }
+ elseif ($mediaType -match 'application/xml' -or $mediaType -match 'text/xml') {
+ return [xml]$content
+ }
+ else {
+ # For non-parsed content (plain text or other formats)
+ return $content
+ }
+ }
+ else {
+ return $null
+ }
+}
+
+Export-ModuleMember -Function Invoke-WebRequestDigest
+Export-ModuleMember -Function Invoke-RestMethodDigest
diff --git a/examples/WebAuth-ApikeyJWT.ps1 b/examples/Authentication/Web-AuthApiKey.ps1
similarity index 94%
rename from examples/WebAuth-ApikeyJWT.ps1
rename to examples/Authentication/Web-AuthApiKey.ps1
index dcf242d54..e7b88e135 100644
--- a/examples/WebAuth-ApikeyJWT.ps1
+++ b/examples/Authentication/Web-AuthApiKey.ps1
@@ -29,7 +29,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Method Get
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/WebAuth-ApikeyJWT.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/WebAuth-ApikeyJWT.ps1
.NOTES
Author: Pode Team
@@ -44,7 +44,7 @@ param(
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasic.ps1 b/examples/Authentication/Web-AuthBasic.ps1
similarity index 93%
rename from examples/Web-AuthBasic.ps1
rename to examples/Authentication/Web-AuthBasic.ps1
index e0a886a02..ef795df87 100644
--- a/examples/Web-AuthBasic.ps1
+++ b/examples/Authentication/Web-AuthBasic.ps1
@@ -23,7 +23,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasic.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasic.ps1
.NOTES
Author: Pode Team
@@ -31,7 +31,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -75,7 +75,7 @@ Start-PodeServer -Threads 2 {
return @{ Message = 'Invalid details supplied' }
}
-
+
# POST request to get current user (since there's no session, authentication will always happen)
Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ScriptBlock {
Write-PodeJsonResponse -Value @{
diff --git a/examples/Web-AuthBasicAccess.ps1 b/examples/Authentication/Web-AuthBasicAccess.ps1
similarity index 96%
rename from examples/Web-AuthBasicAccess.ps1
rename to examples/Authentication/Web-AuthBasicAccess.ps1
index 1bd7d36c5..0c07afe5d 100644
--- a/examples/Web-AuthBasicAccess.ps1
+++ b/examples/Authentication/Web-AuthBasicAccess.ps1
@@ -27,7 +27,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users-all -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Dot-SourceScript.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Dot-SourceScript.ps1
.NOTES
Author: Pode Team
@@ -35,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicAdhoc.ps1 b/examples/Authentication/Web-AuthBasicAdhoc.ps1
similarity index 94%
rename from examples/Web-AuthBasicAdhoc.ps1
rename to examples/Authentication/Web-AuthBasicAdhoc.ps1
index c4eea0b73..b7b905ec5 100644
--- a/examples/Web-AuthBasicAdhoc.ps1
+++ b/examples/Authentication/Web-AuthBasicAdhoc.ps1
@@ -27,7 +27,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicAdhoc.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicAdhoc.ps1
.NOTES
Author: Pode Team
@@ -35,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicAnon.ps1 b/examples/Authentication/Web-AuthBasicAnon.ps1
similarity index 94%
rename from examples/Web-AuthBasicAnon.ps1
rename to examples/Authentication/Web-AuthBasicAnon.ps1
index 597c3cf9c..2f6648779 100644
--- a/examples/Web-AuthBasicAnon.ps1
+++ b/examples/Authentication/Web-AuthBasicAnon.ps1
@@ -27,7 +27,7 @@
Invoke-RestMethod -Uri http://localhost:8081/users -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicAnon.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicAnon.ps1
.NOTES
Author: Pode Team
@@ -35,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicBearer.ps1 b/examples/Authentication/Web-AuthBasicBearer.ps1
similarity index 80%
rename from examples/Web-AuthBasicBearer.ps1
rename to examples/Authentication/Web-AuthBasicBearer.ps1
index f6c73820d..5ccec6948 100644
--- a/examples/Web-AuthBasicBearer.ps1
+++ b/examples/Authentication/Web-AuthBasicBearer.ps1
@@ -9,10 +9,16 @@
.EXAMPLE
To run the sample: ./Web-AuthBasicBearer.ps1
- Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' }
+ Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' } -ResponseHeadersVariable headers
+
+.EXAMPLE
+ "No Authorization header found"
+
+ Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -ResponseHeadersVariable headers -Verbose -SkipHttpErrorCheck
+ $headers | Format-List
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicBearer.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicBearer.ps1
.NOTES
Author: Pode Team
@@ -20,7 +26,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -64,7 +70,7 @@ Start-PodeServer -Threads 2 {
}
# GET request to get list of users (since there's no session, authentication will always happen)
- Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock {
+ Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
diff --git a/examples/Web-AuthBasicClientcert.ps1 b/examples/Authentication/Web-AuthBasicClientcert.ps1
similarity index 92%
rename from examples/Web-AuthBasicClientcert.ps1
rename to examples/Authentication/Web-AuthBasicClientcert.ps1
index 489b37b72..d99470183 100644
--- a/examples/Web-AuthBasicClientcert.ps1
+++ b/examples/Authentication/Web-AuthBasicClientcert.ps1
@@ -10,7 +10,7 @@
To run the sample: ./Web-AuthBasicClientcert.ps1
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicClientcert.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicClientcert.ps1
.NOTES
Author: Pode Team
@@ -18,7 +18,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
diff --git a/examples/Web-AuthBasicHeader.ps1 b/examples/Authentication/Web-AuthBasicHeader.ps1
similarity index 84%
rename from examples/Web-AuthBasicHeader.ps1
rename to examples/Authentication/Web-AuthBasicHeader.ps1
index 0fb0f8d27..169d3f445 100644
--- a/examples/Web-AuthBasicHeader.ps1
+++ b/examples/Authentication/Web-AuthBasicHeader.ps1
@@ -17,7 +17,8 @@
The example used here is Basic authentication.
Login:
- $session = (Invoke-WebRequest -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }).Headers['pode.sid']
+ Invoke-RestMethod -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } -ResponseHeadersVariable headers -SkipHttpErrorCheck
+ $session = $headers['pode.sid']
Users:
Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ 'pode.sid' = "$session" }
@@ -26,7 +27,7 @@
Invoke-WebRequest -Uri http://localhost:8081/logout -Method Post -Headers @{ 'pode.sid' = "$session" }
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicHeader.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthBasicHeader.ps1
.NOTES
Author: Pode Team
@@ -34,7 +35,7 @@
#>
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -81,13 +82,13 @@ Start-PodeServer -Threads 2 {
}
# POST request to login
- Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login'
+ Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login' -ErrorContentType 'application/json'
# POST request to logout
- Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout
+ Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout -ErrorContentType 'application/json'
# POST request to get list of users - the "pode.sid" header is expected
- Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ScriptBlock {
+ Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ErrorContentType 'application/json' -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
diff --git a/examples/Authentication/Web-AuthBearerJWT.ps1 b/examples/Authentication/Web-AuthBearerJWT.ps1
new file mode 100644
index 000000000..1ae85402f
--- /dev/null
+++ b/examples/Authentication/Web-AuthBearerJWT.ps1
@@ -0,0 +1,248 @@
+<#
+.SYNOPSIS
+ A PowerShell script to set up a Pode server with JWT authentication and various route configurations.
+
+.DESCRIPTION
+ This script initializes a Pode server that listens on a specified port, enables request and error logging,
+ and configures JWT authentication using either the request header or query parameters. It also defines
+ a protected route to fetch a list of users, requiring authentication.
+
+.PARAMETER Location
+ Specifies where the API key (JWT token) is expected.
+ Valid values: 'Header', 'Query'.
+ Default: 'Header'.
+
+.EXAMPLE
+ # Run the sample
+ ./WebAuth-bearerJWT.ps1
+
+ JWT payload:
+ {
+ "sub": "1234567890",
+ "name": "morty",
+ "username":"morty",
+ "type": "Human",
+ "id" : "M0R7Y302",
+ "admin": true,
+ "iat": 1516239022,
+ "exp": 2634234231,
+ "iss": "auth.example.com",
+ "sub": "1234567890",
+ "aud": "myapi.example.com",
+ "nbf": 1690000000,
+ "jti": "unique-token-id",
+ "role": "admin"
+ }
+
+.EXAMPLE
+ # Example request using PS512 JWT authentication
+ $jwt = ConvertTo-PodeJwt -PfxPath ./cert.pfx -RsaPaddingScheme Pss -PfxPassword (ConvertTo-SecureString 'mySecret' -AsPlainText -Force)
+ $headers = @{ 'Authorization' = "Bearer $jwt" }
+ $response = Invoke-RestMethod -Uri 'http://localhost:8081/auth/bearer/jwt/PS512' -Method Get -Headers $headers
+
+.EXAMPLE
+ # Example request using RS384 JWT authentication
+ $headers = @{ 'Authorization' = 'Bearer ' }
+ $response = Invoke-RestMethod -Uri 'http://localhost:8081/users' -Method Get -Headers $headers
+
+.EXAMPLE
+ # Example request using HS256 JWT authentication
+ $jwt = ConvertTo-PodeJwt -Algorithm HS256 -Secret (ConvertTo-SecureString 'secret' -AsPlainText -Force) -Payload @{id='id';name='Morty'}
+ $headers = @{ 'Authorization' = "Bearer $jwt" }
+ $response = Invoke-RestMethod -Uri 'http://localhost:8081/users' -Method Get -Headers $headers
+
+ .LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthbearerJWT.ps1
+
+ .NOTES
+ - This script uses Pode to create a lightweight web server with authentication.
+ - JWT authentication is handled via Bearer tokens passed in either the header or query.
+ - Ensure the private key is securely stored and managed for RS256-based JWT signing.
+ - Using query parameters for authentication is **discouraged** due to security risks.
+ - Always use HTTPS in production to protect sensitive authentication data.
+
+ Author: Pode Team
+ License: MIT License
+#>
+
+param(
+ [Parameter()]
+ [ValidateSet('Header', 'Query' )]
+ [string]
+ $Location = 'Header'
+)
+
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 -ApplicationName 'webauth' {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
+
+ New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+
+
+ $JwtVerificationMode = 'Lenient' # Set your desired verification mode (Lenient or Strict)
+
+ $certificateTypes = @{
+ 'RS256' = @{
+ KeyType = 'RSA'
+ KeyLength = 2048
+ }
+ 'RS384' = @{
+ KeyType = 'RSA'
+ KeyLength = 3072
+ }
+ 'RS512' = @{
+ KeyType = 'RSA'
+ KeyLength = 4096
+ }
+ 'PS256' = @{
+ KeyType = 'RSA'
+ KeyLength = 2048
+ }
+ 'PS384' = @{
+ KeyType = 'RSA'
+ KeyLength = 3072
+ }
+ 'PS512' = @{
+ KeyType = 'RSA'
+ KeyLength = 4096
+ }
+ 'ES256' = @{
+ KeyType = 'ECDSA'
+ KeyLength = 256
+ }
+ 'ES384' = @{
+ KeyType = 'ECDSA'
+ KeyLength = 384
+ }
+ 'ES512' = @{
+ KeyType = 'ECDSA'
+ KeyLength = 521
+ }
+ }
+
+ $CertsPath = Join-Path -Path (Get-PodeServerPath) -ChildPath "certs"
+ if (!(Test-Path -Path $CertsPath -PathType Container)) {
+ New-Item -Path $CertsPath -ItemType Directory
+ }
+ foreach ($alg in $certificateTypes.Keys) {
+ $x509Certificate = New-PodeSelfSignedCertificate -Loopback -KeyType $certificateTypes[$alg].KeyType -KeyLength $certificateTypes[$alg].KeyLength -CertificatePurpose CodeSigning -Ephemeral -Exportable
+
+ Export-PodeCertificate -Certificate $x509Certificate -Format PFX -Path (join-path -path $CertsPath -ChildPath $alg)
+
+ # Define the authentication location dynamically (e.g., `/auth/bearer/jwt/{algorithm}`)
+ $pathRoute = "/auth/bearer/jwt/$alg"
+ # Register Pode Bearer Authentication
+ Write-PodeHost "🔹 Registering JWT Authentication for: $alg ($Location)"
+
+ $rsaPaddingScheme = if ($alg.StartsWith('PS')) { 'Pss' } else { 'Pkcs1V15' }
+
+ $param = @{
+ Location = $Location
+ AsJWT = $true
+ RsaPaddingScheme = $rsaPaddingScheme
+ JwtVerificationMode = $JwtVerificationMode
+ X509Certificate = $x509Certificate
+ }
+
+ New-PodeAuthBearerScheme @param |
+ Add-PodeAuth -Name "Bearer_JWT_$alg" -Sessionless -ScriptBlock {
+ param($jwt)
+
+ # here you'd check a real user storage, this is just for example
+ if ($jwt.username -ieq 'morty') {
+ return @{
+ User = @{
+ ID = $jWt.id
+ Name = $jst.name
+ Type = $jst.type
+ }
+ }
+ }
+
+ return $null
+ }
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path $pathRoute -Authentication "Bearer_JWT_$alg" -ScriptBlock {
+
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+ }
+
+
+
+ # setup bearer auth
+ New-PodeAuthBearerScheme -Location $Location -AsJWT -Secret (ConvertTo-SecureString 'your-256-bit-secret' -AsPlainText -Force) -JwtVerificationMode Lenient | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock {
+ param($jwt)
+
+ # here you'd check a real user storage, this is just for example
+ if ($jwt.username -ieq 'morty') {
+ return @{
+ User = @{
+ ID = $jWt.id
+ Name = $jst.name
+ Type = $jst.type
+ }
+ }
+ }
+
+ return $null
+ }
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock {
+
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+
+
+ Register-PodeEvent -Type Stop -Name 'CleanCerts' -ScriptBlock {
+ if ( (Test-Path -Path "$(Get-PodeServerPath)/cert" -PathType Container)) {
+ Remove-Item -Path "$(Get-PodeServerPath)/cert" -Recurse -Force
+ Write-PodeHost "$(Get-PodeServerPath)/cert removed."
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/Authentication/Web-AuthBearerJWTLifecycle.ps1 b/examples/Authentication/Web-AuthBearerJWTLifecycle.ps1
new file mode 100644
index 000000000..90fe58d93
--- /dev/null
+++ b/examples/Authentication/Web-AuthBearerJWTLifecycle.ps1
@@ -0,0 +1,274 @@
+<#
+.SYNOPSIS
+ A PowerShell script demonstrating the full lifecycle of JWT authentication using X.509 certificates in Pode.
+
+.DESCRIPTION
+ This script sets up a Pode server that listens on a specified port and enables JWT authentication using X.509 certificates.
+ It showcases the full JWT authentication lifecycle, including login, renewal, validation, and retrieval of user information.
+ Authentication is performed using JWT tokens signed with the selected cryptographic algorithm.
+
+.PARAMETER Location
+ Specifies where the API key (JWT token) is expected.
+ Valid values: 'Header', 'Query'.
+ Default: 'Header'.
+
+.PARAMETER Algorithm
+ Specifies the cryptographic algorithm used for JWT signing and verification.
+ Valid values: 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512'.
+ Default: 'ES512'.
+
+.EXAMPLE
+ # Run the sample
+ ./WebAuth-bearerJWT.ps1
+
+ JWT payload example:
+ {
+ "sub": "1234567890",
+ "name": "morty",
+ "username": "morty",
+ "type": "Human",
+ "id": "M0R7Y302",
+ "admin": true,
+ "iat": 1516239022,
+ "exp": 2634234231,
+ "iss": "auth.example.com",
+ "aud": "myapi.example.com",
+ "nbf": 1690000000,
+ "jti": "unique-token-id",
+ "role": "admin"
+ }
+
+.LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthbearerJWTLifecycle.ps1
+
+.NOTES
+ - This script uses Pode to create a lightweight web server with authentication.
+ - JWT authentication is handled via Bearer tokens passed in either the header or query parameters.
+ - JWTs are signed using X.509 certificates and verified based on the selected algorithm.
+ - The script implements endpoints for login, token renewal, and token validation.
+ - Ensure the private key is securely stored and managed for RS256-based JWT signing.
+ - Using query parameters for authentication is **discouraged** due to security risks.
+ - Always use HTTPS in production to protect sensitive authentication data.
+
+ Author: Pode Team
+ License: MIT License
+#>
+
+param(
+ [Parameter()]
+ [ValidateSet('Header', 'Query' )]
+ [string]
+ $Location = 'Header',
+
+ [Parameter()]
+ [ValidateSet( 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512')]
+ [string]
+ $Algorithm = 'ES512'
+)
+
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# Define a function to autenticate user credentials
+function Test-User {
+ param (
+ [string]$username,
+ [string]$password
+ )
+ if ($username -eq 'morty' -and $password -eq 'pickle') {
+ return @{
+ Id = 'M0R7Y302'
+ Username = 'morty.smith'
+ Name = 'Morty Smith'
+ Groups = 'Domain Users'
+ }
+ }
+ throw 'Invalid credentials'
+}
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 -ApplicationName 'webauth' {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8043 -Protocol Https -SelfSigned
+
+ New-PodeLoggingMethod -File -Name 'requests' | Enable-PodeRequestLogging
+ New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging
+ # Configure CORS
+ Set-PodeSecurityAccessControl -Origin '*' -Duration 7200 -WithOptions -AuthorizationHeader -autoMethods -AutoHeader -Credentials -CrossDomainXhrRequests
+
+
+ # Enable OpenAPI documentation
+
+ Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -EnableSchemaValidation:($PSVersionTable.PSEdition -eq 'Core') -DisableMinimalDefinitions -NoDefaultResponses
+ Add-PodeOAInfo -Title 'JWT Test' -Version 1.0.17 -Description 'test'
+ Add-PodeOAServerEndpoint -url '/auth/bearer/jwt' -Description 'default endpoint'
+ # Enable OpenAPI viewers
+ Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger'
+ Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' -DarkMode
+ Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode
+ Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode
+ Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode
+ Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' -DarkMode
+
+ # Enable OpenAPI editor and bookmarks
+ Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor'
+ Enable-PodeOAViewer -Bookmarks -Path '/docs'
+
+
+ $JwtVerificationMode = 'Strict' # Set your desired verification mode (Lenient or Strict)
+ # $SecurePassword = ConvertTo-SecureString 'MySecurePassword' -AsPlainText -Force
+
+ $param = @{
+ Location = $Location
+ AsJWT = $true
+ JwtVerificationMode = $JwtVerificationMode
+ SelfSigned = $true
+ }
+ # Register Pode Bearer Authentication
+ New-PodeAuthBearerScheme @param |
+ Add-PodeAuth -Name "Bearer_JWT_$Algorithm" -Sessionless -ScriptBlock {
+ param($jwt)
+
+ # here you'd check a real user storage, this is just for example
+ if ($jwt.id -ieq 'M0R7Y302') {
+ return @{
+ User = @{
+ ID = $jWt.id
+ Name = $jWt.name
+ Type = $jWt.type
+ sub = $jWt.Id
+ username = $jWt.Username
+ groups = $jWt.Groups
+ }
+ }
+ }
+ else {
+ write-podehost $jwt -Explode
+ }
+
+ return $null
+ }
+
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -PassThru -Method Get -Path "/auth/bearer/jwt/$Algorithm" -Authentication "Bearer_JWT_$Algorithm" -ScriptBlock {
+ Write-PodeJsonResponse -Value $WebEvent.auth.User
+ } | Set-PodeOARouteInfo -Summary 'Get my info.' -Tags 'user' -OperationId "myinfo_$Algorithm"
+
+
+
+ Add-PodeRoute -PassThru -Method Post -Path '/auth/bearer/jwt/login' -ArgumentList $Algorithm -ScriptBlock {
+ param(
+ [string]
+ $Algorithm
+ )
+ try {
+ # In a real scenario, you'd validate the incoming credentials from $WebEvent.data
+ $username = $WebEvent.Data.username
+ $password = $WebEvent.Data.password
+ $user = Test-User -username $username -password $password
+
+
+ $payload = @{
+ sub = $user.Id
+ name = $user.Name
+ username = $user.Username
+ id = $user.Id
+ groups = $user.Groups
+ type = 'human'
+ }
+
+ # If valid, generate a JWT that matches the 'ExampleApiKeyCert' scheme
+ $jwt = ConvertTo-PodeJwt -Payload $payload -Authentication "Bearer_JWT_$Algorithm" -Expiration 600
+ Write-PodeJsonResponse -StatusCode 200 -Value @{
+ 'success' = $true
+ 'user' = $user
+ 'token' = $jwt
+ }
+
+ }
+ catch {
+ write-podehost $_.Exception.Message
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid credentials' }
+ }
+ } | Set-PodeOARouteInfo -Summary 'Logs user into the system.' -Tags 'user' -OperationId 'loginUser' -PassThru |
+ Set-PodeOARequest -RequestBody (
+ New-PodeOARequestBody -Description 'Update an existent pet in the store' -Required -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOAStringProperty -Name 'username' -Description 'The user name for login' -Default 'morty' |
+ New-PodeOAStringProperty -Name 'password' -Description 'The password for login in clear text' -Format Password -Default 'pickle' |
+ New-PodeOAObjectProperty)
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOABoolProperty -Name 'success' -Description 'Operation success' -Example $true |
+ New-PodeOAStringProperty -Name 'user' -Description 'The user for login' -Example 'morty' |
+ New-PodeOAStringProperty -Name 'token' -Description 'Bearen JWT token' -Example '6656565' |
+ New-PodeOAObjectProperty
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username/password supplied'
+
+ Add-PodeRoute -PassThru -Method Post -Path '/auth/bearer/jwt/renew' -Authentication "Bearer_JWT_$Algorithm" -ScriptBlock {
+ try {
+
+ $jwt = Update-PodeJwt
+
+ Write-PodeJsonResponse -StatusCode 200 -Value @{
+ 'success' = $true
+ 'token' = $jwt
+ }
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
+ } | Set-PodeOARouteInfo -Summary 'Extend JWT Token.' -Tags 'JWT' -OperationId 'renewToken' -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOABoolProperty -Name 'success' -Description 'Operation success' -Example $true |
+ New-PodeOAStringProperty -Name 'user' -Description 'The user for login' -Example 'morty' |
+ New-PodeOAStringProperty -Name 'token' -Description 'Bearen JWT token' -Example 'eyJ0eXAiOiJKV1QifQ.eyJpZCI6Ik0wUjdZMzAyIi ... UG9kZSJ9.hhU1fmykkSyZhUCr1NSZto-dGyt50r5OUlYj5SgL88EFlnulSOtsM-61tht-X5lEZVP7TCwG2q6ZELiA-4zey7BTIEecKg8zQ4NasZQi6eq9scSL0WJPNHNiGf91F1BsSAQmTxmtJz9-R9l7dxxonFlgLhq9ZwToPuAEK76lYuEQ45ERH-LoO5En9nRnar5N8SLe244To_T7UPKKBgd_DQNSuW4pShMbeK1_TTwELxroV2-d7bPyhUKIwrP61DDsGxgYCzsJ_8XG4YOfFg_u3bHp_JEplCFPoc5KUVNOQHFCzYR0WMZDhRDMnAF6J8Xn0RKTsFB7q1QNC0NF1-7TGQ' |
+ New-PodeOAObjectProperty
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 401 -Description 'Invalid JWT token supplied'
+
+
+ Add-PodeRoute -PassThru -Method Get -Path '/auth/bearer/jwt/info' -Authentication "Bearer_JWT_$Algorithm" -ScriptBlock {
+ try {
+ $jwtInfo = ConvertFrom-PodeJwt -Outputs 'Header,Payload,Signature' -HumanReadable
+ $jwtInfo.success = $true
+ Write-PodeJsonResponse -StatusCode 200 -Value $jwtInfo
+ }
+ catch {
+ Write-PodeJsonResponse -StatusCode 401 -Value @{ error = 'Invalid JWT token supplied' }
+ }
+ } | Set-PodeOARouteInfo -Summary 'return JWT Token info.' -Tags 'JWT' -OperationId 'getInfoToken' -PassThru |
+ Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content (
+ New-PodeOAContentMediaType -ContentType 'application/json' -Content (
+ New-PodeOAObjectProperty -Properties (
+ ( New-PodeOABoolProperty -Name 'success' -Description 'Operation success' -Example $true),
+ (New-PodeOAObjectProperty -Name Header ),
+ ( New-PodeOAObjectProperty -Name Payload), ( New-PodeOAStringProperty -Name Signature)
+ )
+ )
+ ) -PassThru |
+ Add-PodeOAResponse -StatusCode 401 -Description 'Invalid JWT token supplied'
+
+}
\ No newline at end of file
diff --git a/examples/Authentication/Web-AuthDigest.ps1 b/examples/Authentication/Web-AuthDigest.ps1
new file mode 100644
index 000000000..779df40cc
--- /dev/null
+++ b/examples/Authentication/Web-AuthDigest.ps1
@@ -0,0 +1,215 @@
+<#
+.SYNOPSIS
+ PowerShell script to set up a Pode server with Digest authentication or make client requests.
+
+.DESCRIPTION
+ This script can either:
+ - Start a Pode server that listens on a specified port and uses Digest authentication to secure access.
+ - Act as a client to send requests with Digest authentication.
+
+ The authentication details are checked against predefined user data.
+ For non-MD5 algorithms, use ./utility/DigestClient.ps1.
+
+.PARAMETER Client
+ If specified, the script runs in client mode instead of starting a server.
+
+.PARAMETER Algorithm
+ The Digest authentication algorithm(s) to use. Supported values: MD5, SHA-1, SHA-256, SHA-512, SHA-384, SHA-512/256.
+ Defaults to all supported algorithms.
+
+.PARAMETER QualityOfProtection
+ Specifies the Quality of Protection (qop) to use in Digest authentication.
+ Valid options:
+ - 'auth': Authentication only.
+ - 'auth-int': Authentication with integrity protection.
+ - 'auth,auth-int': Support both modes.
+
+.EXAMPLE
+ To start the Pode server with default settings:
+ ```powershell
+ ./Web-AuthDigest.ps1
+ ```
+
+.EXAMPLE
+ To start the Pode server with SHA-256 authentication only:
+ ```powershell
+ ./Web-AuthDigest.ps1 -Algorithm SHA-256
+ ```
+
+.EXAMPLE
+ To run in client mode and send a Digest-authenticated request:
+ ```powershell
+ ./Web-AuthDigest.ps1 -Client
+ ```
+
+.EXAMPLE
+ Client request example using default .Net Digest support:
+
+ ```powershell
+ # Define the URI and credentials
+ $uri = [System.Uri]::new("http://localhost:8081/users")
+ $username = "morty"
+ $password = "pickle"
+
+ # Create a credential cache and add Digest authentication
+ $credentialCache = [System.Net.CredentialCache]::new()
+ $networkCredential = [System.Net.NetworkCredential]::new($username, $password)
+ $credentialCache.Add($uri, "Digest", $networkCredential)
+
+ # Create the HTTP client handler with the credential cache
+ $handler = [System.Net.Http.HttpClientHandler]::new()
+ $handler.Credentials = $credentialCache
+
+ # Create the HTTP client
+ $httpClient = [System.Net.Http.HttpClient]::new($handler)
+
+ # Send the GET request
+ $requestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri)
+ $response = $httpClient.SendAsync($requestMessage).Result
+
+ # Display response headers and content
+ $response.Headers | ForEach-Object { "$($_.Key): $($_.Value)" }
+ $content = $response.Content.ReadAsStringAsync().Result
+ $content
+ ```
+.EXAMPLE
+ Client request example using `Invoke-WebRequestDigest`:
+
+ ```powershell
+ Import-Module './client/Invoke-Digest.psm1'
+
+ # Define the URI and credentials
+ $uri = 'http://localhost:8081/users'
+ $username = 'morty'
+ $password = 'pickle'
+
+ # Convert the password to a SecureString and create a credential object
+ $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+ $credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+ # Make a GET request using Digest authentication
+ $response = Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential
+
+ # Display response headers and content
+ $response.Headers | Format-List
+ Write-Output $response.Content
+ ```
+
+.EXAMPLE
+ Running the server with `auth-int` quality of protection:
+ ```powershell
+ ./Web-AuthDigest.ps1 -QualityOfProtection auth-int
+ ```
+
+.LINK
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthDigest.ps1
+
+.NOTES
+ Author: Pode Team
+ License: MIT License
+#>
+
+[CmdletBinding(DefaultParameterSetName = 'Server')]
+param(
+ [Parameter(ParameterSetName = 'Client')]
+ [switch]
+ $Client,
+
+ [Parameter(ParameterSetName = 'Server')]
+ [string[]]
+ $Algorithm = @('MD5', 'SHA-1', 'SHA-256', 'SHA-512', 'SHA-384', 'SHA-512/256'),
+
+ [Parameter(ParameterSetName = 'Server')]
+ [ValidateSet('auth', 'auth-int', 'auth,auth-int' )]
+ [string[]]
+ $QualityOfProtection = 'auth,auth-int'
+)
+if ($Client) {
+ Import-Module './Modules/Invoke-Digest.psm1'
+ $uri = 'http://localhost:8081/users'
+ $username = 'morty'
+ $password = 'pickle'
+
+ $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
+ $credential = [System.Management.Automation.PSCredential]::new($username, $securePassword)
+
+ $response = Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential
+ $response | Format-List *
+
+ Invoke-WebRequestDigest -Uri $uri -Method 'GET' -Credential $credential -OutFile 'outfile.json'
+
+ $response = Invoke-RestMethodDigest -Uri $uri -Method 'GET' -Credential $credential
+ $response
+ return
+}
+try {
+ # Determine the script path and Pode module path
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
+ $podePath = Split-Path -Parent -Path $ScriptPath
+
+ # Import the Pode module from the source path if it exists, otherwise from installed modules
+ if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) {
+ Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop
+ }
+ else {
+ Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop
+ }
+}
+catch { throw }
+
+# or just:
+# Import-Module Pode
+
+# create a server, and start listening on port 8081
+Start-PodeServer -Threads 2 {
+
+ # listen on localhost:8081
+ Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http
+
+ # setup digest auth
+ New-PodeAuthDigestScheme -Algorithm $Algorithm -QualityOfProtection $QualityOfProtection | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock {
+ param($username, $params)
+
+ # here you'd check a real user storage, this is just for example
+ if ($username -ieq 'morty') {
+ return @{
+ User = @{
+ ID = 'M0R7Y302'
+ Name = 'Morty'
+ Type = 'Human'
+ }
+ Password = 'pickle'
+ }
+ }
+
+ return $null
+ }
+ # If QualityOfProtection is 'auth-int' skip GET because it is not supported
+ if ($QualityOfProtection -ne 'auth-int') {
+ # GET request to get list of users (since there's no session, authentication will always happen)
+ Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock {
+ Write-PodeJsonResponse -Value @{
+ Users = @(
+ @{
+ Name = 'Deep Thought'
+ Age = 42
+ },
+ @{
+ Name = 'Leeroy Jenkins'
+ Age = 1337
+ }
+ )
+ }
+ }
+ }
+
+ Add-PodeRoute -Method Post -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock {
+ if ($WebEvent.data) {
+ Write-PodeJsonResponse -Value $WebEvent.data -StatusCode 200
+ }
+ else {
+ Write-PodeJsonResponse -Value @{success = $false } -StatusCode 400
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/examples/Web-AuthForm.ps1 b/examples/Authentication/Web-AuthForm.ps1
similarity index 92%
rename from examples/Web-AuthForm.ps1
rename to examples/Authentication/Web-AuthForm.ps1
index 45fad7a19..095f6539d 100644
--- a/examples/Web-AuthForm.ps1
+++ b/examples/Authentication/Web-AuthForm.ps1
@@ -22,7 +22,7 @@
You will be redirected to the login page, where you can log in with the credentials provided above.
.LINK
- https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthForm.ps1
+ https://github.com/Badgerati/Pode/blob/develop/examples/Authentication/Web-AuthForm.ps1
.NOTES
Author: Pode Team
@@ -31,7 +31,7 @@
try {
# Determine the script path and Pode module path
- $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
+ $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path))
$podePath = Split-Path -Parent -Path $ScriptPath
# Import the Pode module from the source path if it exists, otherwise from installed modules
@@ -63,7 +63,7 @@ Start-PodeServer -Threads 2 {
Enable-PodeSessionMiddleware -Duration 120 -Extend
# setup form auth (