Skip to content

Stateless-Passwordless Authentication using Cryptography

Say you want to ensure a user has access to a given email address (or phone number) on the device that they are trying to access your service. This is handy in a number of circumstances, but more importantly, can you authenticate the user without using a database?

It turns out the answer is yes, and fairly straightforward.

Determining the endpoint

The first thing is collecting the contact information from the user. Potentially in the form of a login box. You may also want to collect other parameters, such as a redirect for when the user is successfully authenticated.

Now that we have an endpoint to authenticate the user, we simply need to do a few things.

Generating the secret

At this point, we generate some secret bits (such as a GUIDv4) and construct an ed25519 EdDSA curve from that secret. Finally, sign the endpoint address (aka, email address) and any other information you want, such as a CSRF token. The code might look something like this:

$secret = Uuid::uuid4();
$ec = new EdDSA('ed25519');

$key = $ec->keyFromSecret($secret->getHex()->toString());
return $signature = $ec->sign(unpack('C*', $emailAddress . $csrfToken), $key)->toHex();

At this point, you contact the user with a URL that contains the secret. Since a GUIDv4 isn’t very user-friendly, you can use a word list and generate a mnemonic passphrase the user can use to authenticate as well.

This is also pretty straightforward:

    public static function getPassword(UuidInterface $uuid): string
    {
        $hex = $uuid->getHex()->toString();
        $entropy = self::hex2base2($hex, 128);

        $checksumSize = (int) (strlen($entropy) / 32);
        $hash = hash('sha256', hex2bin($hex), binary: false);
        $checksum = substr(self::hex2base2($hash, 256), 0, $checksumSize);

        $pieces = str_split($entropy.$checksum, 11);

        $wordlist = self::wordlist();

        $password = [];
        foreach ($pieces as $piece) {
            $password[] = $wordlist[(int) bindec($piece)];
        }

        return implode('-', $password);
    }

Upon returning from the request, ensure to store the calculated signature somewhere, either in an HTTP-only cookie or in the API response itself.

Authenticating

Once the user enters the password you generated, or clicks the link, you receive their secret, so now we generate the elliptic curve from the given secret and verify the signature with the sent CSRF token and email address in the final request. If the signature is valid, you have authenticated the user. You can now generate a JWT or some other token for the user as a bearer token.

Overview

Uploads are still broken on my blog, so we’ll have to do this with words :)…

  1. User submits $emailAddress and $csrfToken. $csrfToken is an HTTP-only cookie set by the server, potentially encoding an expiration time. $emailAddress is provided in the request body.
  2. Fail if the $csrfToken is invalid or expired, redirect the user back to try again.
  3. Generate a random $secret with enough entropy so as to not be guessable in the time for which the $csrfToken will still be valid.
  4. Use the $secret to sign the $csrfToken + $emailAddress and generate a $signature.
  5. Send the user the $secret through a side channel and send the $signature back in the response.
  6. The user sends back the $secret, $signature, $emailAddress, and $csrfToken in a request.
  7. Use the $secret to reconstruct the private key and validate the signature.
  8. If it is valid and the $csrfToken is valid, the user is authenticated.

WebAuthN is probably better, but this was fun to try out and may be worth implementing. If you wanted a stateless and passwordless flow, this may be a good candidate.

Authentication is easy to fuck up, so tread carefully.

Odds and Ends

If you want to support redirecting to arbitrary locations, make sure to include that in your signature so it can’t be changed between the time the user submits their email address and the when they click the link in the email. Ensure to validate the redirect goes to a valid location and can’t be used to redirect to arbitrary locations. You don’t want your authentication to be used as an open redirect.

No database required

This is completely stateless authentication requiring no database, which is pretty cool. I’m probably not the first person to think of this, but independently (re)discovering it as a method for authN during my encryption dives last week was pretty fun.

%d bloggers like this: