All right, so to implement two-factor authentication, let's go ahead and let's visit our Prisma schema right here, and let's find the user model here. Let's go ahead and add a new field called isTwoFactorEnabled. That's going to be a type of boolean with the default value of false so this is going to be used to decide whether to send the user the two-factor token when they try to log in and another field we're going to create is a relation with two-factor confirmation model which we are going to create. So two-factor confirmation is going to be a two-factor confirmation model relation and it's going to be optional so put a little question mark at the end. And now we have to create this model but before we do that let's create the two-factor token which will be sent to the user's email.
So this one will be exactly the same as our password reset token and our verification token. I just want to keep them in separate models. So let's call this one two-factor token. It's going to have an ID of string and a default value of CUID. Email, which is a string token, which is a unique string, and expires, which is a date time and a unique combination of email and token.
And now let's create the two-factor confirmation model. So model two-factor confirmation is going to have an ID of string, ID and a default of cuID. And it's going to have a relation with the user ID, so that's going to be a string and a user is going to be a type of user, relation, fields, user ID, references ID and let's add on delete cascade, So if the user model gets deleted, we're going to go ahead and also delete this two-factor confirmation if it exists. And let's also go ahead and add add-at unique user ID. Like that.
There we go. So make sure that you add the user relation here with the user, which we have not here, right here. So we have two factor confirmation to this model. Yeah, and make sure you add the unique otherwise you can see that we are getting a one-to-one relation must be unique field or something else like that. So unique user id or I believe you can also add unique here.
I think it's the same thing, yeah, but I'm going to use this method here. Great, so make sure you add this. And now let's go ahead and let's reset our database so everything is clear. So shut down the app. Let's run npx prisma generate.
So we add those new models inside of our node modules. And then let's do npx prisma migrate reset to remove everything inside of our database here and let's confirm this like that and then let's go ahead and run npx prisma database push like this So this will push the new models inside of NEON database. And after you've done that, you can do npm run dev. And if you want to, you can also do command shift p and reload your Visual Studio Code window so there is no any cache. Then refresh your localhost here and it should still work just fine.
And let's also prepare the Prisma Studio in another terminal, so npx prisma studio. That's going to open it on localhost 5555 and let me just enable all the fields to be visible. And there we go, you can see how I now have is two-factor enabled and two-factor confirmation here. I'm going to go ahead and create a new account using my tutorial name. Mailing at gmail.com.
So use the mail which can send the email tutorialmailinggmail.com 123456 I'm going to create an account So we're just testing whether the rest of our application is working fine. There we go. So confirmation email was sent. There we go. Confirm your email.
I'm going to click here and that's going to confirm my verification. There we go. This is the little bug that we have, but only in development. And now I believe if I go ahead and use tutorialmailing at gmail.com here 1 2 3 4 5 6 and click login I should be able to log in just fine. Great And now let's go ahead inside of my Prisma Studio and let's refresh this user.
And by default I should now have a value of IsTwoFactorEnabled false. And I can only change it to true and we have the potential relation with two-factor confirmation, which right now you can see does not exist. Great! So now what we have to do is we have to create the tokens, the data and the email for two-factor token right here. So let's go ahead and do that.
So let's go and close everything. Let's go inside of data and create a new file to factor token.ts. Let's import the database from s-lib database and let's export const getToFactorTokenByToken to be an asynchronous function, which accepts the token, which is a string. Let's open a try and catch block. In the catch, we're gonna return null.
And in here, let's get the two factor token by using await database two factor token, not confirmation, but find unique, where we have a matching token and return two-factor token like that and then let's copy and paste this. Let's rename this one to be get two-factor token by email and in here let's get the asynchronous email and let's use find first and get the matching email like this. There we go. Great. Now let's go ahead and also create a data for two-factor confirmation.
So twoFactorConfirmation.ts. This one is going to be a little bit different, a little bit simpler. So it's only going to have one method called get two factor confirmation by user id so it's going to be an asynchronous function which accepts the user id which is a type of string Let's open a try and catch block here and return null in the catch. And in here, let's get two-factor confirmation by using await database.twoFactorConfirmation. Find unique.
Let me see if I can expand this even further. Okay, basically in one line like this, find unique, where we have a matching user ID. So no need to use the email or the token here because in our schema we have an actual relation with the user so we know this confirmation is tied to the user so we can use the user id directly and let's return two-factor confirmation like that That's all we need for two-factor confirmation here. Now let's go ahead and let's go and generate, create a lib to generate a two-factor token. So for that we're gonna need a package called crypto and I believe it already exists in the node ecosystem so you won't need to install anything at least I don't so I can just do import crypto from crypto you can see how it auto completes and I didn't need to install any package so if you take a look at my package.json I don't have crypto anywhere installed right So you should just have this in by default.
And let me just import crypto actually. Great. And now let's go ahead and let's also import GetToFactorTokenByEmail. So make sure you add that which we just created in a second ago and let's go well we can do it at the top actually here so export const generate to factor token it's going to be an asynchronous function which again accepts the email to where we send the actual token and in here let's go ahead and generate the token but this one is going to be a little bit different because I want it to be a six digit number so we're going to use crypto.randomInteger and in here we're going to write a hundred thousand and here's a little tip for you when writing numbers in JavaScript if you're having a hard time you know seeing how many zeros they are you can add underscore like that and this is still gonna be the exact same thing if we wrote 100, 000 directly so this is the same thing but it's easier to see what it is, so 100, 000 so this is gonna be the start of our range and the end of our range is gonna be 1, 000, 000 like that to string and let's write the expires.
So it's going to be new date. New date. Get time. Plus 3600 times 8000. So it's going to expire in an hour.
Let's see if we can find an existing token by using await get two factor token by email and let's use the email which is passed in the props here. If we have an existing token, in that case await database.twoFactorToken so not confirmation but token be careful dot delete where we have an id which matches the existing token.id And then we can finally create a new two-factor token. So const two-factor token is going to be await database two-factor token.create data, and inside we can safely pass the email for the user to send the token which is going to be a six digit code and expires for when that code expires like that. And let's return to factor token. So if you want to, you know, you can modify the expires for two factor token.
I think it's fine for the password reset token and the verification to last an hour. But if you want to, you can use some different math here to make it maybe 15 minutes rather than an hour if this is too long for you, right? But for the tutorial, I'm just going to use an hour, but let me add a little to do here later change to 15 minutes just so you get reminded that, you know, if you actually plan on doing stuff like this in production, it would be smart to make this expire a bit sooner. Great. And now let's go ahead and let's create a mail util for this.
So inside of lib, let's go to mail here. And let's go ahead and export const send to factor email to factor token email and let's make it an asynchronous function which accepts the email which is a string and a token which is a string like that and then in here let's add const confirm link to be HTTP localhost 3000 slash auth. Actually no, no we don't have the confirmation link here, right? My apologies. It's just a token.
So the email is just going to be, this is your confirmation code and a six digit number. So let's just do await resend email sent from onboarding at resend.dev to email subject 2FA code and HTML open backticks write a little paragraph here and it's gonna be your 2FA code and stringify the token, which we sent from the props. That's it. Great. So what we have to do now is we have to manually enable two-factor authentication for one of the users which we have in our database.
So make sure you have Prisma Studio running. Go inside of your user model here. Find the user which you have, make sure it's registered using credentials, so make sure that you verify the email for it and everything, and change the value of isToFactorEnabled from false to true. And click save one change here. And wait for a second for this to update and there we go.
Now it should be true and you can of course refresh just to confirm that you have the newest value there we go is two-factor enabled is now true. So as you already know I can freely log in with this email. As you can see, I'm already logged in. So I just signed out, but I should not be able to log in that easily if I have two-factor enabled. So it's one way to prevent that in login.ts, which we are going to do, but the first place I want to prevent it is in auth.ts in the callback sign in which where we added to do add two-factor authentication check.
So let's go ahead and do that. So I'm gonna add if existing user.isTwoFactorEnabled in that case what we have to do is we well we can just easily return false first so let's check if that is working so I'm going to use tutorial mailing at gmail.com123456 and because I change that to true in my database there we go something went wrong so I should not be allowed to log in if I have two-factor enabled but you know this isn't exactly useful because once they confirm their token, how exactly are we going to allow them to log in? We're going to use that first by importing get two-factor confirmation. So import get two-factor confirmation by user ID like that and I'm going to use the at sign so imported from data two-factor confirmation so don't accidentally use two-factor tokens here in here we need the confirmation so what we're going to do here is for now remove the return false and instead let's go ahead and see if we have the confirmation so const two-factor confirmation is going to be await get two-factor confirmation by user ID and pass in existing user.id like this great and let me just expand this so we can see this basically it's all in one line like that, it's a long line and now if we don't have two-factor confirmation then return false.
Otherwise, I'm going to add a comment delete two-factor confirmation for next sign in. Obviously, this is my choice of doing this, right? So every time the user logs in, if they have two-factor authentication, I'm going to go ahead and delete the two-factor confirmation for them. So the next time they log in, they have to do the same thing over again. If you want to, you can of course modify that by going inside of schema prisma here and for example you can add expires field here as well to be a date time so I'm not gonna do this in this tutorial but I'm just giving you some tips you can do that if you want to and you know make it work exactly the same as it would with our other tokens.
So when you go ahead and generate the two-factor confirmation, where is that? Oh, we don't generate it yet. So we're gonna do that later. In there, you can add the expires in maybe, I don't know, two days or something like that. But I think it's safer for two-factor to be very strict.
So that's how I'm going to do it. So if the user successfully logs in and they have two-factor confirmation, in that case, let's do await database.twoFactorConfirmation.delete, where the ID is twoFactorConfirmation.id. As simple as that. And then we can go ahead and well we don't have to do anything because we have returned true at the end so if it skips this that means that the other one is true. So just by adding this code I should still not be allowed to log in because I shouldn't have the two-factor confirmation.
So I'm gonna add a console.log two-factor confirmation and I'm gonna log that inside of an object so it's easier to see. So let's check it out. If I go inside of my terminal here. Let's go ahead and click login again. And I should see undefined for that value.
Let's scroll above the error here or null, there we go. So you can see the object here. Let me just scroll up again. There we go. So it's right here.
Two-factor confirmation is null and because of that we do an if if there is no two-factor confirmation we prevent the user from logging in. Perfect! That is exactly what we want. I'm not allowed to sign in and I shouldn't be. So what we have to do now is we have to improve the behavior which happens when user clicks login here.
They should not get an error because they have two-factor authentication. Instead, this should change to be a code input which they will receive from their email. So let's go ahead and do that now. So what we have to do is we have to go inside of actions, login right here. So let's import generate two factor token.
So generate two factor token from lib tokens, or we can reuse this one like that and let me collapse them there we go so make sure you have generate verification token and generate two-factor token and besides that I also have mail here great so we are also going to import send two-factor token email like that so make sure you have send two-factor token email and generate two-factor token here and in here what we are going to do is after we check whether the user has been verified or not but before we try to log in here let's add if user sorry existing user dot to is to is to factor enabled and if we have user.email, sorry, existing user.email, in that case, let's do const two factor token is gonna be await, generate two factor token from existing user email and then await send two factor token email and let's use two factor token dot email as the first argument and two factor token which is the six digit code as the second argument. And let's return a special object two-factor to be true like that so it's gonna break the function and it's gonna give our frontend a specific value so that we know we have to change the display right here.
So let's go ahead and try that out now. So I'm gonna go ahead and log in again and this time I shouldn't get an error. Instead it should just stay empty like this but I should get a new email and there we go your 2FA code and here we have a six digit number. Perfect! So now what we have to do is once the front-end receives this value two-factor to be true we have to modify this inputs so that we can enter that code.
So for that what we have to do is we have to revisit our schemas. So let's go inside of schemas index.ps right here. Let's find the login schema and let's add a third field which is going to be code and this one is going to be optional so z.optional here and let's add z.string inside so very simple like that great and we can also add well actually no let's just leave it like this And now that we have added that, make sure you did that to Login Schema. We can go back inside of Components, out Login Form, right here. And in here, let's go ahead and let's add a new state here.
So above the error, let's go ahead and let's add a new state here so above the error let's go ahead and let's add show to factor and set show to factor and use state by default is going to be false, like that. And then let's go ahead and modify our onSubmit function a bit. So inside of here I'm going to do the following. If I have data?error, I'm going to reset the form so we have some better user experience here and I'm going to set the error to data.error. Then if I have data?success, I can also reset the form and I can set success to data.success.
And last one here is gonna be if data has two factor. In that case, we are not going to reset the form because we need the credentials that user just wrote and then set the show to a factor is simply going to be true like that and we can also add a little dot catch here so in case anything else goes wrong let's set error to be something went wrong like this. Great! And now what I want to do is I want to display a different form field if we need to show two-factor. So let's go ahead and change this and let's go ahead and wrap this form field and the password form field so the entire thing like that and let's go ahead and wrap it inside of a fragment so go here because there's two items so they need to have a parent like that and let's just go ahead and indent those like this So two of these elements should be inside of a fragment.
And then we're gonna go ahead and give this a conditional. If not show to factor, then go ahead and render those and you can wrap them in parentheses if you like. I'm gonna do that as well. There we go. So we are going to render the form field password and the form field email.
Where is it? Right here. Only if we didn't get the to do factor from the back end. And now let's do the opposite one. So if we do have show two factor in that case, let's just go ahead and copy and paste an existing form field like email here.
So copy this one. And let's paste it inside. And let's go ahead and give it a name of code and let's give this two factor code label. Let's change the placeholder to be 123456 and let's change the type and we can just remove it. And let's also give it, oh it already has disabled spending.
Great! So let's take a look at it now. So if I use tutorial mailing at gmail.com now and try to log in I should receive two-factor true and this should change the input for me to enter my two-factor code. There we go. And I should be getting a new email here with my new two-factor code.
So now we have to find a way to enter two-factor code here so that next time we click login we actually verify the code and I also want to change this label here so I'm going to go ahead and find where I wrote login I'm going to change this if show to factor in that case is going to be confirm otherwise it's going to be login so this should now say confirm perfect But if we just enter the code and confirm it's not going to work. So what we have to do is we have to go back inside of our login action. So inside of actions login right here. And let's find where we destructure the fields. So validated fields.data here at the start.
Besides destructuring the email and password, we can also now destructure the code which might or might not exist right? So here's what we're going to do inside of this if clause where we check for two-factor token and we send the two-factor token email we're going to do the following. So in here I'm going to add an if clause If I have the code to do verify code else we're gonna send the two-factor code like that. So that's how we're gonna differentiate if the user just pressed on login or if the user logged in again but this time provided us with the two-factor code so we can do that check inside of here. So let's go ahead and do that.
The first thing we have to do is we have to import get two-factor token by email using data two-factor token like that. All right, and let's go ahead inside of here in this to do. And let's go ahead and see const two-factor token that we have saved in our database for this user using await get two-factor token by email using the existing user.email so we know what's the code so what we're going to do is if there is no two-factor token, we're gonna return an error, invalid code. And then if two-factor token.token is not same to what the user just wrote in the code field we're also going to return an error saying invalid code and then what we can do is check if the code has expired. So has expired is going to be new date, two factor token dot expires is less than new date from now.
So if the token has expired, regardless if it's correct or not, we're going to return error code expired. Great! And then finally we can remove the two-factor token and we can create the two-factor confirmation so that user can finally log in. So let's go ahead and write away database.twoFactorToken.delete where id matches twoFactorToken.id and let's go ahead and import DB so I don't have that so import DB from s-lib database so first we're going to go ahead and delete this one and then let's go ahead and see if we have an existing confirmation so const existing confirmation is going to be await database twoFactorConfirmation.findUnique actually, you know what we can use? We can use getToFactorConfirmationByUserID directly right, we have that util so go ahead and import getToFactorConfirmationByUserID from data to factor confirmation so in here let me zoom out We're gonna pass in the existing user.id or I can separate that like this.
There we go. So get to factor confirmation by user ID. If we already have an existing confirmation, we're gonna go ahead and remove it from here as well. So get two-factor confirmation dot delete, where ID matches existing confirmation dot ID. Like that.
And once we've done that, we are finally ready to go ahead and do await database two-factor confirmation.create where, sorry, data user ID to be existing user.id and we're not going to return anything because what we want is to create the two-factor confirmation this else is going to be skipped and then what we're going to do is try and log in and because we're gonna create the two-factor confirmation just before we try to log in the next time the user goes ahead and triggers instead of auth.ts the sign-in callback guess what We're gonna be able to find two-factor confirmation and this is going to be skipped and we're gonna delete the two-factor confirmation and we're gonna return true. So let's go ahead and test that out. So I'm gonna go ahead and go back to my login page here. I'm gonna repair my emails. I'm gonna use tutorial mailing gmail.com.
I'm going to log in here. This will send my two-factor token on my email. So let's just wait. There we go. There we go, 2FA code.
I'm going to copy this code. I'm going to paste it here. And I'm going to click confirm this time with a code and let's see if that's gonna allow me to log in now let's wait a second and there we go we are officially logged in and we confirmed our two-factor authentication and inside of my Prisma Studio I should not be having any two-factor confirmations so you can see that this no longer exists so if I log out and log in I'm gonna require that again and let's go ahead and look at a two-factor token they don't exist either like that so if I go ahead and try this again, so tutorialmailing at gmail.com 123456 and if I go ahead and log in I'm going to be prompted with two-factor again and if I refresh on the two-factor token here We now have a two-factor token and we can see what is our secret six-digit number here. We still don't have any two-factor confirmation, but I do have a new code here. So let me copy this.
Let's paste it here. Let's confirm it like that. And what this is going to do is remove my two-factor token and it's also gonna create and then immediately remove my two-factor confirmation so we actually didn't even see but this definitely existed for a second before it was immediately removed by the login callback and there we go the old two-factor token has been deleted and let's try one more thing if I go back inside of my user here and change is two-factor enabled back to false and click save changes here and if I sign out now and if I try this again tutorialmailing at gmail.com 1 2 3 4 5 6 this should allow me to log in freely without having to enter two-factor authentication. There we go. So I recommend that you keep it like this for now, just so you can easily log in and log out.
Perfect! So we just wrapped up everything we needed for the out screen here. What we're gonna do next is we're gonna create the actual inside screen where we're gonna have an example of a server component and how we can fetch the currently logged in user, a client component and we're also going to have the settings page where we can change the new email and the new password. And don't forget we also have to come back inside of our login button here and enable so that we can change the mode to a dialogue instead of redirecting to this screen. Great, great job!