nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.83k stars 3.5k forks source link

Magic Passcode #709

Closed alex-cory closed 3 years ago

alex-cory commented 4 years ago

Your question I'm curious if there's currently a way to have the same behavior as the magic link, but instead send a 4-6 digit code to their email that the user can then enter in themselves on the site.

image

Also kind of like this but with email code instead of SMS code.

What are you trying to do I'm working on a PWA and a magic link will take you directly to the mobile browser (ex: iOS Safari) instead of directly to the PWA. This is a way to get around that.

Feedback

glenarama commented 4 years ago

Also experience the same user experience issue - another way to solve this would be to handle email auth the same way https://magic.link/ or Vercel do. Rather than opening a new session, the link authenticates the original session.

alex-cory commented 4 years ago

@glenames I thought about this. I guess I should take a deeper look into that.

glenarama commented 4 years ago

@alex-cory Its not possible with the current email auth implementation - its a suggested alternate feature request I guess. I think it's a fair bit trickier to implement because it would require a subscription to the database from the client requesting access.

alex-cory commented 4 years ago

@glenames @iaincollins How does this flow look?

Also, what is the recommended way of adding the jwt or session token or csrf token to the frontend. I'm not quite sure how next-auth is setting these on the frontend so when calling useSession it will have the correct values to fetch the current session.

I'd be interested in helping put a PR together for this.

stale[bot] commented 3 years ago

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!

alex-cory commented 3 years ago

keep alive

stale[bot] commented 3 years ago

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!

alex-cory commented 3 years ago

keep alive

iaincollins commented 3 years ago

Thanks for bumping! Actually yes this is possible, but I don't think was ever documented.

  1. A generateVerificationToken() callback can be used with email Providers. That will generate a unique token - and hash it in the database, so it is not stored in clear text. You can use this in conjunction with a custom sendVerificationRequest() callback to send either a custom email or trigger an SMS message.

    By default if generateVerificationToken() is not set, then it will generate some random bytes for the token: https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/signin/email.js

    An implementation would look something like this:

 Providers.Email({
    generateVerificationToken: () => { return "ABC123" },
    sendVerificationRequest: ({ identifier: email, url, token, site, provider }) => { /* your function */ }
 })

~A caveat is that, unlike all other callbacks, generateVerificationToken is assumed to be synchronous - i.e. it does not use await when it is called, which is a mistake and we should change that.~ (Maintainer edit. It IS called with await since 3.6.0)

  1. Finally, you would also want to create a custom page for the /api/auth/verify-request route, as documented here: https://next-auth.js.org/configuration/pages

    Note you can actually have multiple providers that work this way (e.g. both Email and a custom one that does SMS) and handle them with pages that render differently, as the 'provider ID' is passed to the verification page, you would just need to set a custom id property on each provider instance.

For more information on customising the email provider, please see: https://next-auth.js.org/providers/email

Additionally, there is also this guide to using SMS for sign in with Everify: https://everify.dev/blog/two-factor-authentication-for-nextjs

This approach uses the credentials callback, which is perfect if you want stateless sign in without a user database - or if you want to use an existing user database in your own way.


I want to have built-in support for sending email codes like this, and potentially even make that the default behaviour for the email provider, as it makes it much easier to support cross-device sign in (e.g. start sign in on desktop, get email on phone, enter code on email and you are done).

