redwood icon indicating copy to clipboard operation
redwood copied to clipboard

[RFC]: Passwordless authentication using Email

Open Olyno opened this issue 1 year ago • 7 comments

Summary

Hi 👋🏻 I was informed that the web auth would be available in the near future (#5680), which I think is great! I would have liked to have a magic link, or an authentication system using emails.

Motivation

Many people are tired of passwords, and with no passwords, it would enhance the security and user experience.

Detailed proposal

We could use nodemailer to send the emails. By default, the emails would be sent locally using maildev, but available in production by specifying SMTP variables.

The dbAuth would have 2 possibilities using CLI flags: using password auth (--password), or passwordless auth (--passwordless).

Are you interested in working on this?

  • [X] I'm interested in working on this

Olyno avatar Aug 10 '22 21:08 Olyno

Thanks @Olyno - assigning to @cannikin to carry on the conversation.

dac09 avatar Aug 11 '22 13:08 dac09

Sorry for the late response! I somehow had notifications turn off for the repo so wasn't getting notified when I got mentions. :(

We do offer Magic.link as an auth option already: https://redwoodjs.com/docs/auth/magic-link But it sounds like you're thinking of adding this functionality natively to dbAuth? Are there any benefits to doing this, besides just skipping another third party integration?

What's the flow when a user clicks on the link in the email? Is it coming to the web side directly, with some process to verify the token? Or going to a serverless function first for verification, and then redirecting to the web side? How would the signup flow need to change, since I assume you must verify your email before continuing?

I've never used maildev before, but I don't know that I personally would want to run a separate local service just to simulate receiving email...could it send actual emails to my actual email address while in development? It sounds like with maildev, you have it running in production, but just relay the email to a real SMTP server? It seems really inefficient to have it be the middle man in production when it's not actually needed? Although maybe you can skip that and use nodemailer directly?

cannikin avatar Sep 08 '22 17:09 cannikin

Sorry if my explanations do not seem clear, I will try to explain again!

Problem

The current problem is that Redwood has (recently) a passwordless system, based on WebAuthn. This method allows anyone to log in without a password using their fingerprint or via facial recognition.

Although WebAuthn is a good start and secure, this method lacks accessibility, both for the developer and the user. Some users still have old devices and are therefore unable to log in with WebAuthn (unless I'm mistaken, in which case feel free to correct me!)

Proposition

The alternative I proposed was a passwordless system, still integrated in Redwood, but based on emails, using Magic link or OTP code.

The advantage of a passwordless system with an OTP code is that the users all have a mailbox, and can check the code on any device, which is not the case with Magic Link. Magic Link logs you in on the device you are on, which is inconvenient when you want to log in on a computer but check your email on a phone.

As you mentioned, Magic Link already offers this service, but I had several problems with it:

  • Inability to store its users in its own database (main reason)
  • Interface not very nice/not smooth
  • Doesn't emphasize passwordless (we have to log in to their site via email/password, which I think is the last straw).

Note: This is only the experience I had.

Additionnal informations

Here is the workflow a user would have with the OTP way:

OTP_workflow

Concerning maildev and nodemail, we can of course bypass maildev if you want, but I hardly see how to integrate a mail system in Redwood without Nodemail.

You can take the example of Supabase, with the rc version adding the OTP part.

Olyno avatar Sep 08 '22 23:09 Olyno

Thanks for the additional info! I have some questions:

  • How are you storing the generated OTP or other identifying information? A new database table? What's the structure of this table look like?
  • How long are the links valid for? Is this time configurable? Where does the configuration go?
  • If using a magic link, where does clicking the link take you to? Is there a new page on the web side, that will communicate back to the server to verify the link?
  • If using an OTP, there must be a new page that you are taken to to enter the OTP? And then submitting that form verifies the password?

Also a note: dbAuth does not use JWTs at all, all session state is maintained in a cookie. This is an HttpOnly cookie, meaning it cannot be read on the browser side—any check of whether the user is logged in involves a round-trip to the server to verify (since only the server is able to read the cookie).

Have you looked at the existing dbAuth code, specifically the DbAuthHandler? This is the class that handles all login/signup/webauth logic, and runs in a serverless function on the api-side. You would need to hook into this to add OTP support. I'd love to hear your plans for what you would need to add/modify to get your proposed solution working!

cannikin avatar Sep 09 '22 17:09 cannikin

How are you storing the generated OTP or other identifying information? A new database table? What's the structure of this table look like?

I think we should make another table, such as:

id email code expiration_date creation_date
1 [email protected] 548954 2022-09-09T22:26.939Z 2022-09-09Y22:26.939Z

Note that each code would be 6 digits long.

How long are the links valid for? Is this time configurable? Where does the configuration go?

The expiration time would be fully configurable using the existing dbAuth configuration. The default time would be 10 to 15 minutes.

If using a magic link, where does clicking the link take you to? Is there a new page on the web side, that will communicate back to the server to verify the link?

I think we could use the OTP code system for the magic link, except that the verification step would be in the link directly. We could add the OTP code directly in query params. So the magic link would look like this:

http://localhost:3000/verify/145974

If using an OTP, there must be a new page that you are taken to to enter the OTP? And then submitting that form verifies the password?

The OTP code system would be in 2 steps:

  • sending the email
  • the verification of the code

The redirections are mainly done on the client side.

I haven't looked at the DbAuthHandler yet, I'll look at that later, thanks for letting me know!

Olyno avatar Sep 09 '22 23:09 Olyno

I believe the table that tracks the OTP code should contain a userId reference to the User table rather than an email (dbAuth refers to this field internally as username, as it doesn't have to be an email, it could be anything). This give you a hard link back to a unique ID for the user that this code is for, rather than an email address which the user can presumably change.

Functions in DbAuthHandler are invoked on the web side by an auth client: https://github.com/redwoodjs/redwood/blob/main/packages/auth/src/authClients/dbAuth.ts The functions getToken, login, signup, etc are a standard set of functions that all auth clients can/should implement. You would need to decide if OTP verification can be handled by one of these existing ones somehow, or if a new one is needed. If new then the function also needs to be exposed via the useAuth() hook so the client can invoke it when needed.

You'll have to look into the CLI setup and generators, as well as updating the docs. Working on the auth system is a big job! 😃

cannikin avatar Sep 09 '22 23:09 cannikin

Well noted, I will see that a little later, thanks!

Olyno avatar Sep 10 '22 15:09 Olyno

I was hoping to setup passwordless so I am amazed to see this issue logged. It's been a few months. @Olyno , did you end up setting this up for your project?

jacebenson avatar Feb 06 '23 03:02 jacebenson

I was waiting for the release of version 4 before continuing this feature (decoupled auth). I will make a new plan asap and post it on this issue to get everyone's opinion

Olyno avatar Feb 06 '23 21:02 Olyno

So I have a ... idea to do this with dbAuth. Custom Auth seems complex and I'm not sure how to set it up. Here's my working draft using dbAuth.

  1. Made component called "LoginPasswordLessForm" that just accepts the username(email)
  2. Made component called "LoginPasswordLessTokenForm" that shows the email, and a password field labeled "Code"
  3. Made a Login page where I create a state for email, and waitingForCode 3.a. Depending on the state of waiting for code i show either the loginpasswordlessform or the token form.
  4. Created a function on my ./src/services/users that will generate a login token. 4.a. This first does a lookup for the user based on the username. If not found returns "request received" 4.b. It creates a random 6 digit code. I'm however going to store it like a password. 4.c. I create a new salt too. 4.d. Then I use CryptoJS.PBKDF2(randNum, salt, {...} to generate the new hashed password 4.e. I update the user with the new password and salt 4.f. Return "request received" 4.g. Send email to user with the code
  5. I add the mutation function to the users.sdl.js with a @skipauth
  6. I update the LoginPasswordLessForm to use the GraphQL call to the mutation function
  7. After login, I'm clearing the salt to break the password on the user.

I'm sure a dedicated ... custom auth setup would be better but @Olyno or anyone, am I missing anything?

jacebenson avatar Feb 07 '23 05:02 jacebenson

I really like your workflow idea, and it seems to be much better than what I had proposed at the beginning! I don't see anything on my side.

Concerning the email sending, I was thinking about creating different adapters (like the different auth systems) so that the developers can choose their smtp server (SendGrid, Mailjet.... or local). What do you think about it ?

Also, you said that you have started an implementation on your side. Would it be possible for you to open a draft pull request and link it to this issue so we can work together on this implementation?

Olyno avatar Feb 07 '23 23:02 Olyno

I dont know how this would play in to dbAuth now that I've done this I think this can exist as just a configuration on dbAuth ya know. For the email stuff I think it might be better just to let folks set up the email integration they want. I'm using Mailgun, but I've used Mailjet, and Sendgrid.

I can't open up a PR and I'm not even sure what I'd make a PR to do unless it was maybe a cookbook on how to configure dbAuth to be Passwordless. I was tlaking about this to a friend and he said the only thing I'm missing is a loginAttempt to disallow brute force attempts.

That all being said, I'll share what I did specifically ina public repo here in a few days.

jacebenson avatar Feb 08 '23 00:02 jacebenson

@Olyno https://github.com/jacebenson/redwood-dbauth-passwordless here's it working with as dbauth

it's a bit of a mess but if you clone it down, try it out, it'll log the logintoken unencrypted to your terminal so you can try it out. You'd need to send an email with that token in production.

jacebenson avatar Feb 15 '23 04:02 jacebenson

Hey @jtoar was this closed with the addition of the how-to because a decision was made to not add first-class pwless support to dbAuth?

If we do want to add that support, I'd be happy to work on it!

arimendelow avatar Sep 03 '23 04:09 arimendelow

I think at the time as it could be handled via configuration, just make a cookbook. I did that. https://github.com/redwoodjs/redwood/pull/7650

jacebenson avatar Sep 05 '23 14:09 jacebenson

Yeah, but it'd be great to have it handled natively :)

With OAuth, it made sense to create a separate plugin, but this feels more appropriate as a first-class solution.

arimendelow avatar Sep 06 '23 02:09 arimendelow