wasp icon indicating copy to clipboard operation
wasp copied to clipboard

How best to add emails for use in Auth?

Open shayneczyzewski opened this issue 2 years ago • 3 comments

We currently lack the ability to do email verification and password reset for the EmailAndPassword Auth method. This RFP-lite proposes a direction to solve this.

We could leverage Nodemailer for the underlying ability to send emails using different email providers (e.g., Sendgrid). The assumption when email is used is this would always be an external concern. Wasp would not attempt to spin up any local SMTP server when running locally, due to the complexity and the fact that most emails would not actually likely be delivered anyways due to spam policies.

We could also use nodemailer-express-handlebars to handle the email templating.

Users could enable email-related functionality in Wasp like so (using the Google AuthN PR updated syntax):

app todoApp {
  title: "ToDo App",
  auth: {
    userEntity: User,
    methods: {
      emailAndPassword: {
        emailValidation: {
          emailTemplate: ValidateEmail,
          linkExpiration: "10m" // some Simple Duration string
        },
        passwordReset: {
          // just use all defaults, but can override any like above
        }
      }
    },
    onAuthFailedRedirectTo: "/login"
  },
  email: {
    templatesDir: "@ext/email/templates",
    configFn: import { config } from "@ext/email/config.js",
    deliveryJob: Foo // optional, if they want to send via a job instead of async call
  }
}

emailTemplate ValidateEmail {
  from: "[email protected]",
  subject: "Please validate your email",
  textTemplateName: "validation.txt.handlebars", // assumed under `app.email.templatesDir`
  htmlTempateName: "validation.html.handlebars"
}
export function config() {
  return {
    perform_deliveries: process.env.NODE_ENV === 'production', // optional, if false show in console instead
    smtp_settings: { // required
      host: process.env.SMTP_HOST,
      port: process.env.SMTP_PORT,
      ...
    }
  }
}
<!-- Provided a context containing { user, magicLinkUrl } -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>Please Validate Email</title>
</head>
<body>
  <h2>Hello {{user.name}}!</h2>
  <p>Please click <a href="{{{magicLinkUrl}}}">here</a> to validate your email. Thanks!</p>
</body>
</html>

Email Verification

Signup

When email verification is enabled and a user signs up, should we:

a) create a new user and mark them as email_verified=false, or b) not create a user yet, but once they click the magic link in their email they become a real user (or can sign up), or c) some other more customizable approach?

Approach (a) feels like the most common, however, it always rubbed me slightly the wrong way. What if someone just starts spamming a site creating lots of accounts to be verified? There will be a lot of real (albeit unverified) users created. Should these be cleaned up periodically? Should we reset the password once they verify, so that someone doesn't accidentally click and the bad actor can then log in using the PW they used during signup (or maybe we force them to login to verify email)? Etc. However, it is also the most flexible, allowing the user potentially to control many aspects of what they can and cannot do until they are verified.

Flags

We will have an external model to handle the magic links for email verification and password reset, but how should we handle the email_verified on the User model flag? Should we:

a) require users to add that field to their User model when they enable email verification (similar to what we do for email/password now), or b) create a separate model that contains this?

The downside to a separate model is the dev will need a way still to get this info.

Password Reset

This one is pretty straightforward. We email a magic link to the user email, store that in a separate model, and when they click take them to a screen that allows password reset (within some time interval, say default to 10 min?).

Magic Link Prisma Model

model MagicLink {
  id          Int             @id @default(autoincrement())
  usage       MagicLinkUsage
  token       String          @default(uuid())
  used        Boolean         @default(false)
  user        User            @relation(fields: [userId], references: [id])
  userId      Int
  validUntil  DateTime
  createdAt   DateTime        @default(now()) /* the time we email them it */
  updatedAt   DateTime        @updatedAt
}

enum MagicLinkUsage {
  EMAIL_VALIDATION
  PASSWORD_RESET
}

Open Questions

  • How can we improve emailTemplates?
    • Should we enable emailTemplate previews somehow?
  • Should we allow users to change the default signup/login behavior? For example, what if some devs want to require email to be verified before signup/login complete, but others want to allow login even if not verified? Or should those just have our default implementation and maybe they can override somehow? Or use callbacks at some key points instead?
  • Should we allow for enabling CAPTCHAs or something when signing up?

shayneczyzewski avatar Aug 11 '22 20:08 shayneczyzewski

Conversation recap: Do a bit more research on how other popular libs solve. Wait for sum types? In meantime can just print a warning in validation step. Figure out how this impacts social logins security.

Can we configure current model to username + password instead, leaving notion of "email" to require validation. Maybe we have to first reconsider what emailAndPassword means for us, should it be usernameAndPassword.

Can we separate authentication and verification? What if they want both?

What of we moved to usernameAndPassword only, no need for this PR, and we just connect social in with an external auth field only used for social, and we autogenerate a username?

user: username | password

external auth: google | google_id | user_id external auth: facebook | facebook_id | user_id

shayneczyzewski avatar Aug 18 '22 14:08 shayneczyzewski

This RFC may not be needed anytime soon if we move forward on this: https://github.com/wasp-lang/wasp/pull/696

shayneczyzewski avatar Aug 19 '22 14:08 shayneczyzewski

Update: we renamed EmailAndPassword to UsernameAndPassword. This will give us time to add this new feature in, but not directly tied to auth itself. I will leave this open but we will likely not need to do anything here for a bit (and the issue itself may need updating after this rename).

shayneczyzewski avatar Aug 24 '22 14:08 shayneczyzewski

https://github.com/wasp-lang/wasp/issues/870 -> opened this one, but turned out to be a duplicate.

matijaSos avatar Dec 07 '22 15:12 matijaSos

Related to #891

Martinsos avatar Dec 20 '22 15:12 Martinsos

Closed via https://github.com/wasp-lang/wasp/pull/1087

infomiho avatar Apr 17 '23 13:04 infomiho