hmac-auth

Description

The hmac-auth Plugin adds HMAC authentication to a Route or a Service.

This Plugin works with a Consumer object and a consumer of your API has to add its key to the request header for verification.

Attributes

NameTypeRequiredDefaultValid valuesDescription
access_keystringTrueUnique key of a Consumer. If different Consumers have the same key, a request matching exception will occur.
secret_keystringTrueUsed in pair with access_key. This field supports saving the value in Secret Manager using the APISIX Secret resource.
algorithmstringFalse“hmac-sha256”[“hmac-sha1”, “hmac-sha256”, “hmac-sha512”]Encryption algorithm used.
clock_skewintegerFalse0Clock skew allowed by the signature in seconds. Setting it to 0 will skip checking the date.
signed_headersarray[string]FalseList of headers to be used in the encryption algorithm. If specified, the client request can only contain the specified headers. When unspecified, all the headers are used in the encryption algorithm.
keep_headersbooleanFalsefalse[ true, false ]When set to true, keeps the request headers X-HMAC-SIGNATURE, X-HMAC-ALGORITHM and X-HMAC-SIGNED-HEADERS in the HTTP request after successful authentication. Otherwise, the headers are removed.
encode_uri_paramsbooleanFalsetrue[ true, false ]When set to true encodes the URI parameters. For example, params1=hello%2Cworld is encoded whereas, params2=hello,world is not.
validate_request_bodybooleanFalsefalse[ true, false ]When set to true, validates the request body.
max_req_bodyintegerFalse512 * 1024Max size of the request body to allow.

NOTE: encrypt_fields = {"secret_key"} is also defined in the schema, which means that the field will be stored encrypted in etcd. See encrypted storage fields.

Enabling the Plugin

First we enable the Plugin on a Consumer object as shown below:

  1. curl http://127.0.0.1:9180/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
  2. {
  3. "username": "jack",
  4. "plugins": {
  5. "hmac-auth": {
  6. "access_key": "user-key",
  7. "secret_key": "my-secret-key",
  8. "clock_skew": 0,
  9. "signed_headers": ["User-Agent", "Accept-Language", "x-custom-a"]
  10. }
  11. }
  12. }'

You can also use the APISIX Dashboard to complete the operation through a web UI.

Next, you can configure the Plugin to a Route or a Service:

  1. curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
  2. {
  3. "uri": "/index.html",
  4. "plugins": {
  5. "hmac-auth": {}
  6. },
  7. "upstream": {
  8. "type": "roundrobin",
  9. "nodes": {
  10. "127.0.0.1:1980": 1
  11. }
  12. }
  13. }'

Example usage

Generating the signature

The formula for calculating the signature is signature = HMAC-SHAx-HEX(secret_key, signing_string).

In order to generate the signature, two parameters, secret_key and signing_string are required. The secret_key is configured by a Consumer and the signing_string is calculated as signing_string = HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string. The different terms in this calculation are explained below:

  • HTTP Method : HTTP request method in uppercase. For example, GET, PUT, POST etc.
  • HTTP URI : HTTP URI. Should start with “/“ and “/“ denotes an empty path.
  • Date : Date in the HTTP header in GMT format.
  • canonical_query_string : The result of encoding the query string in the URL (the string “key1 = value1 & key2 = value2” after the “?” in the URL).
  • signed_headers_string : Concatenation of the specified request headers.
hmac-auth - 图1tip

If any of the terms are missing, they are replaced by an empty string.

The algorithm for generating canonical_query_string is described below:

  1. Extract the query terms from the URL.
  2. Split the query terms into key-value pairs by using & as the separator.
  3. If encode_uri_params is true:
    1. If there are only keys, the conversion formula is uri_encode(key) + "=".
    2. If there are both keys and values, the conversion formula is uri_encode(key) + "=" + uri_encode(value). Here, the value can even be an empty string.
    3. Sort by key in lexicographic order and connect them with & symbol to generate the corresponding canonical_query_string.
  4. If encode_uri_params is false:
    1. If there are only keys, the conversion formula is key + "=".
    2. If there are both keys and values, the conversion formula is key + "=" + value. Here, the value can even be an empty string.
    3. Sort by key in lexicographic order and connect them with & symbol to generate the corresponding canonical_query_string.

And the algorithm for generating the signed_headers_string is as follows:

  1. Obtain the specified headers to add to the calculation from the request header.
  2. Splice the specified headers in name:value format. This is the signed_headers_string.
  1. HeaderKey1 + ":" + HeaderValue1 + "\n"\+
  2. HeaderKey2 + ":" + HeaderValue2 + "\n"\+
  3. ...
  4. HeaderKeyN + ":" + HeaderValueN + "\n"

The example below shows signature string splicing:

  1. curl -i http://127.0.0.1:9080/index.html?name=james&age=36 \
  2. -H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \
  3. -H "x-custom-a: test" \
  4. -H "User-Agent: curl/7.29.0"

Explanation of signature generation formula process

  1. The default HTTP Method for the above request is GET, which gives signing_string as
  1. "GET"
  1. The requested URI is /index.html, and the signing_string is obtained from the HTTP Method + \n + HTTP URI as
  1. "GET
  2. /index.html"
  1. The query item in the URL is name=james&age=36, assuming that encode_uri_params is false. According to the algorithm of canonical_query_string, the focus is on dictionary sorting of key to get age=36&name=james.
  1. "GET
  2. /index.html
  3. age=36&name=james"
  1. The access_key is user-key, and the signing_string is obtained from HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key as
  1. "GET
  2. /index.html
  3. age=36&name=james
  4. user-key"
  1. Date is in GMT format, as in Tue, 19 Jan 2021 11:33:20 GMT, and the signing_string is obtained from the HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date as
  1. "GET
  2. /index.html
  3. age=36&name=james
  4. user-key
  5. Tue, 19 Jan 2021 11:33:20 GMT"
  1. signed_headers_string is used to specify the headers involved in the signature, which in the above example includes User-Agent: curl/7.29.0 and x-custom-a: test.

