Antiforgery Protection

Protect state‑changing endpoints (POST/PUT/PATCH/DELETE) from Cross-Site Request Forgery by validating a token that is bound to a user session cookie.

The antiforgery middleware issues:

Artifact Default (sample) Purpose
Cookie .Kestrun.AntiXSRF Stores the cryptographic token (HttpOnly, Secure, SameSite=Lax)
Header X-CSRF-TOKEN Client echoes the request (header value) for unsafe verbs
Endpoint /csrf-token Returns a fresh token for single-page apps / JS fetch

Safe verbs (GET, HEAD, OPTIONS, TRACE) do not require the header. Only unsafe verbs modifying server state are validated.

Full source

Sample script: pwsh/tutorial/examples/10.1-Antiforgery.ps1

<#
  FileName: 10.1-Antiforgery.ps1 (with antiforgery)
#>
param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)
# 1. Logging
New-KrLogger |
    Set-KrLoggerLevel -Value Debug |
    Add-KrSinkConsole |
    Register-KrLogger -Name 'console' -SetAsDefault
# 2. Server
New-KrServer -Name 'Simple Server'
# 3. Listener
Add-KrEndpoint -Port $Port -IPAddress $IPAddress -SelfSignedCert

# Required to run PowerShell routes/middleware


# --- Antiforgery: modern SPA preset ---
# Cookie: .Kestrun.AntiXSRF (HttpOnly, Secure, SameSite=Lax)
# Header: X-CSRF-TOKEN
Add-KrAntiforgeryMiddleware -CookieName '.Kestrun.AntiXSRF' -HeaderName 'X-CSRF-TOKEN'

Enable-KrConfiguration

# Add token endpoint
Add-KrAntiforgeryTokenRoute -Path '/csrf-token' | Out-Null

# ----- Sample routes (GETs don’t need tokens; they’re safe by convention) -----
Add-KrMapRoute -Verbs Get -Path '/hello' -ScriptBlock {
    Write-KrTextResponse -InputObject 'Hello, World!' -StatusCode 200
}
Add-KrMapRoute -Verbs Get -Path '/hello-json' -ScriptBlock {
    Write-KrJsonResponse -InputObject @{ message = 'Hello, World!' } -StatusCode 200
}
Add-KrMapRoute -Verbs Get -Path '/hello-xml' -ScriptBlock {
    Write-KrXmlResponse -InputObject @{ message = 'Hello, World!' } -StatusCode 200
}
Add-KrMapRoute -Verbs Get -Path '/hello-yaml' -ScriptBlock {
    Write-KrYamlResponse -InputObject @{ message = 'Hello, World!' } -StatusCode 200
}

# ----- Your /json route (explicitly opt-out from antiforgery) -----
$options = [Kestrun.Hosting.Options.MapRouteOptions]::new()
$options.Pattern = '/json'
$options.HttpVerbs = [Kestrun.Utilities.HttpVerb[]] @('Post')
$options.ScriptCode = @{
    ScriptBlock = {
        $message = Get-KrRequestHeader -Name 'message'
        Write-KrJsonResponse -InputObject @{ message = $message } -StatusCode 200
    }
}
$options.DisableAntiforgery = $true  # this remains open (no CSRF validation)
Add-KrMapRoute -Options $options

# ----- Example of a PROTECTED POST (requires X-CSRF-TOKEN header) -----
Add-KrMapRoute -Verbs Post -Path '/profile' -ScriptBlock {
    # If the X-CSRF-TOKEN header is missing/invalid, the antiforgery middleware will reject the request.
    $body = Get-KrRequestBody
    Write-KrJsonResponse -InputObject @{ saved = $true; name = $body.name } -StatusCode 200
}
# Note: No -DisableAntiforgery here → this POST is protected.

Start-KrServer

Step-by-step

  1. Add middleware and token endpoint early in configuration.
  2. Client fetches /csrf-token; server issues antiforgery cookie + returns token.
  3. For unsafe verbs (POST/PUT/PATCH/DELETE), client includes both cookie and X-CSRF-TOKEN header.
  4. Middleware validates that the header token matches the cookie context.
  5. If valid → request proceeds; if missing/invalid → 400/403 rejection.
  6. Safe verbs (GET/HEAD/OPTIONS/TRACE) and routes with -DisableAntiforgery skip validation.

Configuration

Add the middleware early (after runtime) and optionally customize names:

Add-KrAntiforgeryMiddleware -CookieName '.Kestrun.AntiXSRF' -HeaderName 'X-CSRF-TOKEN'
Add-KrAntiforgeryTokenRoute -Path '/csrf-token' | Out-Null

