Skip to content

Commit 4577eb7

Browse files
author
Harshit Yadav
committed
feat: 2FA implemented
1 parent 73b72c0 commit 4577eb7

File tree

11 files changed

+524
-93
lines changed

11 files changed

+524
-93
lines changed

README.md

+239
Original file line numberDiff line numberDiff line change
@@ -1276,3 +1276,242 @@ export const newPassword = async (
12761276
```
12771277

12781278
It shoul now start working.
1279+
1280+
## 2Factor Authentication
1281+
1282+
95. Update `schema.prisma` file
1283+
1284+
- user model
1285+
1286+
```
1287+
model User {
1288+
id String @id @default(cuid())
1289+
name String?
1290+
email String? @unique
1291+
emailVerified DateTime?
1292+
image String?
1293+
password String?
1294+
accounts Account[]
1295+
role UserRole @default(USER)
1296+
isTwoFactorEnabled Boolean @default(false)
1297+
twoFactorConfirmation TwoFactorConfirmation?
1298+
}
1299+
```
1300+
1301+
- isTwoFactorEnabled model
1302+
1303+
```
1304+
model TwoFactorToken {
1305+
id String @id @default(cuid())
1306+
email String
1307+
token String @unique
1308+
expires DateTime
1309+
1310+
@@unique([email, token])
1311+
}
1312+
```
1313+
1314+
- TwoFactorConfirmation model
1315+
1316+
```
1317+
model TwoFactorConfirmation {
1318+
id String @id @default(cuid())
1319+
1320+
userId String
1321+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
1322+
1323+
@@unique([userId])
1324+
}
1325+
```
1326+
1327+
96. Now run the following command
1328+
`npx prisma generate` and `npx prisma db push`
1329+
1330+
97. # Setting up two-factor-token
1331+
1332+
- Create a new file in `lib/actions/auth` folder i.e `two-factor-token.ts` file
1333+
1334+
```
1335+
import { db } from "@/lib/database.connection";
1336+
1337+
export const getTwoFactorTokenByToken = async (token: string) => {
1338+
try {
1339+
const twoFactorToken = await db.twoFactorToken.findUnique({
1340+
where: { token },
1341+
});
1342+
1343+
return twoFactorToken;
1344+
} catch {
1345+
return null;
1346+
}
1347+
};
1348+
1349+
export const getTwoFactorTokenByEmail = async (email: string) => {
1350+
try {
1351+
const twoFactorToken = await db.twoFactorToken.findFirst({
1352+
where: { email },
1353+
});
1354+
1355+
return twoFactorToken;
1356+
} catch {
1357+
return null;
1358+
}
1359+
};
1360+
```
1361+
1362+
- Setting up the action for confirming two-factor token in `lib/actions/auth` folder `two-factor-confirmation.ts` file
1363+
1364+
```
1365+
import { db } from "@/lib/database.connection";
1366+
1367+
export const getTwoFactorConfirmationByUserId = async (userId: string) => {
1368+
try {
1369+
const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({
1370+
where: { userId },
1371+
});
1372+
1373+
return twoFactorConfirmation;
1374+
} catch {
1375+
return null;
1376+
}
1377+
};
1378+
1379+
```
1380+
1381+
- Generating Two factor token in `lib/token` file
1382+
1383+
```
1384+
export const generateTwoFactorToken = async (email: string) => {
1385+
const token = crypto.randomInt(100_000, 1_000_000).toString();
1386+
const expires = new Date(new Date().getTime() + 5 * 60 * 1000);
1387+
1388+
const existingToken = await getTwoFactorTokenByEmail(email);
1389+
1390+
if (existingToken) {
1391+
await db.twoFactorToken.delete({
1392+
where: {
1393+
id: existingToken.id,
1394+
}
1395+
});
1396+
}
1397+
1398+
const twoFactorToken = await db.twoFactorToken.create({
1399+
data: {
1400+
email,
1401+
token,
1402+
expires,
1403+
}
1404+
});
1405+
1406+
return twoFactorToken;
1407+
}
1408+
```
1409+
1410+
- Sned two factor token email
1411+
1412+
```
1413+
export const sendTwoFactorTokenEmail = async (
1414+
email: string,
1415+
token: string
1416+
) => {
1417+
await resend.emails.send({
1418+
1419+
to: email,
1420+
subject: "2FA Code",
1421+
html: `<p>Your 2FA code: ${token}</p>`
1422+
});
1423+
};
1424+
```
1425+
1426+
98. Go to prisma studio and enable the 2FA for a user
1427+
1428+
99. Modify the login function in `auth.ts` file
1429+
1430+
```
1431+
1432+
// * Prevent sign in without two factor confirmation (99)
1433+
if (existingUser.isTwoFactorEnabled) {
1434+
const twoFactorConfirmation = await getTwoFactorConfirmationByUserId(
1435+
existingUser.id
1436+
);
1437+
1438+
if (!twoFactorConfirmation) return false;
1439+
1440+
// Delete two factor confirmation for next sign in
1441+
await db.twoFactorConfirmation.delete({
1442+
where: { id: twoFactorConfirmation.id },
1443+
});
1444+
}
1445+
```
1446+
1447+
100. Add 2FA Verification in `login.ts` file
1448+
1449+
```
1450+
//* 2FA verification
1451+
if (exisitingUser.isTwoFactorEnabled && exisitingUser.email) {
1452+
if (code) {
1453+
const twoFactorToken = await getTwoFactorTokenByEmail(exisitingUser.email);
1454+
1455+
if (!twoFactorToken) {
1456+
return { error: "Invalid code!" };
1457+
}
1458+
1459+
if (twoFactorToken.token !== code) {
1460+
return { error: "Invalid code!" };
1461+
}
1462+
1463+
const hasExpired = new Date(twoFactorToken.expires) < new Date();
1464+
1465+
if (hasExpired) {
1466+
return { error: "Code expired!" };
1467+
}
1468+
1469+
await db.twoFactorToken.delete({
1470+
where: { id: twoFactorToken.id },
1471+
});
1472+
1473+
const existingConfirmation = await getTwoFactorConfirmationByUserId(
1474+
exisitingUser.id
1475+
);
1476+
1477+
if (existingConfirmation) {
1478+
await db.twoFactorConfirmation.delete({
1479+
where: { id: existingConfirmation.id },
1480+
});
1481+
}
1482+
1483+
await db.twoFactorConfirmation.create({
1484+
data: {
1485+
userId: exisitingUser.id,
1486+
},
1487+
});
1488+
} else {
1489+
const twoFactorToken = await generateTwoFactorToken(exisitingUser.email);
1490+
await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token);
1491+
1492+
return { twoFactor: true };
1493+
}
1494+
}
1495+
```
1496+
1497+
101. Update the login schema
1498+
1499+
```
1500+
export const LoginSchema = z.object({
1501+
email: z.string().email({
1502+
message: "Email is required",
1503+
}),
1504+
password: z.string().min(1, {
1505+
message: "Password is required",
1506+
}),
1507+
code: z.optional(z.string()),
1508+
});
1509+
1510+
```
1511+
102. Update the login form, based on the conditions (see the code directly from file)
1512+
- concept is to show 2FA code, when after the login button is clicked, and two factor is enabled
1513+
- (might be incomplete)
1514+
1515+
1516+
1517+

actions/auth/login.ts

+57-8
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import { signIn } from "@/auth";
66
import { DEFAULT_LOGIN_REDIRECT } from "@/route";
77
import { AuthError } from "next-auth";
88
import { getUserByEmail } from "@/lib/actions/user.action";
9-
import { generateVerificationToken } from "@/lib/token";
10-
import { sendVerificationEmail } from "@/lib/mail";
9+
import { generateTwoFactorToken, generateVerificationToken } from "@/lib/token";
10+
import { sendTwoFactorTokenEmail, sendVerificationEmail } from "@/lib/mail";
11+
import { getTwoFactorConfirmationByUserId } from "@/lib/actions/auth/two-factor-confirmation";
12+
import { db } from "@/lib/database.connection";
13+
import { getTwoFactorTokenByEmail } from "@/lib/actions/auth/two-factor-token";
1114

1215
export const Login = async (values: z.infer<typeof LoginSchema>) => {
1316
const validatedFields = LoginSchema.safeParse(values); // valdiating the input values
1417
if (!validatedFields.success) {
1518
return { error: "Invalid fields! " };
1619
}
17-
const { email, password } = validatedFields.data;
20+
const { email, password, code } = validatedFields.data;
1821

1922
// * not allowing the user to login if the email is not verified (69)
2023
const exisitingUser = await getUserByEmail(email);
@@ -29,14 +32,60 @@ export const Login = async (values: z.infer<typeof LoginSchema>) => {
2932
);
3033

3134
// * sending mail while logging in if email is not verified (72)
32-
await sendVerificationEmail(
33-
verificationToken.email,
34-
verificationToken.token
35-
);
35+
await sendVerificationEmail(
36+
verificationToken.email,
37+
verificationToken.token
38+
);
3639

3740
return { success: "Confirmation Email sent!" };
3841
}
39-
// *
42+
//* 2FA verification
43+
if (exisitingUser.isTwoFactorEnabled && exisitingUser.email) {
44+
if (code) {
45+
const twoFactorToken = await getTwoFactorTokenByEmail(
46+
exisitingUser.email
47+
);
48+
49+
if (!twoFactorToken) {
50+
return { error: "Invalid code!" };
51+
}
52+
53+
if (twoFactorToken.token !== code) {
54+
return { error: "Invalid code!" };
55+
}
56+
57+
const hasExpired = new Date(twoFactorToken.expires) < new Date();
58+
59+
if (hasExpired) {
60+
return { error: "Code expired!" };
61+
}
62+
63+
await db.twoFactorToken.delete({
64+
where: { id: twoFactorToken.id },
65+
});
66+
67+
const existingConfirmation = await getTwoFactorConfirmationByUserId(
68+
exisitingUser.id
69+
);
70+
71+
if (existingConfirmation) {
72+
await db.twoFactorConfirmation.delete({
73+
where: { id: existingConfirmation.id },
74+
});
75+
}
76+
77+
await db.twoFactorConfirmation.create({
78+
data: {
79+
userId: exisitingUser.id,
80+
},
81+
});
82+
} else {
83+
const twoFactorToken = await generateTwoFactorToken(exisitingUser.email);
84+
await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token);
85+
86+
return { twoFactor: true };
87+
}
88+
}
4089

4190
try {
4291
await signIn("credentials", {

app/auth/login/page.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import LoginForm from '@/components/auth/login-form'
1+
2+
import { LoginForm } from '@/components/auth/login-form'
23
import React from 'react'
34

45
const LoginPage = () => {

auth.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { db } from "./lib/database.connection";
55
import { PrismaAdapter } from "@auth/prisma-adapter";
66
import { getUserById } from "./lib/actions/user.action";
77
import { UserRole } from "@prisma/client";
8+
import { getTwoFactorConfirmationByUserId } from "./lib/actions/auth/two-factor-confirmation";
89

910
export const {
1011
handlers: { GET, POST },
@@ -31,14 +32,27 @@ export const {
3132
callbacks: {
3233
// * (70)
3334
async signIn({ user, account }) {
35+
// Allow OAuth without email verification
3436
if (account?.provider !== "credentials") return true;
3537

36-
const exisitingUser = await getUserById(user.id);
38+
const existingUser = await getUserById(user.id);
3739

38-
//* prevent sign in without email verification
39-
if (!exisitingUser?.emailVerified) return false;
40+
// Prevent sign in without email verification
41+
if (!existingUser?.emailVerified) return false;
4042

41-
// TODO : Add 2FA check
43+
// * Prevent sign in without two factor confirmation (99)
44+
if (existingUser.isTwoFactorEnabled) {
45+
const twoFactorConfirmation = await getTwoFactorConfirmationByUserId(
46+
existingUser.id
47+
);
48+
49+
if (!twoFactorConfirmation) return false;
50+
51+
// Delete two factor confirmation for next sign in
52+
await db.twoFactorConfirmation.delete({
53+
where: { id: twoFactorConfirmation.id },
54+
});
55+
}
4256

4357
return true;
4458
},

0 commit comments

Comments
 (0)