And the signing_string is obtained from the HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n as

  1. "GET
  2. /index.html
  3. age=36&name=james
  4. user-key
  5. Tue, 19 Jan 2021 11:33:20 GMT
  6. User-Agent:curl/7.29.0
  7. x-custom-a:test
  8. "

The Python code below shows how to generate the signature:

  1. import base64
  2. import hashlib
  3. import hmac
  4. secret = bytes('my-secret-key', 'utf-8')
  5. message = bytes("""GET
  6. /index.html
  7. age=36&name=james
  8. user-key
  9. Tue, 19 Jan 2021 11:33:20 GMT
  10. User-Agent:curl/7.29.0
  11. x-custom-a:test
  12. """, 'utf-8')
  13. hash = hmac.new(secret, message, hashlib.sha256)
  14. # to lowercase base64
  15. print(base64.b64encode(hash.digest()))
TypeHash
SIGNATURE8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg=

You can also refer to Generating HMAC signatures for how to generate signatures for different programming languages.

Validating request body

When the validate_request_body attribute is set to true, the Plugin will calculate the HMAC-SHA value of the request body and checks it again the X-HMAC-DIGEST header:

  1. X-HMAC-DIGEST: base64(hmac-sha(<body>))

If there is no request body, you can set the X-HMAC-DIGEST value to the HMAC-SHA of an empty string.

hmac-auth - 图2note

To calculate the digest of the request body, the Plugin will load the body to memory which can cause high memory consumption if the body is large. To avoid this, you can limit the max allowed body size by configuring max_req_body (default 512KB). Request bodies larger than the set size will be rejected.

Using the generated signature to make requests

You can now use the generated signature to make requests as shown below:

  1. curl -i "http://127.0.0.1:9080/index.html?name=james&age=36" \
  2. -H "X-HMAC-SIGNATURE: 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg=" \
  3. -H "X-HMAC-ALGORITHM: hmac-sha256" \
  4. -H "X-HMAC-ACCESS-KEY: user-key" \
  5. -H "Date: Tue, 19 Jan 2021 11:33:20 GMT" \
  6. -H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \
  7. -H "x-custom-a: test" \
  8. -H "User-Agent: curl/7.29.0"
  1. HTTP/1.1 200 OK
  2. Content-Type: text/html; charset=utf-8
  3. Transfer-Encoding: chunked
  4. Connection: keep-alive
  5. Date: Tue, 19 Jan 2021 11:33:20 GMT
  6. Server: APISIX/2.2
  7. ......

The signature can be put in the Authorization header of the request:

  1. curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + DATE + # + SIGNED_HEADERS' -i
  1. HTTP/1.1 200 OK
  2. Content-Type: text/html
  3. Content-Length: 13175
  4. ...
  5. Accept-Ranges: bytes
  6. <!DOCTYPE html>
  7. <html lang="cn">
  8. ...

Or, the signature can be placed separately in another request header:

  1. curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'Date: DATE' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -H 'X-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' -i
  1. HTTP/1.1 200 OK
  2. Content-Type: text/html
  3. Content-Length: 13175
  4. ...
  5. Accept-Ranges: bytes
  6. <!DOCTYPE html>
  7. <html lang="cn">
hmac-auth - 图3note
  1. If there are multiple signed headers, they must be separated by ;. For example, x-custom-header-a;x-custom-header-b.
  2. SIGNATURE needs to be base64 encoded for encryption.

Using custom header keys

You can use custom header keys for the auth parameters by changing the plugin_attr in your configuration file (conf/config.yaml):

  1. plugin_attr:
  2. hmac-auth:
  3. signature_key: X-APISIX-HMAC-SIGNATURE
  4. algorithm_key: X-APISIX-HMAC-ALGORITHM
  5. date_key: X-APISIX-DATE
  6. access_key: X-APISIX-HMAC-ACCESS-KEY
  7. signed_headers_key: X-APISIX-HMAC-SIGNED-HEADERS
  8. body_digest_key: X-APISIX-HMAC-BODY-DIGEST

Now you can use the new keys while making a request:

  1. curl http://127.0.0.1:9080/index.html \
  2. -H 'X-APISIX-HMAC-SIGNATURE: base64_encode(SIGNATURE)' \
  3. -H 'X-APISIX-HMAC-ALGORITHM: ALGORITHM' \
  4. -H 'X-APISIX-DATE: DATE' \
  5. -H 'X-APISIX-HMAC-ACCESS-KEY: ACCESS_KEY' \
  6. -H 'X-APISIX-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' \
  7. -H 'X-APISIX-HMAC-BODY-DIGEST: BODY_DIGEST' -i
  1. HTTP/1.1 200 OK
  2. Content-Type: text/html
  3. Content-Length: 13175
  4. ...
  5. Accept-Ranges: bytes
  6. <!DOCTYPE html>
  7. <html lang="cn">

Disable Plugin

To disable the hmac-auth Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.

  1. curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
  2. {
  3. "uri": "/index.html",
  4. "plugins": {},
  5. "upstream": {
  6. "type": "roundrobin",
  7. "nodes": {
  8. "127.0.0.1:1980": 1
  9. }
  10. }
  11. }'