Skip to content

Commit

Permalink
2fa auth with node.js
Browse files Browse the repository at this point in the history
  • Loading branch information
wpcodevo committed Oct 3, 2022
0 parents commit 1291a3e
Show file tree
Hide file tree
Showing 10 changed files with 1,346 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env
236 changes: 236 additions & 0 deletions controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import crypto from "crypto";
import { Prisma } from "@prisma/client";
import { Request, Response, NextFunction } from "express";
import { prisma } from "../server";
import speakeasy from "speakeasy";

const RegisterUser = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const { name, email, password } = req.body;

await prisma.user.create({
data: {
name,
email,
password: crypto.createHash("sha256").update(password).digest("hex"),
},
});

res.status(201).json({
status: "success",
message: "Registered successfully, please login",
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
return res.status(409).json({
status: "fail",
message: "Email already exist, please use another email address",
});
}
}
res.status(500).json({
status: "error",
message: error.message,
});
}
};

const LoginUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;

const user = await prisma.user.findUnique({ where: { email } });

if (!user) {
return res.status(404).json({
status: "fail",
message: "No user with that email exists",
});
}

res.status(200).json({
status: "success",
user: {
id: user.id,
name: user.name,
email: user.email,
otp_enabled: user.otp_enabled,
},
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};

const GenerateOTP = async (req: Request, res: Response) => {
try {
const { user_id } = req.body;
const { ascii, hex, base32, otpauth_url } = speakeasy.generateSecret({
issuer: "codevoweb.com",
name: "[email protected]",
length: 15,
});

await prisma.user.update({
where: { id: user_id },
data: {
otp_ascii: ascii,
otp_auth_url: otpauth_url,
otp_base32: base32,
otp_hex: hex,
},
});

res.status(200).json({
base32,
otpauth_url,
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};

const VerifyOTP = async (req: Request, res: Response) => {
try {
const { user_id, token } = req.body;

const user = await prisma.user.findUnique({ where: { id: user_id } });
const message = "Token is invalid or user doesn't exist";
if (!user) {
return res.status(401).json({
status: "fail",
message,
});
}

const verified = speakeasy.totp.verify({
secret: user.otp_base32!,
encoding: "base32",
token,
});

if (!verified) {
return res.status(401).json({
status: "fail",
message,
});
}

const updatedUser = await prisma.user.update({
where: { id: user_id },
data: {
otp_enabled: true,
otp_verified: true,
},
});

res.status(200).json({
otp_verified: true,
user: {
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
otp_enabled: updatedUser.otp_enabled,
},
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};

const ValidateOTP = async (req: Request, res: Response) => {
try {
const { user_id, token } = req.body;
const user = await prisma.user.findUnique({ where: { id: user_id } });

const message = "Token is invalid or user doesn't exist";
if (!user) {
return res.status(401).json({
status: "fail",
message,
});
}

const validToken = speakeasy.totp.verify({
secret: user?.otp_base32!,
encoding: "base32",
token,
window: 1,
});

if (!validToken) {
return res.status(401).json({
status: "fail",
message,
});
}

res.status(200).json({
otp_valid: true,
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};

const DisableOTP = async (req: Request, res: Response) => {
try {
const { user_id } = req.body;

const user = await prisma.user.findUnique({ where: { id: user_id } });
if (!user) {
return res.status(401).json({
status: "fail",
message: "User doesn't exist",
});
}

const updatedUser = await prisma.user.update({
where: { id: user_id },
data: {
otp_enabled: false,
},
});

res.status(200).json({
otp_disabled: true,
user: {
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
otp_enabled: updatedUser.otp_enabled,
},
});
} catch (error) {
res.status(500).json({
status: "error",
message: error.message,
});
}
};

export default {
RegisterUser,
LoginUser,
GenerateOTP,
VerifyOTP,
ValidateOTP,
DisableOTP,
};
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "2fa_nodejs",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child server.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && npx prisma generate",
"db:push": "npx prisma db push"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/morgan": "^1.9.3",
"@types/node": "^18.7.23",
"@types/speakeasy": "^2.0.7",
"morgan": "^1.10.0",
"prisma": "^4.4.0",
"ts-node-dev": "^2.0.0",
"typescript": "^4.8.4"
},
"dependencies": {
"@prisma/client": "^4.4.0",
"cors": "^2.8.5",
"express": "^4.18.1",
"speakeasy": "^2.0.0"
}
}
Binary file added prisma/dev.db
Binary file not shown.
16 changes: 16 additions & 0 deletions prisma/migrations/20220928095254_user_entity/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT NOT NULL,
"otp_enabled" BOOLEAN NOT NULL DEFAULT false,
"otp_verified" BOOLEAN NOT NULL DEFAULT false,
"otp_ascii" TEXT,
"otp_hex" TEXT,
"otp_base32" TEXT,
"otp_auth_url" TEXT
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"
23 changes: 23 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

model User {
id String @id @default(uuid())
email String @unique
name String
password String
otp_enabled Boolean @default(false)
otp_verified Boolean @default(false)
otp_ascii String?
otp_hex String?
otp_base32 String?
otp_auth_url String?
}
13 changes: 13 additions & 0 deletions routes/auth.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import express from "express";
import authController from "../controllers/auth.controller";

const router = express.Router();

router.post("/register", authController.RegisterUser);
router.post("/login", authController.LoginUser);
router.post("/otp/generate", authController.GenerateOTP);
router.post("/otp/verify", authController.VerifyOTP);
router.post("/otp/validate", authController.ValidateOTP);
router.post("/otp/disable", authController.DisableOTP);

export default router;
52 changes: 52 additions & 0 deletions server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { PrismaClient } from "@prisma/client";
import express, { Request, Response } from "express";
import cors from "cors";
import morgan from "morgan";

export const prisma = new PrismaClient();
const app = express();

async function main() {
// Middleware
app.use(morgan("dev"));
app.use(
cors({
origin: ["https://localhost:3000"],
credentials: true,
})
);
app.use(express.json());

// Health Checker
app.get("/api/healthchecker", (res: Response) => {
res.status(200).json({
status: "success",
message: "Welcome to Two-Factor Authentication with Node.js",
});
});

// Register the API Routes

// Catch All
app.all("*", (req: Request, res: Response) => {
return res.status(404).json({
status: "fail",
message: `Route: ${req.originalUrl} not found`,
});
});

const PORT = 8000;
app.listen(PORT, () => {
console.info(`Server started on port: ${PORT}`);
});
}

main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
Loading

0 comments on commit 1291a3e

Please sign in to comment.