{"id":17786043,"url":"https://github.com/kemalcr/kemal-hmac","last_synced_at":"2025-03-16T05:31:09.865Z","repository":{"id":257913842,"uuid":"871714067","full_name":"kemalcr/kemal-hmac","owner":"kemalcr","description":"HMAC middleware for Kemal","archived":false,"fork":false,"pushed_at":"2024-10-29T07:46:42.000Z","size":309810,"stargazers_count":12,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-07T17:45:37.003Z","etag":null,"topics":["authentication","crystal-lang","hmac","kemal","shard"],"latest_commit_sha":null,"homepage":"https://kemalcr.github.io/kemal-hmac/","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kemalcr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-12T18:23:39.000Z","updated_at":"2024-11-25T15:02:57.000Z","dependencies_parsed_at":"2024-10-19T04:58:50.179Z","dependency_job_id":null,"html_url":"https://github.com/kemalcr/kemal-hmac","commit_stats":null,"previous_names":["grantbirki/kemal-hmac"],"tags_count":3,"template":false,"template_full_name":"GrantBirki/crystal-base-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemalcr%2Fkemal-hmac","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemalcr%2Fkemal-hmac/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemalcr%2Fkemal-hmac/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemalcr%2Fkemal-hmac/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kemalcr","download_url":"https://codeload.github.com/kemalcr/kemal-hmac/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243806122,"owners_count":20350773,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","crystal-lang","hmac","kemal","shard"],"created_at":"2024-10-27T09:04:12.244Z","updated_at":"2025-03-16T05:31:04.851Z","avatar_url":"https://github.com/kemalcr.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kemal-hmac\n\n[![test](https://github.com/GrantBirki/kemal-hmac/actions/workflows/test.yml/badge.svg)](https://github.com/GrantBirki/kemal-hmac/actions/workflows/test.yml)\n[![build](https://github.com/GrantBirki/kemal-hmac/actions/workflows/build.yml/badge.svg)](https://github.com/GrantBirki/kemal-hmac/actions/workflows/build.yml)\n[![lint](https://github.com/GrantBirki/kemal-hmac/actions/workflows/lint.yml/badge.svg)](https://github.com/GrantBirki/kemal-hmac/actions/workflows/lint.yml)\n[![acceptance](https://github.com/GrantBirki/kemal-hmac/actions/workflows/acceptance.yml/badge.svg)](https://github.com/GrantBirki/kemal-hmac/actions/workflows/acceptance.yml)\n[![docs](https://github.com/GrantBirki/kemal-hmac/actions/workflows/docs.yml/badge.svg)](https://github.com/GrantBirki/kemal-hmac/actions/workflows/docs.yml)\n[![coverage](./docs/assets/coverage.svg)](./docs/assets/coverage.svg)\n\nHMAC middleware for Crystal's [kemal](https://github.com/kemalcr/kemal) framework\n\n## About\n\nWhy should I use HMAC in a client/server system with kemal? Here are some of the benefits:\n\n- **Data Integrity**: HMAC ensures that the data hasn't been tampered with during transit\n- **Authentication**: Verifies the identity of the sender, providing a level of trust in the communication\n- **Keyed Security**: Uses a secret key for hashing, making it more secure than simple hash functions\n- **Protection Against Replay Attacks**: By incorporating timestamps, HMAC helps prevent the replay of old messages\n\nThis readme will be broken up into two parts. The first part will cover how to use the server middleware in a kemal application. The second part will cover how to use the client to communicate with a server that uses the middleware.\n\n## Quick Start ⭐\n\n### Installation\n\nSimply add the shard to your `shard.yml` file:\n\n```yaml\ndependencies:\n  kemal-hmac:\n    github: grantbirki/kemal-hmac\n```\n\n### Basic Example\n\nThe most basic example possible enabling HMAC authentication for all routes in a kemal application:\n\n```crystal\nrequire \"kemal\"\nrequire \"kemal-hmac\"\n\nhmac_auth({\"my_client\" =\u003e [\"my_secret\"]})\n\nget \"/\" do |env|\n  \"Hi, %s! You passed HMAC auth\" % env.kemal_authorized_client?\nend\n\nKemal.run\n```\n\n## Server Usage\n\nFirst, you must require the `kemal-hmac` shard in your kemal application and call it:\n\n```crystal\n# file: hmac_server.cr\nrequire \"kemal\"\nrequire \"kemal-hmac\"\n\n# Initialize the HMAC middleware with the client name that will be sending requests to this server and a secret\n# Note: You can use more than one client name and secret pair. You can also use multiple secrets for the same client name (helps with key rotation)\nhmac_auth({\"my_client\" =\u003e [\"my_secret\"]})\n\n# Now all endpoints are protected with HMAC authentication\n# env.kemal_authorized_client? will return the client name that was used to authenticate the request\nget \"/\" do |env|\n  \"Hi, %s! You sent a request that was successfully verified with HMAC auth\" % env.kemal_authorized_client?\nend\n\n# The `hmac_auth` method also protects websocket routes\nws \"/websocket\" do |socket|\n  socket.send \"HMAC protected websocket route, hooray!\"\n  socket.close\nend\n\nKemal.run\n\n# $ crystal run hmac_server.cr\n# [development] Kemal is ready to lead at http://0.0.0.0:3000\n```\n\nIn a new terminal, you can send a request into the kemal server and verify that HMAC authentication is working:\n\n```crystal\n# file: client_test.cr\nrequire \"kemal-hmac\"  # \u003c-- import the kemal-hmac shard\nrequire \"http/client\" # \u003c-- here we will just use the crystal standard library\n\n# Initialize the HMAC client\nclient = Kemal::Hmac::Client.new(\"my_client\", \"my_secret\")\n\n# Generate the HMAC headers for the desired path\npath = \"/\"\nheaders = HTTP::Headers.new\nclient.generate_headers(path).each do |key, value|\n  headers.add(key, value)\nend\n\n# Make the HTTP request with the generated headers to the server that uses `kemal-hmac` for authentication\nresponse = HTTP::Client.get(\"http://localhost:3000#{path}\", headers)\n\n# Handle the response\nif response.status_code == 200\n  puts \"Success: #{response.body}\"\nelse\n  puts \"Error: #{response.status_code}\"\nend\n\n# $ crystal run client_test.cr\n# Success: Hi, my_client! You sent a request that was successfully verified with HMAC auth\n```\n\n### Authentication for specific routes\n\nThe `Kemal::Hmac::Handler` inherits from `Kemal::Handler` and it is therefore easy to create a custom handler that adds HMAC authentication to specific routes instead of all routes.\n\n```crystal\n# file: hmac_server.cr\nrequire \"kemal\"\nrequire \"kemal-hmac\"\n\nclass CustomAuthHandler \u003c Kemal::Hmac::Handler\n  only [\"/admin\", \"/api\"] # \u003c-- only protect the /admin and /api routes\n  \n  def call(context)\n    return call_next(context) unless only_match?(context)\n    super\n  end\nend\n\n# Initialize the HMAC middleware with the custom handler\nKemal.config.hmac_handler = CustomAuthHandler\nadd_handler CustomAuthHandler.new({\"my_client\" =\u003e [\"my_secret\"]})\n\n# The root (/) endpoint is not protected by HMAC authentication in this example\nget \"/\" do |env|\n  \"hello world\"\nend\n\n# The /admin endpoint is protected by HMAC authentication in this example\nget \"/admin\" do |env|\n  \"Hi, %s! You sent a request that was successfully verified with HMAC auth to the /admin endpoint\" % env.kemal_authorized_client?\nend\n\nKemal.run\n\n# $ crystal run hmac_server.cr\n# [development] Kemal is ready to lead at http://0.0.0.0:3000\n```\n\n### `kemal_authorized_client?`\n\nWhen a request is made to a protected route, the `kemal_authorized_client?` method is available on the `env` object. This method returns the client name that was used to authenticate the request if the request was successfully verified with HMAC authentication. Otherwise, it returns `nil`.\n\n```crystal\nget \"/admin\" do |env|\n  \"Hi, %s! You sent a request that was successfully verified with HMAC auth\" % env.kemal_authorized_client?\nend\n```\n\n### Environment Variable Configuration\n\nThe `kemal-hmac` server middleware can be configured completely through environment variables. For example, if you had the following environment variables set:\n\n```bash\nexport MY_CLIENT_HMAC_SECRET_BLUE=\"my_secret_1\"\nexport MY_CLIENT_HMAC_SECRET_GREEN=\"my_secret_2\"\n```\n\nThen simply calling `hmac_auth(enable_env_lookup: true)` in your kemal application will automatically configure the middleware with the client names and secrets from the environment variables. Here is how it works:\n\n1. When the `hmac_auth()` method is called with the `enable_env_lookup: true` argument, the middleware will look for environment variables that start with the client name in all caps and end with `HMAC_SECRET_BLUE` or `HMAC_SECRET_GREEN` (these are called the `HMAC_KEY_SUFFIX_LIST` and can be further configured with environment variables as well). For example, if the client name is `my_client`, the middleware will look for an environment variable called `MY_CLIENT_HMAC_SECRET_BLUE` or `MY_CLIENT_HMAC_SECRET_GREEN`.\n2. If one or more matching secrets are found for the client name, the middleware will be configured with the client name and the secrets.\n3. The client name and secrets will be used to generate the HMAC token for incoming requests.\n4. The first matching secret for the client that successfully generates a valid HMAC token will be used to authenticate the request.\n\nHere is an example:\n\n```crystal\n# file: hmac_server.cr\nrequire \"kemal\"\nrequire \"kemal-hmac\"\n\n# Initialize the HMAC middleware with the 'enable_env_lookup: true' param so it can self-hydrate from the environment variables\nhmac_auth(enable_env_lookup: true)\n\n# Now all endpoints are protected with HMAC authentication\nget \"/\" do |env|\n  \"Hi, %s! You sent a request that was successfully verified with HMAC auth using environment variables\" % env.kemal_authorized_client?\nend\n```\n\n\u003e Note: The `enable_env_lookup: true` argument is optional and defaults to `false`. If you do not pass this argument, you will need to pass the `hmac_secrets` argument to the `hmac_auth` method to configure the middleware. This is the desired way to configure the middleware in production as it is more explicit, less error-prone, and performs significantly better than using environment variables.\n\n## Configuration\n\nThis section goes into detail on the configuration options available for the `kemal-hmac` middleware and the client utility.\n\n### Global Environment Variables\n\nThese environment variables can be set globally for the `kemal-hmac` middleware and the client utility to change the default behavior.\n\n| Environment Variable | Default Value | Description |\n| -------------------- | ------------- | ----------- |\n| `HMAC_CLIENT_HEADER` | `hmac-client` | The name of the header that contains the client name |\n| `HMAC_TIMESTAMP_HEADER` | `hmac-timestamp` | The name of the header that contains the iso8601 timestamp |\n| `HMAC_TOKEN_HEADER` | `hmac-token` | The name of the header that contains the HMAC token |\n| `HMAC_TIMESTAMP_SECOND_WINDOW` | `30` | The number of seconds before and after the current time that a timestamp is considered valid - helps with clock drift |\n| `HMAC_REJECTED_CODE` | `401` | The status code to return when a request is rejected |\n| `HMAC_REJECTED_MESSAGE_PREFIX` | `Unauthorized:` | The prefix to add to the response body when a request is rejected |\n| `HMAC_KEY_SUFFIX_LIST` | `HMAC_SECRET_BLUE,HMAC_SECRET_GREEN` | A comma-separated list of key suffixes to use for looking up secrets in the environment. Using a blue/green pattern is best for key rotation |\n| `HMAC_KEY_DELIMITER` | `_` | The delimiter to use for separating the client name from the key suffix in the environment variable name |\n| `HMAC_ALGORITHM` | `SHA256` | The algorithm to use for generating the HMAC token. See [here](./src/kemal-hmac/hmac_algorithm.cr) for all supported algorithms |\n\n### Direct Middleware Configuration\n\nPassing in configuration options directly to the `hmac_auth` method is the most explicit way to configure the `kemal-hmac` middleware and these options take precedence over the environment variables.\n\n```crystal\n# A very verbose example of how to configure the middleware\n# file: hmac_server.cr\n\nrequire \"kemal\"\nrequire \"kemal-hmac\"\n\nhmac_auth(\n  hmac_secrets: {\"my_client\" =\u003e [\"my_secret_blue\", \"my_secret_green\"], \"my_other_client\" =\u003e [\"my_other_secret\"]},\n  hmac_client_header: \"hmac-client\",\n  hmac_timestamp_header: \"hmac-timestamp\",\n  hmac_token_header: \"hmac-token\",\n  timestamp_second_window: 30,\n  rejected_code: 401,\n  rejected_message_prefix: \"Unauthorized:\",\n  hmac_key_suffix_list: [\"HMAC_SECRET_BLUE\", \"HMAC_SECRET_GREEN\"],\n  hmac_key_delimiter: \"_\",\n  hmac_algorithm: \"SHA256\",\n  enable_env_lookup: false\n)\n\n# ... kemal logic here\n```\n\n## Client Usage\n\nThe `Kemal::Hmac::Client` class is designed to facilitate making HTTP requests to a remote server that uses HMAC (Hash-based Message Authentication Code) authentication implemented by this same shard. This class helps generate the necessary HMAC headers required for authenticating requests.\n\nHere are some examples of the relevant headers that are generated by the `Kemal::Hmac::Client` class:\n\n```ini\nhmac-client = \"client-name-sending-request-to-the-server\"\nhmac-timestamp = \"2024-10-15T05:10:36Z\"\nhmac-token = \"LongHashHere\n```\n\n### Initialization\n\nTo initialize the `Kemal::Hmac::Client` class, you need to provide the client name, secret, and optionally, the algorithm used to generate the HMAC token. The default algorithm is SHA256.\n\n```crystal\nrequire \"kemal-hmac\"\n\nclient = Kemal::Hmac::Client.new(\"my_client\", \"my_secret\")\n```\n\nYou can also specify a different algorithm:\n\n```crystal\nrequire \"kemal-hmac\"\n\nclient = Kemal::Hmac::Client.new(\"my_client\", \"my_secret\", \"SHA512\")\n```\n\n### Generating HMAC Headers\n\nThe generate_headers method generates the necessary HMAC headers for a given HTTP path. These headers can then be included in your HTTP request to the server.\n\n```crystal\nrequire \"kemal-hmac\"\n\nclient = Kemal::Hmac::Client.new(\"my_client\", \"my_secret\")\nhmac_headers = client.generate_headers(\"/api/path\")\n```\n\n### Example: Making an HTTP Request\n\nHere is a complete example of how to use the `Kemal::Hmac::Client` class to make an HTTP request to a remote server that uses `kemal-hmac` for authentication.\n\n```crystal\n# Example using crystal's standard library for making HTTP requests with \"http/client\"\n\nrequire \"kemal-hmac\" # \u003c-- import the kemal-hmac shard\nrequire \"http/client\" # \u003c-- here we will just use the crystal standard library\n\n# Initialize the HMAC client\nclient = Kemal::Hmac::Client.new(\"my_client\", \"my_secret\")\n\n# Generate the HMAC headers for the desired path\npath = \"/\" # \u003c-- can be any request path you like\nheaders = HTTP::Headers.new\n# loop over the generated headers and add them to the HTTP headers\nclient.generate_headers(path).each do |key, value|\n  headers.add(key, value)\nend\n\n# Make the HTTP request with the generated headers to the server that uses `kemal-hmac` for authentication\nresponse = HTTP::Client.get(\"https://example.com#{path}\", headers: headers)\n\n# Handle the response\nif response.status_code == 200\n  puts \"Success: #{response.body}\"\nelse\n  puts \"Error: #{response.status_code}\"\nend\n```\n\n### Example: Making an HTTP Request with the `crest` shard\n\nHere is a complete example of how to use the `Kemal::Hmac::Client` class to make an HTTP request to a remote server that uses `kemal-hmac` for authentication. This example uses the popular `crest` library for making HTTP requests.\n\n```crystal\n# Example using the popular `crest` library for making HTTP requests\n\nrequire \"kemal-hmac\" # \u003c-- import the kemal-hmac shard\nrequire \"crest\"      # \u003c-- here we will use the popular `crest` library\n\n# Initialize the HMAC client\nclient = Kemal::Hmac::Client.new(\"my_client\", \"my_secret\")\n\npath = \"/\"\n\n# Make the HTTP request with the generated headers to the server that uses `kemal-hmac` for authentication (using the `crest` library)\nresponse = Crest.get(\n  \"http://localhost:3000#{path}\",\n  headers: client.generate_headers(path)\n)\n\n# Handle the response\nif response.status_code == 200\n  puts \"Success: #{response.body}\"\nelse\n  puts \"Error: #{response.status_code}\"\nend\n```\n\n## Generating an HMAC secret\n\nTo generate an HMAC secret, you can use the following command for convenience:\n\n```bash\nopenssl rand -hex 32\n```\n\n## Benchmarks ⚡\n\n**TL;DR**: The `kemal-hmac` middleware has a minimal impact on the performance of a kemal application.\n\nRunning `kemal` with the `kemal-hmac` middleware results in an extra `0.14ms` of latency per request on average.\n\nWhereas running Ruby + Sinatra + Puma results in an extra `118ms` of latency per request on average.\n\n[![rps](./docs/assets/rps.png)](./docs/assets/rps.png)\n\n## kemal + kemal-hmac\n\n```shell\n$ wrk -c 100 -d 40 -H \"hmac-client: my_client\" -H \"hmac-timestamp: 2024-10-15T22:01:46Z\" -H \"hmac-token: 5b1d59098a2cccfb6e68bfea32dee4c19ae6bbd816d79285fbce3add5f2590d1\" http://localhost:3000/applications/123/tokens/123\nRunning 40s test @ http://localhost:3000/applications/123/tokens/123\n  2 threads and 100 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency     1.14ms  426.66us  15.60ms   98.16%\n    Req/Sec    44.71k     3.15k   55.55k    67.75%\n  3559413 requests in 40.01s, 492.21MB read\nRequests/sec:  88965.26\nTransfer/sec:     12.30MB\n```\n\n## kemal without kemal-hmac\n\n```shell\n$ wrk -c 100 -d 40 http://localhost:3000/applications/123/tokens/123\nRunning 40s test @ http://localhost:3000/applications/123/tokens/123\n  2 threads and 100 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency     1.00ms  409.37us  10.66ms   97.56%\n    Req/Sec    51.30k     4.63k   66.11k    72.62%\n  4084149 requests in 40.01s, 564.77MB read\nRequests/sec: 102080.95\nTransfer/sec:     14.12MB\n```\n\n## Ruby with Sinatra + Puma\n\n```shell\n$ wrk -c 100 -d 40 http://localhost:3000/applications/123/tokens/123\nRunning 40s test @ http://localhost:3000/applications/123/tokens/123\n  2 threads and 100 connections\n  Thread Stats   Avg      Stdev     Max   +/- Stdev\n    Latency   119.23ms  152.42ms 582.52ms   78.86%\n    Req/Sec     3.53k     1.00k    5.73k    75.50%\n  280940 requests in 40.07s, 46.24MB read\nRequests/sec:   7010.87\nTransfer/sec:      1.15MB\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkemalcr%2Fkemal-hmac","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkemalcr%2Fkemal-hmac","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkemalcr%2Fkemal-hmac/lists"}