Implement HMAC Authentication
HMAC authentication signs selected request components with a shared secret. APISIX recalculates the signature to verify that the caller holds the secret and that the signed components were not modified. The request date limits how old an accepted signature can be, but it does not prevent replay within the accepted clock-skew window. Because the client and gateway share the signing secret, HMAC authentication does not provide non-repudiation.
In this guide, you will implement a scenario where there are two consumers using HMAC 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 generate current request signatures.
- If you use the Admin API examples and authentication is enabled, export an admin-role key as
ADMIN_API_KEY. Configure the key underdeployment.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 hmac-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-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "john-key",
"secret_key": "john-secret-key"
}
}
}'
Create hmac-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-hmac-auth",
"plugins": {
"hmac-auth": {
"key_id": "jane-key",
"secret_key": "jane-secret-key"
}
}
}'
Create a Route
Create a route and enable hmac-auth:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "hmac-auth-route",
"uri": "/anything",
"methods": ["GET"],
"plugins": {
"hmac-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
Create a Signature
Generate a current Date and matching HMAC-SHA256 signature for each consumer. The signed path must match the requested path, including any query string. Save this script as hmac-sig-header-gen.py:
import base64
import hashlib
import hmac
import shlex
from email.utils import formatdate
for prefix, key_id, secret in [
("JOHN", "john-key", "john-secret-key"),
("JANE", "jane-key", "jane-secret-key"),
]:
date = formatdate(usegmt=True)
signing_string = f"{key_id}\nGET /anything\ndate: {date}\n"
digest = hmac.new(secret.encode(), signing_string.encode(), hashlib.sha256).digest()
signature = base64.b64encode(digest).decode()
authorization = (
f'Signature keyId="{key_id}",algorithm="hmac-sha256",'
f'headers="@request-target date",signature="{signature}"'
)
print(f"export {prefix}_HMAC_DATE={shlex.quote(date)}")
print(f"export {prefix}_HMAC_AUTH={shlex.quote(authorization)}")
Load the generated headers into the current shell:
eval "$(python3 hmac-sig-header-gen.py)"
The default clock-skew window is 300 seconds. Run the script again if the headers become too old, and keep each Date paired with the signature generated for it. The timestamp bounds request age; it does not prevent replay within that accepted window.
Verify
Using the headers generated, send a request to the route using johndoe's credential:
curl -X GET "http://127.0.0.1:9080/anything" \
-H "Date: ${JOHN_HMAC_DATE}" \
-H "Authorization: ${JOHN_HMAC_AUTH}"
You should see an HTTP/1.1 200 OK response similar to the following:
{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
...
"X-Consumer-Username": "johndoe",
"X-Credential-Identifier": "cred-john-hmac-auth",
"X-Consumer-Custom-Id": "john-doe-junior",
"X-Forwarded-Host": "127.0.0.1"
},
...
}
Wait for the current 30-second quota window to expire, then send three requests with John's key:
sleep 31
resp=$(seq 3 | xargs -I{} curl -X GET "http://127.0.0.1:9080/anything" \
-H "Date: ${JOHN_HMAC_DATE}" \
-H "Authorization: ${JOHN_HMAC_AUTH}" \
-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 credential:
resp=$(seq 3 | xargs -I{} curl -X GET "http://127.0.0.1:9080/anything" \
-H "Date: ${JANE_HMAC_DATE}" \
-H "Authorization: ${JANE_HMAC_AUTH}" \
-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, regenerate the headers if needed, then send a well-formed Authorization header with an intentionally incorrect signature:
curl -X GET "http://127.0.0.1:9080/anything" \
-H "Date: ${JOHN_HMAC_DATE}" \
-H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="AAAA"'
You should see an HTTP/1.1 401 Unauthorized response with a message similar to the following:
{"message":"client request can't be validated"}
Next Steps
You have now learned how to implement HMAC authentication. APISIX supports other built-in authentication methods, such as key authentication, basic authentication, and JWT authentication.