Feature Request: For Passwordless implementation in mix gen auth, make it a magic code and not a link

Please change passwordless login from a link to a 6-8 digit code a user can enter.

Gen Auth in the latest RC 1.8 of Phoenix defaults to passwordless magic links where you click a link in an eamil.

Problem with magic link logins: Mobile email apps (looking at you Gmail) now embed their own web browser in such a way that when you click on a link in the email, it will log you into the embedded browser but NOT the user’s browser. So once the user closes the embedded browser in say the Gmail app, your login goes away.

The solution: Most passwordless logins have shifted to using a code (6-8 digits). The user then copies the code from the email and pastes into the web browser.

Example of this: We wrote two versions of this at https://www.rivalsee.com (which is an AI SEO tracker written in Elixir/Liveview) and https://www.tellmel.ai (an AI biographer written in Elixir/Liveview :slight_smile:

In both cases we implemented our own magic login code on top of Phoenix 1.8’s passwordless magic link login.

The request is to have the magic code be the default as it is becoming the standard everywhere because of the mobile app browser problem.

Thanks!

3 Likes

I will also add that some versions of outlook do some weird base64 encoding on the magic links so the login link functionality does not work with those browsers. it’s very frustrating.

1 Like

Hm, for my signup flow I offer the user both a link and code and then validate the original session if the user visits the link on another device.

But for magic links you are also using this flow for auth after account creation, so that results in a serious risk of hijacking by spamming users hoping they click the link and validate an attacker’s session. I assume that’s the underlying reason for this pattern?

I have to say, 6-8 numeric digits does not seem like very much entropy for a login mechanism. If this is implemented the endpoints will have to be rate-limited very carefully.

1 Like

I agree - If you allow the magic link to validate the original session you have a big risk of hi-jacking. If it is limited to just the session/browser where it is clicked, I am not sure how hijacking can happen?

As for codes - they do need to be rate limited - yes. But it’s not that hard. You can just invalidate the code after X attempts and lock the account for Y minutes after Z attempts. Also the code obviously is only temporary which helps.

Even if you rotate the code on every attempt the probability of randomly guessing a 6-digit numeric code is 1 / 1,000,000, so it would take on average 1 million requests to compromise an account. If on the other hand you never rotate the code and brute-force every possible code in order, it would take on average 500,000 requests to compromise an account. In other words, rotating the code does not buy you much.

This is rate-limiting, yes, but I disagree that it’s “not that hard”. The problem is that with such low-entropy codes the rate-limits go from “defense in depth” to holding up the security of the entire account system. If there is any bug in your rate-limiter then any account is trivially brute-forced. So you have to be very careful, which was my point.

As an example, say you limit each account to 1 attempt per hour. Now say you have 1 million users. All someone has to do is try every user over the course of each hour and they will on average get into one account per hour! So you have to rate-limit globally as well.

If this feature is being proposed for Phoenix all of this has to be carefully considered. Phoenix does not have any rate-limiting solution, so something will have to be built or added. If you pull in a dependency for rate-limiting (Hammer is one) then that dependency is now holding up the security of the entire auth stack, and has to be carefully audited for bugs.

Alternatively you could user higher entropy codes at the cost of the user experience. For a framework like Phoenix which has to cater to everyone, that might be a better path to go down.

2 Likes

Agree with everything, I would add:

Even if you rotate the code on every attempt the probability of randomly guessing a 6-digit numeric code is 1 / 1,000,000

It not taking account timing attacks that may make it even faster to crack a code if implementation is weak.

1 Like

A good point in general, though strangely in this case the token is so small that it trivially fits into an int32 so I imagine the (integer) comparison would take place in a single CPU cycle. You never know, though, easier to hash anyway than worry about it.

Another reason to hash the tokens is to protect against a database compromise, but that doesn’t help here because the tokens have too little entropy for the hash to be effective.

1 Like

Also note that in this case there is so little entropy that a crypto-hash alone is not enough to prevent the timing attack for a non-constant comparison because there are so few values that you can just build e.g. a sha256 lookup table (32MB for 6 digits) and then do the timing attack against it, reversing the token back out.

I guess if you salted the hashes that would work? Ensuring the comparison is constant-time would be sufficient here, though, as long as you don’t use the token as the lookup key in the DB. Which you wouldn’t here, I think.

This stuff is never easy!

2 Likes

Username/password are still king, if anything it should have been passkeys. :man_shrugging:

3 Likes

I still haven’t caught up on passkeys. Apropos, what are they really? (Thought it’s relevant to the discussion here.)

It’s webauthn Passkey - Wikipedia

It’s part of the “Web authentication API” (webauthn) and uses the PublicKeyCredential interface available in the browser. Those APIs can be used for various things and support a number of options, none of which are called “passkeys,” making it confusing if you are just getting started in trying to implement passkeys, residentKey is what you’ll actually see in code and the documentation will also refer to them as “discoverable keys.” How tou use those browser APIs also depends on how you want your authentication process to flow: do you want the device to potentially trigger the passkey UI before you know who they are (this is what is meant by discoverable), or do you want the user to first provide a username/email and then trigger the passkey UI once you know your system has their public key.

The gist is that passkeys are private/public key pairs generated on the user’s device (or in something like 1password) via the JavaScript API, the private key is used to sign a challenge (random bytes previously generated on the server) on the device and the public key is used to verify the signature on the server.

There are two flows: creating a passkey and authenticating with a passkey. In both flows the server first generates and stores the challenge, sends the challenge to the client, which is included in the webauthn API call. The result of the creation API includes public key which needs to be stored by the server for future authentication flows. The result of the authentication API includes a signature of the challenge which the server needs to verify using the previously stored public key.

I recently spiked adding passkeys to what’s generated by phx.gen.auth in Phoenix 1.8 RC: First pass as passkeys · jswanner/phx-passkey-spike@dd88cf9 · GitHub

7 Likes