Skip to content

ggicci/caddy-jwt

Folders and files

NameName
Last commit message
Last commit date
Dec 7, 2023
May 23, 2022
Jun 30, 2021
Mar 3, 2023
Mar 17, 2023
Mar 17, 2023
Dec 27, 2022
Dec 27, 2022
Dec 8, 2023
Dec 8, 2023
Aug 7, 2023
May 1, 2023

Repository files navigation

caddy-jwt

Go Workflow codecov Go Report Card Go Reference

A Caddy HTTP Module - who Facilitates JWT Authentication

This module fulfilled http.handlers.authentication middleware as a provider named jwt.

Documentation

Install

Build this module with caddy at Caddy's official download site. Or:

xcaddy --with github.com/ggicci/caddy-jwt

Sample Caddyfile

{
	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"
	}
	reverse_proxy http://172.16.0.14:8080
}

NOTE:

  1. If you were using symmetric signing algorithms, e.g. HS256, encode your key bytes in base64 format as sign_key's value.
TkZMNSowQmMjOVU2RUB0bm1DJkU3U1VONkd3SGZMbVk=
  1. If you were using asymmetric signing algorithms, e.g. 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-----
  1. If you were using JWK, configure jwk_url and leave sign_key unset.

  2. caddy-jwt will determine the signing algorithm by looking into the following values:

    1. alg value in the JWT header;
    2. alg value of the matched JWK if using JWK;
    3. value of the sign_alg config.
  3. The priority of from_xxx is from_query > from_header > from_cookies.

Test it by yourself

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.
make example

TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5NTU4OTI2NzAsImp0aSI6IjgyMjk0YTYzLTk2NjAtNGM2Mi1hOGE4LTVhNjI2NWVmY2Q0ZSIsInN1YiI6IjM0MDYzMjc5NjM1MTY5MzIiLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6WyJodHRwczovL2FwaS5leGFtcGxlLmlvIl0sInVzZXJuYW1lIjoiZ2dpY2NpIn0.O8kvRO9y6xQO3AymqdFE7DDqLRBQhkntf78O9kF71F8

curl -v "http://localhost:8080?access_token=${TEST_TOKEN}"
# You should see authenticated output:
#
# User Authenticated with ID: 3406327963516932
#
# 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"
}

How it works?

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`β”œβ”€β”€β”€β”€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/

FAQ

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.

References