JWT Tokens

Issue and validate bearer tokens for stateless auth.

Full source

File: pwsh/tutorial/examples/8.4-Jwt.ps1

<#
    Sample: JWT Token Issuance & Validation
    Purpose: Show issuing a JWT after Basic auth and protecting routes with a bearer scheme.
    File:    8.4-Jwt.ps1
    Notes:   Uses symmetric HMAC key; store securely (e.g., KeyVault, env var) in production.
#>
param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)
# 1. Logging
New-KrLogger | Add-KrSinkConsole | Register-KrLogger -Name 'console' -SetAsDefault | Out-Null

# 2. Server
New-KrServer -Name 'Auth JWT'

# 3. Listener
Add-KrEndpoint -Port $Port -IPAddress $IPAddress -SelfSignedCert


# 5. Initial Basic scheme for token issuance
Add-KrBasicAuthentication -Name 'BasicInit' -Realm 'Init' -AllowInsecureHttp -ScriptBlock { param($Username, $Password) $Username -eq 'admin' -and $Password -eq 'password' }

# 6. Build JWT configuration
$jwtBuilder = New-KrJWTBuilder |
    Add-KrJWTIssuer -Issuer 'KestrunApi' |
    Add-KrJWTAudience -Audience 'KestrunClients' |
    Protect-KrJWT -HexadecimalKey '6f1a1ce2e8cc4a5685ad0e1d1f0b8c092b6dce4f7a08b1c2d3e4f5a6b7c8d9e0' -Algorithm HS256
$result = Build-KrJWT -Builder $jwtBuilder
$validation = $result | Get-KrJWTValidationParameter

# 7. Register bearer scheme
Add-KrJWTBearerAuthentication -Name 'Bearer' -ValidationParameter $validation

# 8. Finalize configuration
Enable-KrConfiguration

# 9. Route: issue token (requires Basic)
Add-KrMapRoute -Verbs Get -Pattern '/token/new' -AuthorizationSchema 'BasicInit' -ScriptBlock {
    $user = $Context.User.Identity.Name
    Write-KrLog -Level Information -Message 'Generating JWT token for user {User}' -Values $user
    Write-KrLog -Level Information -Message 'Issuer : {Issuer} ' -Values $JwtTokenBuilder.Issuer
    Write-KrLog -Level Information -Message 'Audience : {Audience} ' -Values $JwtTokenBuilder.Audience
    Write-KrLog -Level Information -Message 'Algorithm: {Algorithm} ' -Values $JwtTokenBuilder.Algorithm

    $build = Copy-KrJWTTokenBuilder -Builder $jwtBuilder |
        Add-KrJWTSubject -Subject $user |
        Add-KrJWTClaim -UserClaimType Name -Value $user |
        Add-KrJWTClaim -UserClaimType Role -Value 'admin' |
        Build-KrJWT
    $token = $build | Get-KrJWTToken
    Write-KrJsonResponse @{ access_token = $token; expires = $build.Expires }
}

Add-KrMapRoute -Verbs Get -Pattern '/token/renew' -AuthorizationSchema $JwtScheme -ScriptBlock {
    $user = $Context.User.Identity.Name

    Write-KrLog -Level Information -Message 'Generating JWT token for user {0}' -Values $user
    $accessToken = $jwtBuilder | Update-KrJWT -FromContext
    Write-KrJsonResponse -InputObject @{
        access_token = $accessToken
        token_type = 'Bearer'
        expires_in = $build.Expires
    } -ContentType 'application/json'
}

# 10. Route: protected with bearer token
Add-KrMapRoute -Verbs Get -Pattern '/secure/jwt/hello' -AuthorizationSchema 'Bearer' -ScriptBlock {
    Write-KrTextResponse -InputObject "JWT Hello $( $Context.User.Identity.Name )"
}

# 11. Start server
Start-KrServer -CloseLogsOnExit


Step-by-step

  1. Logger: create console logger and register as default.
  2. Server: create named server Auth JWT.
  3. Listener: add HTTPS listener on loopback with a self-signed certificate.
  4. Runtime: enable PowerShell runtime.
  5. Initial auth: add Basic scheme (BasicInit) for first-factor credential check.
  6. Build JWT config: chain issuer, audience, and HMAC protection; build & capture validation params.
  7. Bearer scheme: register JWT bearer authentication using validation parameters.
  8. Enable configuration: lock in server/listener/auth schemes.
  9. Issue route: /token/new requires Basic and creates a token with subject, name and role claims.
  10. Renew route: /token/renew (Bearer) refreshes the token using Update-KrJWT -FromContext.
  11. Protected route: /secure/jwt/hello requires a valid bearer token.
  12. Start server.

Try it

# 1. Get initial bearer token using Basic auth
$basicHeader = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('admin:password'))
$token = (Invoke-RestMethod https://127.0.0.1:5000/token/new -SkipCertificateCheck -Headers @{ Authorization = $basicHeader }).access_token

# 2. Call protected route with current bearer token
Invoke-RestMethod https://127.0.0.1:5000/secure/jwt/hello -SkipCertificateCheck -Headers @{ Authorization = "Bearer $token" }

# 3. Renew token (issue a fresh one using existing bearer)
$refreshedToken = (Invoke-RestMethod -Uri https://localhost:5000/token/renew -SkipCertificateCheck -Headers @{ Authorization = "Bearer $token" }).access_token

# 4. Call protected route with renewed token
Invoke-RestMethod -Uri https://localhost:5000/secure/jwt/hello -SkipCertificateCheck -Headers @{ Authorization = "Bearer $refreshedToken" }

# (Optional) Inspect token (header + payload; signature not revalidated here):
Get-KrJWTInfo -Token $token | Format-List Issuer, Audience, Expires, Algorithm, Claims

# (Optional) Validate token explicitly (signature + lifetime):
Test-KrJWT -Token $token -ValidationParameter ($jwtBuilder | Build-KrJWT | Get-KrJWTValidationParameter)

Note: -SkipCertificateCheck is used because the sample listener issues a self-signed cert. For production, trust the certificate or use a CA-issued cert instead.

References

Troubleshooting

Symptom Cause Fix
401 Bearer Missing or invalid token Provide Authorization: Bearer <token>
Token expired Expiration passed Re-issue via /token/new or /token/renew
Signature invalid Wrong key/algorithm Ensure same secret & algorithm
401 on renew Missing/invalid bearer on renew Include current valid Authorization: Bearer
Claims not updated Builder not copied per request Use Copy-KrJWTTokenBuilder before adding claims

Previous / Next

See also: JWT Guide for advanced scenarios (copying builders, renewal patterns, security).