Closed alex-cory closed 3 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.
@glenames I thought about this. I guess I should take a deeper look into that.
@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.
@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
{ email, isAddedToHomescreen }
to /api/auth/magic-link
private-magic-link-me@gmail.com
private-magic-link-me@gmail.com
- set JWT from socket body/api/auth/magic-link
POST
userId
, email
, isAddedToHomescreen
and whatever else should go into the JWT/magic-link?jwt=<the-jwt>
/magic-link?jwt=<the-jwt>
SSR page
getServerSideProps
private-magic-link-me@gmail.com
isAddedToHomescreen
display message saying "you have been logged in, now go back to your app added to your homescreen"I'd be interested in helping put a PR together for this.
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!
keep alive
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!
keep alive
Thanks for bumping! Actually yes this is possible, but I don't think was ever documented.
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
)
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.
For your interest, when #1378 is merged, generateVerificationToken
will be an async function! 🎉
So #1378 is merged and I think Iain gave a good explanation. Can this be closed @alex-cory?
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?
@ramiel were you able to solve this?
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.
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
Just reiterating two possible solutions from above, either of which would be fine with me
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 :)
OTP is already possible. We implemented it with next-auth at Hypersay Events .
@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 :)
@trentprynn your wish is an order :D https://www.ramielcreations.com/nexth-auth-magic-code
@ramiel so awesome! thank you so much, I really appreciate it :)
@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!
@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)?
For a prototype, I guess that would be alright
@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?
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
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:
/api/auth/signin
, use a dummy Credentials Provider (with id otp-generation
) to generate a UI containing only an email input field/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)/api/auth/signin
/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 fieldauthorize
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;
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?
@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.
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.
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.
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.
@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.
Hi! Are there any updates on this regarding a built-in option?
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:
I talked about it here: https://github.com/nextauthjs/next-auth/discussions/7524 but no reaction yet :)
@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());
Has anyone got generateVerificationToken()
override to work? It always seems to generate a really long pin for me.
Has anyone got
generateVerificationToken()
override to work? It always seems to generate a really long pin for me.
probably hashed value
@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);
}
}),
Would LOVE to see built in OTP for email or customizable for SMS :)
Up! It's merged not in this repo.
Anyone using phone numbers instead of emails? (SMS / WhatsApp, etc)
What does this identifier look like?
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.
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...
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.
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