Skip to content

Instantly share code, notes, and snippets.

@TJC
Last active October 3, 2022 21:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TJC/291ed03160facd19dcdbcd31fbb9f7e4 to your computer and use it in GitHub Desktop.
Save TJC/291ed03160facd19dcdbcd31fbb9f7e4 to your computer and use it in GitHub Desktop.
(Simplified) HTTP Basic auth vs MAC signed auth

Client:

username = "alice"
secretKey = "12345"
method = GET
url = "https://example.com/private"

Client sends what it essentially this HTTP request:
method = GET
URL = "https://example.com/private"
header["Basic"] = "alice:12345"

Server:

It receives:
method = GET
URL = "https://example.com/private"
header["Basic"] = (alice,12345)

It does:
(username, password) = header["Basic"]

Then:
if (password == datastore.get_user(username).password) authorised!

Problems with this method:

  • Username and password are sent with every request in plaintext.
  • Request is encrypted with TLS due to the HTTPS, but this isn't super trustworthy encryption.
  • If someone gets hold of the plaintext HTTP request, they now have the password and can make unlimited requests with it.

These examples are simplified -- in the real world it's slightly more complicated.

For the Basic auth, the username and password are base64 encoded - this is just to avoid issues with unicode or whitespace, and doesn't provide any encryption.

For the MAC signed auth, it normally includes a subset of HTTP headers in the creation of the "canonical request". For instance, you might want to include "X-Account-Id", if that was an important part of the command being sent, as otherwise an attacker could copy and replay a request, but change the account number.

Client:

username = "alice"
secretKey = "12345"
method = GET
url = "https://example.com/private"

Client computes this one-way checksum:
canonical_request = method + url + secretKey + current_time_seconds()
# canonical_request = "GET https://example.com/private 12345 1581993909"

checksum = sha256(canonical_request)
# checksum = 12142bdc5beb4847372000a16de3c61a606d7560d487b9b60638092207c1c1f0

Client sends what it essentially this HTTP request:
method = GET
URL = "https://example.com/private"
header["Time"] = current_time_seconds
header["Bearer"] = "12142bdc5beb4847372000a16de3c61a606d7560d487b9b60638092207c1c1f0"
header["User"] = "alice"

Server:

It receives:
method = GET
URL = "https://example.com/private"
header["Time"] = current_time_seconds()
header["Bearer"] = "12142bdc5beb4847372000a16de3c61a606d7560d487b9b60638092207c1c1f0"
header["User"] = "alice"


It checks that the claimed request time is pretty close to the real time.
(This is to prevent replay attacks)
their_time = header["Time"]
my_time = current_time_seconds()
if (abs(my_time - their_time) > 30) reject!

Then it can fetch the secret key from it's datastore:
secretKey = datastore.get_user(header["user"]).secretKey

Then it recreates the "canonical request" in the same way as the client:
canonical_request = method + url + secretKey + their_time
# canonical_request = "GET https://example.com/private 12345 1581993909"
checksum = sha256(canonical_request)
# checksum = 12142bdc5beb4847372000a16de3c61a606d7560d487b9b60638092207c1c1f0

Now the server compares the calculated checksum with the one the client sent us:
if (checksum == header["Bearer"]) authorised!

Advantages of this method:

  • The secretkey is never transmitted over the wire
  • Even if someone gets hold of the plaintext version of the request, they can't use it to create more requests.
  • However they can replay (re-send) the same request, during a small time window.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment