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.
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.
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.