Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-redirect Client API support for passwordless email/sms codes #3260

Closed
Izhaki opened this issue Nov 22, 2021 · 8 comments
Closed

Non-redirect Client API support for passwordless email/sms codes #3260

Izhaki opened this issue Nov 22, 2021 · 8 comments
Labels
enhancement New feature or request stale Did not receive any activity for 60 days

Comments

@Izhaki
Copy link

Izhaki commented Nov 22, 2021

Description 📓

Passwordless Email/SMS Code

Earlier on today I've shared an example of passwordless email code flow. I plan to share a similar example using SMS code later this week.

This flow, where a code is sent instead of a callback link, means only the same device can be authenticated - but that's a desired behaviour for many, how Instagram do it, and pretty much every SMS auth flow.

Currently, this flow is only possible with redirects, whereas I think it would be useful to add an API support for it.

Whilst attempting this, I also came across a few possible improvement that are not specific for this flow, described below.

What's the Problem with Redirects?

Redirects are fairly standard part of authentication, and are pivotal for OAuth. However, they are not a necessity with passwordless code flows, and there can be some benefits doing all via API, with the user staying on the same page.

Consider these two options as to what happens when a non-registered user clicks the "Ask a question" in a site like StackOverflow:

  1. The user is redirected to a login page, registers, then fill the new question form and submits.
  2. The user is allowed to fill the question form unauthorised, but upon "Submit new Question":
    2.1 A dialog shows prompting for email
    2.2 Then an email code prompt
    2.3 The dialog closes
    2.4 Either the question is automatically submitted, or the user has to click submit again.

Option 2 has a retention benefit (for the business) because users have already invested effort, and loss aversion has it that users are more likely to register at this point.

For the user, it is the same effort regardless of option 1 or 2.

There is a signIn API

https://next-auth.js.org/getting-started/client#signin

Can we have a verifySentToken API

I'm calling it verifySentToken and not verifyEmailToken, because the same applies for SMSs.

Point is, the user has just entered her email in the frontend, and shortly after entered the code sent. We just need a client API method to verify this email/code combo.

Although I'm talking about verifySentToken, the exact API is to be discussed - it could part of the signIn method.

Some issues and points to consider

In one of my attempts I've written this function:

async function verifyEmailToken({ email, token }) {
  const encode = encodeURIComponent;
  const apiRoute = "api/auth/callback/email";
  const url = `${apiRoute}?email=${encode(email)}&token=${encode(token)}`;

  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    // For an unknown reason `json` needs to be `"true"` rather than `true`.
    // https://github.com/nextauthjs/next-auth/blob/76bf524e8ec144162bcd0474adba250028f9c4db/src/next/index.ts#L45
    body: JSON.stringify({ json: "true" }),
  });

  const { url: redirectUrl } = await response.json();
  console.log(redirectUrl);
}

Things to consider:

"true" rather than true

json: "true" prevents redirects.

Spent quite some time on this (I had true not "true"), and I'm not sure why the library checks for req.body?.json !== "true" instead of req.body?.json !== true here.

Most body with fetch/post do JSON.stringify and the fact we query req.body.json (a plain JS object and not a JSON string) makes it looks like true is more correct here.

The endpoint returns 200 even for errors

With json: "true" you always get 200, even if there was an error.

While this feature may be considered "internal", it still feels like a bad practice.

Currently one can search for "error" in the returned url to figure out there was an error, but a proper status code would probably be appropriate here.

No way to refresh the app

Even when successful, once this function returns and all is good, the app view does not update (although the cookie session is in the client). It all works after page reload.

I couldn't find anywhere in the Client API something like:

const { refresh } = useSession();

Which will tell SessionProvider to update based on the current session cookie.

This is another addition that will have to be made to support this flow.

Contributing 🙌🏽

Yes, I am willing to help implement this feature in a PR

@Izhaki Izhaki added the enhancement New feature or request label Nov 22, 2021
@balazsorban44
Copy link
Member

I am not sure if I understand what is being proposed here entirely, but I can point you to these other issues/discussions, which I think would cover this. If you agree, could we close this one since it is a duplicate?

#2269 updating session without requiring a page reload

#1465 Generalize the Email provider

@Izhaki
Copy link
Author

Izhaki commented Nov 23, 2021

The emphasis is on non-redirect flow.

Practically what is being proposed is a new verifyToken Client API, so we do not depend on redirects for things to work.