Disable antiforgery per route when appropriate (e.g., read-only JSON echo):

$options = [Kestrun.Hosting.Options.MapRouteOptions]::new()
$options.Pattern = '/json'
$options.HttpVerbs = [Kestrun.Utilities.HttpVerb[]] @('get')
$options.Code = {
    $message = Get-KrRequestHeader -Name 'message'
    Write-KrJsonResponse -InputObject @{ message = $message } -StatusCode 200
}
$options.Language = 'PowerShell'
$options.DisableAntiforgery = $true
Add-KrMapRoute -Options $options

Try it

The sample defines multiple routes; below we exercise each type (safe GETs, opt-out GET, protected POST success/failure).

# 1. Start the sample script (runs until you Ctrl+C)
pwsh .\docs\_includes\examples\pwsh\10.1-Antiforgery.ps1

# 2. Explore SAFE GET routes (no antiforgery header required)
Invoke-RestMethod -Uri https://127.0.0.1:5000/hello -SkipCertificateCheck
Invoke-RestMethod -Uri https://127.0.0.1:5000/hello-json -SkipCertificateCheck
Invoke-RestMethod -Uri https://127.0.0.1:5000/hello-xml -SkipCertificateCheck -Headers @{ 'Accept' = 'application/xml' }
Invoke-RestMethod -Uri https://127.0.0.1:5000/hello-yaml -SkipCertificateCheck -Headers @{ 'Accept' = 'application/x-yaml' }

# 3. Call the /json route (explicitly opted out via DisableAntiforgery)
Invoke-RestMethod -Uri 'https://127.0.0.1:5000/json?message=Ping' -SkipCertificateCheck

# 4. Fetch a CSRF token (sets cookie + returns token JSON)
$response = Invoke-WebRequest -Uri https://127.0.0.1:5000/csrf-token -SessionVariable sess -SkipCertificateCheck
$token = ($response.Content | ConvertFrom-Json).token

# (Optional) Inspect session cookie name/value
$sess.Cookies.GetCookies('https://127.0.0.1:5000') | Where-Object { $_.Name -like '*AntiXSRF' } | Format-List *

# 5. Protected POST SUCCESS (token + cookie)
Invoke-RestMethod -Uri https://127.0.0.1:5000/profile -Method Post -WebSession $sess -SkipCertificateCheck -Headers @{ 'X-CSRF-TOKEN' = $token } -Body '{"name":"Alice"}' -ContentType 'application/json'

# Create a new session for the second request (to simulate a separate request with same cookie) but not the header
$sess2 = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$sess2.Cookies = $sess.Cookies   # copy cookie container reference

# 6. Protected POST FAILURE (missing header) – expect 400/403
Invoke-RestMethod -Uri https://127.0.0.1:5000/profile -Method Post -WebSession $sess2 -SkipCertificateCheck -Body '{"name":"Bob"}' -ContentType 'application/json' -SkipHttpErrorCheck

# 7. (Optional) Re-fetch token to demonstrate rotation / idempotence
Invoke-RestMethod -Uri https://127.0.0.1:5000/csrf-token -WebSession $sess -SkipCertificateCheck

Security Notes

Concern Guidance
SameSite Lax is balanced for typical form/SPA flows. Use Strict if you do not rely on cross-site top-level navigation.
HTTPS Always serve antiforgery cookies over TLS in production (Secure flag).
Token Leakage Never log raw tokens. Treat them as secrets while valid.
Read-Only APIs For pure JSON GET APIs, antiforgery adds no value—focus on auth instead.
APIs with Bearer Tokens If you ONLY accept Bearer tokens (no cookies), antiforgery is usually unnecessary.

Need more depth (architecture, SPA patterns, form integration)? See the Antiforgery Deep Dive guide page.

Troubleshooting

Symptom Cause Fix
400/403 on POST Missing header Include X-CSRF-TOKEN with value from /csrf-token endpoint
Token rotates unexpectedly Browser clearing cookies or domain mismatch Check cookie domain/path and dev tools storage
Works in curl but not browser Header not set by frontend HTTP client Ensure JS fetch/axios includes custom header
Token endpoint not found Token route not added Call Add-KrAntiforgeryTokenRoute before Start-KrServer
Route unexpectedly bypassed -DisableAntiforgery set Remove flag for protected endpoints

References


Return to the Middleware index or the Tutorial index.

Previous / Next

Previous: Middleware Roadmap Next: Rate Limiting (planned)