A Caddy HTTP Module - who Facilitates JWT Authentication
This module fulfilled http.handlers.authentication middleware as a provider named jwt.
Build this module with caddy at Caddy's official download site. Or build it with xcaddy locally by yourself:
# A caddy binary will be produced in your current directory.
xcaddy build --with github.com/ggicci/caddy-jwt
Requirements for a local build:
| caddy-jwt Version | Go version | Caddy Version |
|---|---|---|
| v1.1.1 | >=1.25.0 | >=2.10.1 |
| v1.1.0 | >=1.20.0 | >=2.8.0 |
{
order jwtauth before basicauth
}
api.example.com {
jwtauth {
sign_key TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
sign_alg HS256
jwk_url https://api.example.com/jwk/keys
from_query access_token token
from_header X-Api-Token
from_cookies user_session
issuer_whitelist https://api.example.com
audience_whitelist https://api.example.io https://learn.example.com
user_claims aud uid user_id username login
meta_claims "IsAdmin->is_admin" "settings.payout.paypal.enabled->is_paypal_enabled"
log_level debug
}
reverse_proxy http://172.16.0.14:8080
}
NOTE:
HS256, encode your key bytes in base64 format as sign_key's value.TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
RS256, encode your public key in x.509 PEM format as sign_key's value.-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArzekF0pqttKNJMOiZeyt
RdYiabdyy/sdGQYWYJPGD2Q+QDU9ZqprDmKgFOTxUy/VUBnaYr7hOEMBe7I6dyaS
5G0EGr8UXAwgD5Uvhmz6gqvKTV+FyQfw0bupbcM4CdMD7wQ9uOxDdMYm7g7gdGd6
SSIVvmsGDibBI9S7nKlbcbmciCmxbAlwegTYSHHLjwWvDs2aAF8fxeRfphwQZKkd
HekSZ090/c2V4i0ju2M814QyGERMoq+cSlmikCgRWoSZeWOSTj+rAZJyEAzlVL4z
8ojzOpjmxw6pRYsS0vYIGEDuyiptf+ODC8smTbma/p3Vz+vzyLWPfReQY2RHtpUe
hwIDAQAB
-----END PUBLIC KEY-----
If you were using JWK, configure jwk_url and leave sign_key unset.
caddy-jwt will determine the signing algorithm by looking into the following values:
alg value in the JWT header;alg value of the matched JWK if using JWK;sign_alg config.The priority of from_xxx is from_query > from_header > from_cookies.
Bypass the verification by turning on skip_verification option, #85.
Instead of specifying the sign_key directly as a value, you can use a feature introduced in Caddy v2.8.0 to load it from a file using sign_key {file./path/to/sign_key.txt}.
log_level controls the level of the user authenticated log line emitted on every successfully authenticated request. It accepts debug, info (the default), warn, or error. Production services typically want a single log line per request rather than one per middleware, so setting this to debug lets you fold this message into your access log's per-request debug output instead of duplicating it at info.
For caddy-jwt users, we assume you've already got a custom caddy binary built with our caddy-jwt plugin. Then you can run the test:
echo '{
order jwtauth before basicauth
}
:8080 {
jwtauth {
sign_key TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
sign_alg HS256
from_query access_token token
from_header X-Api-Token
from_cookies user_session
user_claims aud uid user_id username login
}
respond "User authenticated with ID: {http.auth.user.id}"
}' > /tmp/caddy-jwt-test.Caddyfile
# ./caddy is your custom caddy built, see Install section above
./caddy run --config /tmp/caddy-jwt-test.Caddyfile
# This token won't expire until year 2285.
TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5NTU4OTI2NzAsImp0aSI6IjgyMjk0YTYzLTk2NjAtNGM2Mi1hOGE4LTVhNjI2NWVmY2Q0ZSIsInN1YiI6IjM0MDYzMjc5NjM1MTY5MzIiLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6WyJodHRwczovL2FwaS5leGFtcGxlLmlvIl0sInVzZXJuYW1lIjoiZ2dpY2NpIn0.O8kvRO9y6xQO3AymqdFE7DDqLRBQhkntf78O9kF71F8
curl -v "http://localhost:8080?access_token=${TEST_TOKEN}"
# You should see
# 1. caddy log:
# http.authentication.providers.jwt user authenticated {"token_string": "eyJhbGciOiJIUzI1β¦Qhkntf78O9kF71F8", "user_claim": "username", "id": "ggicci"}
#
# 2. request response (curl command output):
# User Authenticated with ID: ggicci
# And the following command should also work:
curl -v -H"X-Api-Token: ${TEST_TOKEN}" "http://localhost:8080"
curl -v -H"Authorization: Bearer ${TEST_TOKEN}" "http://localhost:8080"
NOTE: you can decode the ${TEST_TOKEN} above at jwt.io to get human readable payload as follows:
{
"exp": 9955892670,
"jti": "82294a63-9660-4c62-a8a8-5a6265efcd4e",
"sub": "3406327963516932",
"iss": "https://api.example.com",
"aud": ["https://api.example.io"],
"username": "ggicci"
}
For caddy-jwt developers, you need to clone this repo, and start the caddy server in the repo folder:
git clone https://github.com/ggicci/caddy-jwt.git
cd caddy-jwt
# Build a caddy with this module and run an example server at localhost.
xcaddy run --config /tmp/caddy-jwt-test.Caddyfile
Any local code changes should reflect immediately.
Module caddy-jwt behaves like a "JWT Validator". The authentication flow is:
ββββββββββββββββββββ
βExtract token fromβ
β 1. query β
β 2. header β
β 3. cookies β
ββββββββββ¬ββββββββββ
β
βββββββββΌββββββββββββ
β is valid? β
β using `sign_key` β
β or validation is β
β disabled ββNOββββββββ
βββββββββ¬ββββββββββββ β
βYES β
βββββββββββββΌββββββββββββ β
βPopulate {http.user.id}β β
β by `user_claims` β β
βββββββββββββ¬ββββββββββββ β
β β
ββββββββββββΌββββββββββββ β
βis {http.user.id} set?βββNO(empty)
ββββββββββββ¬ββββββββββββ β β
βYES(non-empty) β β
ββββββββββββΌββββββββββββ β β
βPopulate {http.user.*}β β β
β by `meta_claims` β β β
ββββββββββββ¬ββββββββββββ β β
β β β
ββββββββββΌβββββββββββ ββββββββΌβββΌββββββ
β Authenticated β βUnauthenticatedβ
β Continue to Caddy β β 401 β
βββββββββββββββββββββ βββββββββββββββββ
flowchart by https://asciiflow.com/
Q1: How to deal with 401 responses on OPTIONS requests? (CORS related)
It should be handled separately by Caddy. Please read #24 for more details.
Q2: What to note when using a public key as the value of sign_key in Caddyfile?
Using multi-line content in a directive should be quoted as Caddy's documentation says. And the public key should be represented in PKCS#1 PEM format. Here's a simple command to derive such a public key from an RSA private key: openssl rsa -in input.rsa -pubout. Related: #36.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
6,486 followers Β· starred Aug 2022
140 followers Β· starred Dec 2025
296 followers Β· starred Mar 2022
33 followers Β· starred Sep 2025
A Caddy HTTP Module - who Facilitates JWT Authentication
This module fulfilled http.handlers.authentication middleware as a provider named jwt.
Build this module with caddy at Caddy's official download site. Or build it with xcaddy locally by yourself:
# A caddy binary will be produced in your current directory.
xcaddy build --with github.com/ggicci/caddy-jwt
Requirements for a local build:
| caddy-jwt Version | Go version | Caddy Version |
|---|---|---|
| v1.1.1 | >=1.25.0 | >=2.10.1 |
| v1.1.0 | >=1.20.0 | >=2.8.0 |
{
order jwtauth before basicauth
}
api.example.com {
jwtauth {
sign_key TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
sign_alg HS256
jwk_url https://api.example.com/jwk/keys
from_query access_token token
from_header X-Api-Token
from_cookies user_session
issuer_whitelist https://api.example.com
audience_whitelist https://api.example.io https://learn.example.com
user_claims aud uid user_id username login
meta_claims "IsAdmin->is_admin" "settings.payout.paypal.enabled->is_paypal_enabled"
log_level debug
}
reverse_proxy http://172.16.0.14:8080
}
NOTE:
HS256, encode your key bytes in base64 format as sign_key's value.TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
RS256, encode your public key in x.509 PEM format as sign_key's value.-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArzekF0pqttKNJMOiZeyt
RdYiabdyy/sdGQYWYJPGD2Q+QDU9ZqprDmKgFOTxUy/VUBnaYr7hOEMBe7I6dyaS
5G0EGr8UXAwgD5Uvhmz6gqvKTV+FyQfw0bupbcM4CdMD7wQ9uOxDdMYm7g7gdGd6
SSIVvmsGDibBI9S7nKlbcbmciCmxbAlwegTYSHHLjwWvDs2aAF8fxeRfphwQZKkd
HekSZ090/c2V4i0ju2M814QyGERMoq+cSlmikCgRWoSZeWOSTj+rAZJyEAzlVL4z
8ojzOpjmxw6pRYsS0vYIGEDuyiptf+ODC8smTbma/p3Vz+vzyLWPfReQY2RHtpUe
hwIDAQAB
-----END PUBLIC KEY-----
If you were using JWK, configure jwk_url and leave sign_key unset.
caddy-jwt will determine the signing algorithm by looking into the following values:
alg value in the JWT header;alg value of the matched JWK if using JWK;sign_alg config.The priority of from_xxx is from_query > from_header > from_cookies.
Bypass the verification by turning on skip_verification option, #85.
Instead of specifying the sign_key directly as a value, you can use a feature introduced in Caddy v2.8.0 to load it from a file using sign_key {file./path/to/sign_key.txt}.
log_level controls the level of the user authenticated log line emitted on every successfully authenticated request. It accepts debug, info (the default), warn, or error. Production services typically want a single log line per request rather than one per middleware, so setting this to debug lets you fold this message into your access log's per-request debug output instead of duplicating it at info.
For caddy-jwt users, we assume you've already got a custom caddy binary built with our caddy-jwt plugin. Then you can run the test:
echo '{
order jwtauth before basicauth
}
:8080 {
jwtauth {
sign_key TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
sign_alg HS256
from_query access_token token
from_header X-Api-Token
from_cookies user_session
user_claims aud uid user_id username login
}
respond "User authenticated with ID: {http.auth.user.id}"
}' > /tmp/caddy-jwt-test.Caddyfile
# ./caddy is your custom caddy built, see Install section above
./caddy run --config /tmp/caddy-jwt-test.Caddyfile
# This token won't expire until year 2285.
TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5NTU4OTI2NzAsImp0aSI6IjgyMjk0YTYzLTk2NjAtNGM2Mi1hOGE4LTVhNjI2NWVmY2Q0ZSIsInN1YiI6IjM0MDYzMjc5NjM1MTY5MzIiLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6WyJodHRwczovL2FwaS5leGFtcGxlLmlvIl0sInVzZXJuYW1lIjoiZ2dpY2NpIn0.O8kvRO9y6xQO3AymqdFE7DDqLRBQhkntf78O9kF71F8
curl -v "http://localhost:8080?access_token=${TEST_TOKEN}"
# You should see
# 1. caddy log:
# http.authentication.providers.jwt user authenticated {"token_string": "eyJhbGciOiJIUzI1β¦Qhkntf78O9kF71F8", "user_claim": "username", "id": "ggicci"}
#
# 2. request response (curl command output):
# User Authenticated with ID: ggicci
# And the following command should also work:
curl -v -H"X-Api-Token: ${TEST_TOKEN}" "http://localhost:8080"
curl -v -H"Authorization: Bearer ${TEST_TOKEN}" "http://localhost:8080"
NOTE: you can decode the ${TEST_TOKEN} above at jwt.io to get human readable payload as follows:
{
"exp": 9955892670,
"jti": "82294a63-9660-4c62-a8a8-5a6265efcd4e",
"sub": "3406327963516932",
"iss": "https://api.example.com",
"aud": ["https://api.example.io"],
"username": "ggicci"
}
For caddy-jwt developers, you need to clone this repo, and start the caddy server in the repo folder:
git clone https://github.com/ggicci/caddy-jwt.git
cd caddy-jwt
# Build a caddy with this module and run an example server at localhost.
xcaddy run --config /tmp/caddy-jwt-test.Caddyfile
Any local code changes should reflect immediately.
Module caddy-jwt behaves like a "JWT Validator". The authentication flow is:
ββββββββββββββββββββ
βExtract token fromβ
β 1. query β
β 2. header β
β 3. cookies β
ββββββββββ¬ββββββββββ
β
βββββββββΌββββββββββββ
β is valid? β
β using `sign_key` β
β or validation is β
β disabled ββNOββββββββ
βββββββββ¬ββββββββββββ β
βYES β
βββββββββββββΌββββββββββββ β
βPopulate {http.user.id}β β
β by `user_claims` β β
βββββββββββββ¬ββββββββββββ β
β β
ββββββββββββΌββββββββββββ β
βis {http.user.id} set?βββNO(empty)
ββββββββββββ¬ββββββββββββ β β
βYES(non-empty) β β
ββββββββββββΌββββββββββββ β β
βPopulate {http.user.*}β β β
β by `meta_claims` β β β
ββββββββββββ¬ββββββββββββ β β
β β β
ββββββββββΌβββββββββββ ββββββββΌβββΌββββββ
β Authenticated β βUnauthenticatedβ
β Continue to Caddy β β 401 β
βββββββββββββββββββββ βββββββββββββββββ
flowchart by https://asciiflow.com/
Q1: How to deal with 401 responses on OPTIONS requests? (CORS related)
It should be handled separately by Caddy. Please read #24 for more details.
Q2: What to note when using a public key as the value of sign_key in Caddyfile?
Using multi-line content in a directive should be quoted as Caddy's documentation says. And the public key should be represented in PKCS#1 PEM format. Here's a simple command to derive such a public key from an RSA private key: openssl rsa -in input.rsa -pubout. Related: #36.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
6,486 followers Β· starred Aug 2022
140 followers Β· starred Dec 2025
296 followers Β· starred Mar 2022
33 followers Β· starred Sep 2025