So in the same way we call:

signIn("email", { email: "[email protected]", redirect: false });

We can call

verifyToken("email", { email: "[email protected]", token: "ABC123", redirect: false });

Currently you need to call api/auth/callback/email and hack it with body: JSON.stringify({ json: "true" }) (and implement some odd logic around the response) in order to do this.

Also, for non-redirect flow to work, there will need to be a way to tell SessionProvider that there's a new session cookie so it needs to trigger a render cycle in React.

Currently, you can hack things around with api/auth/callback/email and you end up with a valid session cookie, but the UI doesn't update - you are still seen as logged out - because the SessionProvider is not aware there's a new session cookie (the user has never left the page, and there are no new pages you are being redirected to).

Here is what I mean (the verify click uses the function above):

next-auth-no-redirect

#2269 updating session without requiring a page reload

This is about updating session in the client, and requires a trip to the backend. That's different from the non-redirect case where you already have the correct session cookie in the front end, but you just want to update the UI to reflect that.

#1465 Generalize the Email provider

This is a fantastic idea, and I would be more than happy to give this a go.

But this is not a dependency of what is being proposed here. In other words, the proposal here could be implemented regardless.

In a way, the email provider already enables code based flow (no email link, just a token that the user type in the UI), so this token provider is really just a generalisation of it that will be more native to SMS, nodemailer-free email support, and so on.

The no-redirect support requested here applies to both cases.

@Izhaki
Copy link
Author

Izhaki commented Nov 24, 2021

To further clarify, adding this to src/react/index.tsx solves the ui update:

export async function refresh() {
  await __NEXTAUTH._getSession({ event: "storage" })
}

This is called like so:

  const handleVerify = async () => {
    await verifyEmailToken({ email, token });
    refresh();
  };

next-auth-refresh-session

@Izhaki
Copy link
Author

Izhaki commented Nov 25, 2021

OK, so having looked at the implementation of the client's signIn method, clarifies that these client functions go about achieving their task very much like what I've sketched above (by calling the route and transforming the response).

So we could add something roughly like this to the client functions:

async function verifyEmailToken({
  email,
  token,
  callbackUrl = window.location.href,
  redirect = true,
}) {
  const apiRoute = "api/auth/callback/email";
  const params = new URLSearchParams({
    email,
    token,
  });
  const url = `${apiRoute}?${params}`;

  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({ json: true }),
  });

  const data = await res.json();

  if (redirect) {
    const url = data.url ?? callbackUrl;
    window.location.href = url;
    // If url contains a hash, the browser does not reload the page. We reload manually
    if (url.includes("#")) window.location.reload();
    return;
  }

  const error = new URL(data.url).searchParams.get("error");

  if (res.ok) {
    await __NEXTAUTH._getSession({ event: "storage" });
  }

  return {
    error,
    status: res.status,
    ok: res.ok,
    url: error ? null : data.url,
  };
}

Please let me know if you wish me to raise a PR. We might need to discuss the API of this function.

(As a side not, I believe this is what @armandabric was asking for when he requested

Add a verifyUser(providerName, args) function to the client.

@stale
Copy link

stale bot commented Jan 25, 2022

Hi there! It looks like this issue hasn't had any activity for a while. It will be closed if no further activity occurs. If you think your issue is still relevant, feel free to comment on it to keep it open. (Read more at #912) Thanks!

@stale stale bot added the stale Did not receive any activity for 60 days label Jan 25, 2022
@stale
Copy link

stale bot commented Feb 3, 2022

Hi there! It looks like this issue hasn't had any activity for a while. To keep things tidy, I am going to close this issue for now. If you think your issue is still relevant, just leave a comment and I will reopen it. (Read more at #912) Thanks!

@stale stale bot closed this as completed Feb 3, 2022
@karrui
Copy link

karrui commented Mar 21, 2023

Any support for this in the latest NextAuth? From a quick peek, seems like this feature request is still unimplemented. Furthermore, the workaround above seems to not work anymore. Can I re-request for a non-redirect flow as I want to implement my own email sign in and verify OTP flow without the usage of magic links?

@najibghadri
Copy link

@balazsorban44 Hi! I think the idea above is described very clearly. I think all it needs is an API that supports JSON response, so we can check whether the token is valid or not. Any chance this will be picked up again?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request stale Did not receive any activity for 60 days
Projects
None yet
Development

No branches or pull requests

4 participants