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

Magic Passcode #709

Closed
2 of 5 tasks
alex-cory opened this issue Sep 24, 2020 · 42 comments
Closed
2 of 5 tasks

Magic Passcode #709

alex-cory opened this issue Sep 24, 2020 · 42 comments
Labels
enhancement New feature or request question Ask how to do something or how something works

Comments

@alex-cory
Copy link
Contributor

alex-cory commented Sep 24, 2020

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

  • Found the documentation helpful
  • Found documentation but was incomplete
  • Could not find relevant documentation
  • Found the example project helpful
  • Did not find the example project helpful
@alex-cory alex-cory added the question Ask how to do something or how something works label Sep 24, 2020
@glenarama
Copy link

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
Copy link
Contributor Author

alex-cory commented Sep 28, 2020

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

@glenarama
Copy link

@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
Copy link
Contributor Author

alex-cory commented Sep 28, 2020

@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.

  • /login frontend
    • type in email, click login/signup - POST { email, isAddedToHomescreen } to /api/auth/magic-link
    • if isAddedToHomescreen, subscribe to channel private-magic-link-me@gmail.com
      • on trigger event from channel private-magic-link-me@gmail.com - set JWT from socket body
  • /api/auth/magic-link POST
    • create/get user with post body.email
    • create JWT with userId, email, isAddedToHomescreen and whatever else should go into the JWT
    • email user with link /magic-link?jwt=<the-jwt>
  • /magic-link?jwt=<the-jwt> SSR page
    • in getServerSideProps
      • deserialize JWT to get email, trigger socket with JWT from query params as the socket body, send to channel private-magic-link-me@gmail.com
    • frontend
      • if isAddedToHomescreen display message saying "you have been logged in, now go back to your app added to your homescreen"
      • else client side redirect to home page

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

@iaincollins iaincollins added the enhancement New feature or request label Oct 20, 2020
@stale
Copy link

stale bot commented Dec 19, 2020

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 Dec 19, 2020
@alex-cory
Copy link
Contributor Author

keep alive

@stale stale bot removed the stale Did not receive any activity for 60 days label Dec 20, 2020
@stale
Copy link

stale bot commented Feb 18, 2021

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 Feb 18, 2021
@alex-cory
Copy link
Contributor Author

keep alive

@stale stale bot removed the stale Did not receive any activity for 60 days label Feb 22, 2021
@iaincollins
Copy link
Member

iaincollins commented Feb 23, 2021

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
Copy link
Member

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

@balazsorban44
Copy link
Member

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

@ramiel
Copy link
Contributor

ramiel commented Apr 12, 2021

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
Copy link

baumant commented Jun 14, 2021

@ramiel were you able to solve this?

@ramiel
Copy link
Contributor

ramiel commented Jun 14, 2021

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
Copy link

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
Copy link
Contributor

ramiel commented Aug 28, 2021

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

@trentprynn
Copy link

@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
Copy link
Contributor

ramiel commented Aug 30, 2021

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

@trentprynn
Copy link

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

@balazsorban44
Copy link
Member

For a prototype, I guess that would be alright

@itsbrex
Copy link

itsbrex commented Jul 2, 2022

@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
Copy link
Member

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

#4965 (comment)

@bard
Copy link

bard commented Jan 9, 2023

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
Copy link

nmicun commented Jan 22, 2023

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
Copy link

bard commented Jan 22, 2023

@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
Copy link

nmicun commented Jan 22, 2023

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
Copy link

bard commented Jan 22, 2023

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
Copy link

nmicun commented Jan 22, 2023

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
Copy link

@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
Copy link

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

@thomasmol
Copy link

thomasmol commented May 28, 2023

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: #7524 but no reaction yet :)

@widavies
Copy link

widavies commented Jun 1, 2023

@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
Copy link

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

@youminkim
Copy link

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

probably hashed value

@thomasmol
Copy link

@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
Copy link

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

@dortonway
Copy link

Up! It's merged not in this repo.

@shawnmclean
Copy link
Contributor

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

What does this identifier look like?

@lobotomoe
Copy link

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
Copy link

melodyclue commented Oct 3, 2024

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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Ask how to do something or how something works
Projects
None yet
Development

No branches or pull requests