I would also still support signing in by just clicking the button in the email; the messaging would just need to be right to not confuse users (as once the button was clicked the token would "used up" so you wouldn't want them clicking it on their phone by mistake).

We would need the built-in verify-request page to look a little different to accommodate short tokens like this (i.e. it would need to have form input elements for the code).

The final consideration is that shorter tokens really ought to have a shorter expiry time. This functionality is currently baked in to each database adapter (in the createVerificationRequest(), getVerificationRequest() and deleteVerificationRequest() methods).

Ideally I would like to see this abstracted out so that it is possible to use the Email provider without a database adapter, if you have these methods defined on the provider, although this is already possible with the existing callbacks (which can be very powerful) as shown in the example blog post.

balazsorban44 commented 3 years ago

For your interest, when #1378 is merged, generateVerificationToken will be an async function! 🎉

balazsorban44 commented 3 years ago

So #1378 is merged and I think Iain gave a good explanation. Can this be closed @alex-cory?

ramiel commented 3 years ago

As said by @iaincollins I need to customize the verify-request page to let the user insert the pin. Once there, what shouold I do with the PIN? My idea is that I have to send it to api/auth/callback/email?email=<EMAIL>&token=<PIN>. Do I have access to email in verify-request page?

baumant commented 3 years ago

@ramiel were you able to solve this?

ramiel commented 3 years ago

Yes, I was. The only way is to use the version of signin that does not redirect (https://next-auth.js.org/getting-started/client#using-the-redirect-false-option). That way I remain on the page where the user just inserted their email and I don't loose it. Otherwise, in the callback page there's no way to know the email of the sigin process (which I still think it's a problem and should be fixed in next-auth). Of course I had to handle all the logic to show the email input and the pin input.

Regardless of what is written in this issue, next-auth is not ready to have a login with pin just by customising the generateVerificationToken function.

trentprynn commented 3 years ago

Just jumping in to say I'd also really appreciate this ability! Currently I'm using sessions + magic links for authentication in my habit tracking project and I'm running into the same problem with magic links.

Here's an example mobile login flow where magic links on iOS are not working as well as I'd hope

  1. User opens Safari browser, navigates to website and clicks login
  2. User enters their email to login / create account
  3. User goes to their email app (gmail in my case) and clicks magic link they received
  4. gmail opens the link in the Safari (in-app) browser
    • note: this browser does not share local storage with the main Safari browser
  5. authentication is successful in the Safari (in-app) browser
  6. user goes back to their main Safari browser, where they initiated the login request from
  7. user refreshes the page and they are still unauthenticated

Just reiterating two possible solutions from above, either of which would be fine with me

  1. email authentication flow supports using a 6 digit OTP (replaces link button in email) that the user enters on the screen after entering their email and pressing the login button
  2. the magic link in the email the user receives authenticates the original session it was created from

Thank you for all of the work on this great library, the NextJS development experience has been a breath of fresh air thanks to all the cool libraries like this one :)

ramiel commented 3 years ago

OTP is already possible. We implemented it with next-auth at Hypersay Events .

trentprynn commented 3 years ago

@ramiel I agree it looks possible but I wish it had first party support though the Email provider using a configuration option like method:otp

If you have some time on your hands I'd really appreciate a write up of the steps needed to use OTP for email auth! I see you have a blog, I think this would make an awesome post if you happen to have the time / desire to do one :)

ramiel commented 3 years ago

@trentprynn your wish is an order :D https://www.ramielcreations.com/nexth-auth-magic-code

trentprynn commented 3 years ago

@ramiel so awesome! thank you so much, I really appreciate it :)

balazsorban44 commented 3 years ago

@ramiel very nice post! I agree it would be nice if we could implement it built-in, and when I finally have time, I might look into it!

trentprynn commented 3 years ago

@balazsorban44 I'd be happy to at least take a look at implementing first party email provider support for passcodes in nextauth

from an API perspective what do you think about the email provider allowing method: "otp" during configuration (I'm thinking we would have it be method: "link" by default so current users are not impacted)?

balazsorban44 commented 3 years ago

For a prototype, I guess that would be alright

itsbrex commented 2 years ago

@trentprynn your wish is an order :D https://www.ramielcreations.com/nexth-auth-magic-code

This is awesome! For whatever reason tho I can't seem to get it to work on mobile. Have you noticed this as well?

balazsorban44 commented 2 years ago

For anyone interested, I created a much-simplified version, based on @ramiel's blog post:

https://github.com/nextauthjs/next-auth/issues/4965#issuecomment-1189094806

bard commented 1 year ago

Here's a strategy/hack to support magic code without a database adapter and without implementing custom UI.

Demo:

Screencast from 2023-01-09 21-31-58.webm

I might do a proper writeup later but the idea is:

  1. on first invocation of /api/auth/signin, use a dummy Credentials Provider (with id otp-generation) to generate a UI containing only an email input field
  2. upon submission, hijack handling of /api/callback/otp-generation to generate and save an OTP code (in db, Redis, or even module scope) and place the email in a cookie (will be needed soon)
  3. redirect back to /api/auth/signin
  4. on this invocation of /api/auth/signin, notice presence of cookie and use a second Credentials Provider (with id otp-verification) to generate a UI containing only the code input field
  5. upon submission, handle request as normal in the authorize callback of the second Credentials Provider, verifying that the code credential is valid for the email provided in the cookie.

Code:

import cookie from "cookie";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { NextApiHandler } from "next";

const handler: NextApiHandler = async (req, res) => {
  if (
    req.query.nextauth !== undefined &&
    req.query.nextauth[0] === "callback" &&
    req.query.nextauth[1] === "otp-generation" &&
    req.method === "POST"
  ) {
    const { email } = req.body;
    if (!IMPLEMENTME_isValidEmail(email)) {
      return res.status(400).end();
    }

    const code = await IMPLEMENTME_generateOtp();
    await IMPLEMENTME_saveOtpForUser(email, code);
    await IMPLEMENTME_sendOtpToUser(email, code);

    res.setHeader(
      "set-cookie",
      cookie.serialize("otp-flow.user-email", req.body.email, {
        httpOnly: true,
        maxAge: 5 * 60, // 5 minutes
        path: "/",
      })
    );

    return res.redirect("/api/auth/signin");
  }

  const isOtpFlowInProgress = req.cookies["otp-flow.user-email"] !== undefined;

  return NextAuth({
    providers: isOtpFlowInProgress
      ? [
          CredentialsProvider({
            id: "otp-verification",
            name: "Magic Code",
            credentials: {
              code: {
                label: "Code",
                type: "text",
                placeholder: "Enter the code you received via email",
              },
            },
            async authorize(credentials, _req) {
              const email = req.cookies["otp-flow.user-email"];
              const code = credentials?.code;

              if (email === undefined || code === undefined) {
                return null;
              }

              if (!(await IMPLEMENTME_isOtpValid(email, code))) {
                return null;
              }

              const user = await IMPLEMENTME_findOrCreateUser(email);

              res.setHeader(
                "set-cookie",
                cookie.serialize("otp-flow.user-email", "", {
                  maxAge: -1,
                  path: "/",
                })
              );

              return user;
            },
          }),
        ]
      : [
          CredentialsProvider({
            id: "otp-generation",
            name: "Magic Code",
            credentials: {
              email: {
                label: "Email",
                type: "email",
                placeholder: "Your email address",
              },
            },
            async authorize() {
              return null;
            },
          }),
        ],
  })(req, res);
};

export default handler;
nmicun commented 1 year ago

Is this possible with AuthJs and svelteKit? I already made solution with email provider, but as my sveltekit will be used with pwa, I saw on ios I cannot redirect magic link to installed app on homescreen only safari browser, so I quess this solution with otp can be helpful. Any code solution for svelteKit and authJS with magic code?

bard commented 1 year ago

@nmicun if you're referring to the strategy I posted above, it uses server-rendered pages ony, so I'd imagine it would work regardless of the framework you're using for your front end.

nmicun commented 1 year ago

hello @bard thanks for your reply. I'm on very beginning with SvelteKit and all of this, that's why I asked if someone has already example there. For Email provider with magic link, its very easy to setup as its built in, but for this I'm not sure where to add changes. I also follow @balazsorban44 solution, but I think its not the same with @auth/sveltekit.

bard commented 1 year ago

Sorry @nmicun, my brain is still stuck at when AuthJS was only next-auth.

Indeed, I see that sveltekit comes with a different backend altogether, so the above might not be applicable. Hopefully someone familiar with it can chime in.

nmicun commented 1 year ago

looks like @balazsorban44 solution is compatible with sveltekit too. not sure for now how to keep user on same page after click signIn button, or maybe how to redirect on new page with only code input field but to pass email info also.

matthew-tanner commented 1 year ago

@trentprynn your wish is an order :D https://www.ramielcreations.com/nexth-auth-magic-code

Do you have a working code example of this somewhere on the site? The page details seem to be missing some pieces or an older version from V4 changes maybe. An example repo would be very helpful on this.

lauridskern commented 1 year ago

Hi! Are there any updates on this regarding a built-in option?

thomasmol commented 1 year ago

Would love to have this built-in as well. I am running auth/core in production and using email sign up. Users often land on the auth-error page because:

  1. They input their email on sign in/up page
  2. Receive a link in their email client (wherever that might be)
  3. Click on the login link button
  4. Which opens their default browser of their OS, which is more often than not the actual browser they most often use and in which they signed up.
  5. Result in them signing in in their default browser, but not in the actual browser they want to use, so they refresh the page, thinking they are logged in, they aren't, and
  6. then copy the login link in their email and open it in their other browser,
  7. but now the link is expired so they land on the auth-error page.

I talked about it here: https://github.com/nextauthjs/next-auth/discussions/7524 but no reaction yet :)

widavies commented 1 year ago

@trentprynn your wish is an order :D https://www.ramielcreations.com/nexth-auth-magic-code

This is awesome! For whatever reason tho I can't seem to get it to work on mobile. Have you noticed this as well? @itsbrex

Had this same issue, tried the HEAD early return method and a few other things. Turns out for me it was pretty straightforward - mobile devices tend to auto capitalize the first letter, the callback url appears to be case sensitive. I ended up using the following for the callback (call toLowerCase() on email before passing it as a query parameter):

 window.location.href = "/api/auth/callback/email?email=" + encodeURIComponent(inputEmail.toLowerCase().trim()) + "&token=" + encodeURIComponent(code.trim());
jmcelreavey commented 1 year ago

Has anyone got generateVerificationToken() override to work? It always seems to generate a really long pin for me.

youminkim commented 1 year ago

Has anyone got generateVerificationToken() override to work? It always seems to generate a really long pin for me.

probably hashed value

thomasmol commented 1 year ago

@jmcelreavey I do this:

generateVerificationToken: () => {
  const random = crypto.getRandomValues(new Uint8Array(8));
  return Buffer.from(random).toString('hex').slice(0, 6);
},

It generates a random string of 6 characters [a-z,0-9]

Full emailprovider object looks like this (this is in SvelteKit +hooks.server.ts):

EmailProvider({
    server: EMAIL_SERVER,
    from: EMAIL_FROM,
    maxAge: 15 * 60, // 15 minutes
    generateVerificationToken: () => {
        const random = crypto.getRandomValues(new Uint8Array(8));
        return Buffer.from(random).toString('hex').slice(0, 6);
    },
    sendVerificationRequest(params) {
        customSendVerificationRequest(params);
    }
}),
sharpsteelsoftware commented 1 year ago

Would LOVE to see built in OTP for email or customizable for SMS :)

dortonway commented 10 months ago

Up! It's merged not in this repo.

shawnmclean commented 3 months ago

Anyone using phone numbers instead of emails? (SMS / WhatsApp, etc)

What does this identifier look like?

lobotomoe commented 1 month ago

I know this is really old issue, but anyway. My solution:

Server (auth config):

providers: [
    Nodemailer({
      id: "email-code",
      server: {
        host: env.SMTP_HOST,
        port: env.SMTP_PORT,
        auth: {
          user: env.SMTP_USER,
          pass: env.SMTP_PASSWORD,
        },
      },
      from: env.EMAIL_FROM,
      generateVerificationToken() {
        const code = Math.floor(100000 + Math.random() * 900000); // random 6-digit code
        return code.toString();
      },
      sendVerificationRequest: sendVerificationRequestCode, // Just make it magic-code view, it's easy. sendVerificationRequest well-documented.
    }),
// ...

Client:

const checkToken = async (email: string, token: string) => {
  const url = new URL("/api/auth/callback/email-code", window.location.href);
  url.searchParams.append("email", email);
  url.searchParams.append("token", token);
  const response = await fetch(url.href);

  const responseUrl = new URL(response.url, window.location.href); // Final url with all redirects
  const errorSearchParam = responseUrl.searchParams.get("error");
  if (errorSearchParam === null) { // Everything fine.
    return true;
  }
  throw new Error(
    `Error during token check: ${errorSearchParam}. URL: ${response.url}`,
  );
};

After successful checkToken call you can refetch session data.

melodyclue commented 1 month ago

I know this is really old issue, but anyway. My solution:

Server (auth config):

providers: [
    Nodemailer({
      id: "email-code",
      server: {
        host: env.SMTP_HOST,
        port: env.SMTP_PORT,
        auth: {
          user: env.SMTP_USER,
          pass: env.SMTP_PASSWORD,
        },
      },
      from: env.EMAIL_FROM,
      generateVerificationToken() {
        const code = Math.floor(100000 + Math.random() * 900000); // random 6-digit code
        return code.toString();
      },
      sendVerificationRequest: sendVerificationRequestCode, // Just make it magic-code view, it's easy. sendVerificationRequest well-documented.
    }),
// ...

Client:

const checkToken = async (email: string, token: string) => {
  const url = new URL("/api/auth/callback/email-code", window.location.href);
  url.searchParams.append("email", email);
  url.searchParams.append("token", token);
  const response = await fetch(url.href);

  const responseUrl = new URL(response.url, window.location.href); // Final url with all redirects
  const errorSearchParam = responseUrl.searchParams.get("error");
  if (errorSearchParam === null) { // Everything fine.
    return true;
  }
  throw new Error(
    `Error during token check: ${errorSearchParam}. URL: ${response.url}`,
  );
};

After successful checkToken call you can refetch session data.

Hi, This code does works well, but I feel that there is not much reason to heavily rely on auth.js if you use TOTP...