Skip to main content
Version: 3.19.0

Implement JWT Authentication

JWT authentication is a secure and stateless method for authenticating API clients with a signed JSON Web Token. The token can contain claims such as user identity and roles, allowing an API to validate a request without querying an authentication database. This approach works well for distributed applications that need lightweight authentication. Because a token remains valid until it expires, use short expiration times or implement token revocation for sensitive APIs.

In this guide, you will implement a scenario where there are two consumers using JWT authentication to authenticate with APISIX, each with a different rate limiting quota. Once implemented, consumers should have access to the upstream service and forward consumer IDs to the upstream service, opening up options for additional business logic.

Prerequisite(s)​

  • Install Python 3 to issue the example JWTs locally.
  • If you use the Admin API examples and authentication is enabled, export an admin-role key as ADMIN_API_KEY. Configure the key under deployment.admin.admin_key. The Docker quickstart disables authentication for local evaluation; keep it enabled in production.

Create Consumers​

A consumer is an application or a developer who consumes the API. You should always create consumers when using APISIX built-in authentication methods.

Create a consumer johndoe with an optional custom ID and a rate limiting quota of one request in a 30-second window:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "johndoe",
"labels": {
"custom_id": "john-doe-junior"
},
"plugins": {
"limit-count": {
"count": 1,
"time_window": 30,
"rejected_code": 429
}
}
}'

The custom ID will be forwarded to the upstream service, should you wish to implement additional business logic.

Create another consumer janedoe with an optional custom ID and a rate limiting quota of two requests in a 30-second window:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "janedoe",
"labels": {
"custom_id": "jane-doe-senior"
},
"plugins": {
"limit-count": {
"count": 2,
"time_window": 30,
"rejected_code": 429
}
}
}'

Create Consumer Credentials​

Credentials are used to configure authentication credentials associated with consumers.

Create jwt-auth credential for johndoe:

curl "http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "john-key",
"secret": "john-hs256-secret-that-is-very-long"
}
}
}'

Create jwt-auth credential for janedoe:

curl "http://127.0.0.1:9180/apisix/admin/consumers/janedoe/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jane-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "jane-key",
"secret": "jane-hs256-secret-that-is-very-long"
}
}
}'

Create a Route​

Create a route and enable jwt-auth:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "jwt-auth-route",
"uri": "/anything",
"plugins": {
"jwt-auth": {"claims_to_verify": ["exp"]}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

Issue JWT​

Issue both HS256 tokens locally so the signing secrets stay out of third-party websites. Each token below carries the consumer's key, its issue time, and an expiration five minutes later. Save the script as issue-jwts.py:

issue-jwts.py
import base64
import hashlib
import hmac
import json
import shlex
import time

def encode(value):
data = json.dumps(value, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

now = int(time.time())
for variable, key, secret in [
("john_jwt_token", "john-key", "john-hs256-secret-that-is-very-long"),
("jane_jwt_token", "jane-key", "jane-hs256-secret-that-is-very-long"),
]:
header = encode({"alg": "HS256", "typ": "JWT"})
payload = encode({"key": key, "iat": now, "exp": now + 300})
message = f"{header}.{payload}"
signature = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b"=").decode()
print(f"export {variable}={shlex.quote(message + '.' + encoded_signature)}")

Load fresh tokens into the current shell:

eval "$(python3 issue-jwts.py)"

The route explicitly requires exp through claims_to_verify. When that list is unset or empty, exp and nbf are checked only when present. The token header's alg must match the credential's configured algorithm; an algorithm mismatch is rejected before signature verification. Regenerate the tokens if verification takes longer than five minutes.

Verify​

Send a request to the route with john's key:

curl -i "http://127.0.0.1:9080/anything" -H "Authorization: ${john_jwt_token}"

You should see an HTTP/1.1 200 OK response similar to the following:

{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJqb2huLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.TgoM2efMEaSX7KrIWujGLGK5dpPGS2nbaiinmVVjKKw",
"Host": "127.0.0.1",
"User-Agent": "curl/8.6.0",
"X-Amzn-Trace-Id": "Root=1-6873b51b-5502cfcd72380323266c22e8",
"X-Consumer-Custom-Id": "john-doe-junior",
"X-Consumer-Username": "johndoe",
"X-Credential-Identifier": "cred-john-jwt-auth",
"X-Forwarded-Host": "127.0.0.1"
},
...
}

Let the earlier verification request's 30-second quota window expire, then generate three requests to the route with john's key:

sleep 31
resp=$(seq 3 | xargs -I{} curl "http://127.0.0.1:9080/anything" -H "Authorization: ${john_jwt_token}" -o /dev/null -s -w "%{http_code}\n") && \
count_200=$(echo "$resp" | grep "200" | wc -l) && \
count_429=$(echo "$resp" | grep "429" | wc -l) && \
echo "200": $count_200, "429": $count_429

You should see the following response, showing that out of the 3 requests, 1 request was successful while the others were rejected:

200: 1, 429: 2

Generate three requests to the route with jane's key:

resp=$(seq 3 | xargs -I{} curl "http://127.0.0.1:9080/anything" -H "Authorization: ${jane_jwt_token}" -o /dev/null -s -w "%{http_code}\n") && \
count_200=$(echo "$resp" | grep "200" | wc -l) && \
count_429=$(echo "$resp" | grep "429" | wc -l) && \
echo "200": $count_200, "429": $count_429

You should see the following response, showing that out of the 3 requests, 2 requests were successful while the other was rejected:

200: 2, 429: 1

Finally, send a request with an invalid key:

curl -i "http://127.0.0.1:9080/anything" -H 'Authorization: somewrongkey'

You should see an HTTP/1.1 401 Unauthorized response with the following message:

{"message":"JWT token invalid"}

Next Steps​

You have now learned how to implement basic authentication. APISIX supports other built-in authentication methods, such as key authentication, basic authentication, and HMAC authentication.