Skip to content

Instantly share code, notes, and snippets.

@nz
Last active October 3, 2023 07:46
Show Gist options
  • Save nz/78c96f543f345d43d3d1 to your computer and use it in GitHub Desktop.
Save nz/78c96f543f345d43d3d1 to your computer and use it in GitHub Desktop.
Light weight HMAC token auth over HTTP Basic Auth

HMAC over Basic Auth

This is a pattern I use fairly frequently for administrative APIs. It's a sort of OAuth lite for non-public APIs that produces good quality tokens. Once you build it a few times, it's not any harder than using arbitrary basic auth in your APIs.

The client and the app share a secret, which is never transmitted across the wire. The client uses this secret to create an HMAC digest of a payload consisting of the current time and a random nonce value. The nonce is provided as the Basic Authorization user, and the resulting HMAC digest is provided as the Basic Authorization password.

A similar process is followed on the server side. The server uses the supplied nonce, its own time, and its own copy of the shared secret. It may want to check against several tokens across a small window of times to account for clock drift.

  • Using HMAC means the secret is never transmitted across the wire. Theoretically these are safe across plaintext connections, but you're using TLS anyway, right?
  • The inclusion of the current time ensure that all generated tokens expire automatically, helping to reduce the window for replay attacks.
  • The nonce gives HMAC a bit more randomness to digest, helping to prevent a rainbow table attack on the generated tokens.
  • The nonce completely eliminates replay attacks if the server takes the additional step of enforce its uniqueness for the allowed time window.

Client side

secret = ENV['API_SECRET']

time  = Time.now.to_i / 10
nonce = rand(36**10).to_s(36)
token = OpenSSL::HMAC.hexdigest('sha256', secret, "#{nonce}#{time}")
auth  = Base64.encode64("#{nonce}:#{token}").delete("\r\n")

headers['Authorization'] = "Basic #{auth}"

We generate the HMAC token using a shared secret, arbitrary random nonce, and the current time in seconds, divided by 10 to reduce the precision a bit. We use the nonce as the HTTP Basic user and the token as the password, encoding them with Base64 and then using them for a Basic Authorization header in our HTTP client of choice.

Server side

secret = ENV['API_SECRET']
authenticate_or_request_basic_auth do |nonce, token|
  time = Time.now.to_i / 10
  [ time - 1, time, time + 1 ].any? do |t|
    # enforce nonce uniqueness for this time - a bloom filter would do nicely
    token == OpenSSL::HMAC.hexdigest('sha256', secret, "#{nonce}#{t}")
  end
end

Given the client's nonce and HMAC token, the server generates a handful of its own tokens, across a short window of times. In this case, we're effectively rounding to 10 second intervals, then checking three such intervals, producing a valid time window of 30 seconds. Enforcing strict nonce uniqueness is left as an exercise to the reader.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment