supabase / auth

A JWT based API for managing users and issuing JWT tokens
https://supabase.com/docs/guides/auth
MIT License
1.54k stars 373 forks source link

Generate custom link for change email confirmation #361

Closed kangmingtay closed 2 years ago

kangmingtay commented 2 years ago

Discussed in https://github.com/supabase/supabase/discussions/5125

Originally posted by **haydn** January 24, 2022 We're generating all our own emails and injecting links created with the `generateLink()` function. (This is so we can fully personalise the emails and so they match the other emails we send out.) However, I'm not sure how to get this working for the confirmation emails sent when a user's email address is changed. It seems like those emails are only ever sent as a side-effect of changing a user's email with `updateUser()` and the only way to customise those emails is via config. So, my questions are: 1. How do we generate the custom links for email change confirmations? 2. How do we prevent the default emails from being sent out when a user is updated?
haydn commented 2 years ago

@kangmingtay Thanks for getting this onto the backlog. I've put some context and ideas below.

Our use case

We (@WorkniceHR) are using the POST /admin/generate_link endpoint (via the gotrue-js client) to populate links in custom emails we send out when inviting users, resetting passwords, confirming account creation etc. The templates for these emails are part of our codebase (not configuration) so that we can fully personalise them with the user's name, organisation, branding etc.

The one scenario where we can't send out fully custom emails is when a user updates their email address and need to confirm their old and new addresses. We'd like to be able to use the POST /admin/generate_link endpoint (or similar) for this too instead of relying on configured email templates.

Problems to solve

Disable the automatically sent emails

The confirmation emails are currently sent out as a side-effect of editing the user. If we're generating our own link and sending them ourselves we don't won't confirmations email getting sent automatically.

This change seems fairly straightforward. Either a config variable can be added (something like MAILER_DISABLE_AUTO_CONFIRM_ON_EMAIL_CHANGE) or an options field can be added to the PUT /user endpoint:

# PUT /user
{
  "email": "new-email@example.com",
  "options": {
    "sendConfirmationEmails": false,
  }
}

I think I prefer the addition of an option field as it feels closer to the existing API design.

Generate links for email confirmation

The exisiting POST /admin/generate_link endpoint doesn't lend itself naturally to this use. The biggest difference seems to be the need to generate multiple links (one to confirm the old address and one for the new address). I'm not familiar enough with GoTrue to understand all the trade-offs, but intuitively it feels like a separate PUT /user/email endpoint for changing email addresses makes sense (and can be backwards compatible).

kangmingtay commented 2 years ago

For anyone who's keen on attempting this, else we'll get to fixing / adding this soon:

  1. Need an additional type=email_change for the POST /admin/generate_link endpoint to allow developers to generate their own email change links a. Need to include the secure option where 2 links are generated (one for the old email, and one for the new email)
  2. PUT /user/email should accept a query param (shouldSendEmail) to allow disabling sending of email confirmation links if the email is updated. (Note that this doesn't imply that the user should be automatically confirmed - it just prevents gotrue from sending out the email confirmation link and (1) needs to be carried out else the user's new email will remain unconfirmed)
mustafaTokmak commented 2 years ago

For anyone who's keen on attempting this, else we'll get to fixing / adding this soon:

  1. Need an additional type=email_change for the POST /admin/generate_link endpoint to allow developers to generate their own email change links a. Need to include the secure option where 2 links are generated (one for the old email, and one for the new email)
  2. PUT /user/email should accept a query param (shouldSendEmail) to allow disabling sending of email confirmation links if the email is updated. (Note that this doesn't imply that the user should be automatically confirmed - it just prevents gotrue from sending out the email confirmation link and (1) needs to be carried out else the user's new email will remain unconfirmed)

I opened a PR(#365) for this issue, but that is not exactly like this, but I will modify again.

mustafaTokmak commented 2 years ago

PR is ready for review

For anyone who's keen on attempting this, else we'll get to fixing / adding this soon:

  1. Need an additional type=email_change for the POST /admin/generate_link endpoint to allow developers to generate their own email change links a. Need to include the secure option where 2 links are generated (one for the old email, and one for the new email)
  2. PUT /user/email should accept a query param (shouldSendEmail) to allow disabling sending of email confirmation links if the email is updated. (Note that this doesn't imply that the user should be automatically confirmed - it just prevents gotrue from sending out the email confirmation link and (1) needs to be carried out else the user's new email will remain unconfirmed)

I opened a PR(#365) for this issue, but that is not exactly like this, but I will modify again.

hf commented 2 years ago

Hey support for this was added in #560.

ForgeSolutions-JBrenner commented 2 years ago

@haydn not sure if this is the right place for this but what is the email provider / setup that you are using to do this? I would love to do the same.

haydn commented 2 years ago

Hey @ForgeSolutions-JBrenner, no problem!

It's a Next.js app hosted on Vercel. We're using Postmark for email delivery and MJML to generate the email HTML.

Here's what an "invite" email looks like with this setup:

const InviteEmail = ({
  accountName,
  accountLogo,
  recipientName,
  acceptUrl,
  color = "#90116e",
}: Props) => (
  <Mjml>
    <MjmlHead>
      <MjmlTitle>{`You've been invited to ${accountName}`}</MjmlTitle>
      <MjmlAttributes>
        <MjmlButton
          backgroundColor={color}
          borderRadius={6}
          padding={20}
        />
        <MjmlText align="center" />
      </MjmlAttributes>
    </MjmlHead>
    <MjmlBody>
      <MjmlSection>
        <MjmlColumn>
          {accountLogo ? (
            <MjmlImage
              src={accountLogo}
              width={360}
              alt={accountName}
              href="https://worknice.com"
            />
          ) : null}
          <MjmlText>
            <h1>
              You&apos;ve been invited to
              <br />
              {accountName}
            </h1>
          </MjmlText>
          <MjmlText>
            <p>
              Hi {recipientName}, {accountName} uses Worknice to manage its HR. You&apos;ve been
              invited to create an account.
            </p>
          </MjmlText>
          <MjmlButton href={acceptUrl}>Create Your Account</MjmlButton>
        </MjmlColumn>
      </MjmlSection>
    </MjmlBody>
  </Mjml>
);

Sending an invite email is along these lines:

import { ServerClient } from "postmark";
import { createClient } from "@supabase/supabase-js";
import { render } from "mjml-react";

const postmark = new ServerClient(POSTMARK_TOKEN);
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

const email = "jane.doe@example.com";

await supabase.auth.api.createUser({ email });
const { data: linkData } = await supabase.auth.api.generateLink(
  "invite",
  email
);

const { html } = render(
  <InviteEmail
    accountName="Example"
    accountLogo="https://www.placecage.com/360/270"
    recipientName="Jane Doe"
    acceptUrl={`${BASE_URL}/accept-invite#otp=${
      linkData.email_otp
    }&email=${encodeURIComponent(email)}`}
  />,
  { validationLevel: "strict" }
);

await postmark.sendEmail({
  From: "noreply@worknice.com",
  To: email,
  Subject: "You've been invited to Example",
  HtmlBody: html,
  MessageStream: "outbound",
});

Verifying the OTP once the user has followed the link in the email is like this:

await supabase.auth.api.verifyOTP({
  email,
  token: otp,
  type: "invite",
});

(NB: This is using supabase-js v1, I'm assuming the API has changed a bit in v2.)

ForgeSolutions-JBrenner commented 2 years ago

@haydn Thank you so much for this! You are the absolute best 🤓