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

Separate Authentication Module #26

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
MONGODB_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret_key
HASH_SECRET=your_HmacSHA256_secret_key
SYSTEM_MAIL=your_email_address
SYSTEM_MAIL_PASS=your_email_address_password
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,20 @@ To set up Travel Buddy locally, follow these steps:
3. Configure environment variables:

- Create a `.env` file in the root directory.
- Add your MongoDB Atlas connection string:
- Copy the content from `.env.example` to `.env`:

```env
MONGODB_URI=your_mongodb_connection_string
```bash
cp .env.example .env
```

- Add your JWT secret key:

```env
MONGODB_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret_key
HASH_SECRET=your_HmacSHA256_secret_key
SYSTEM_MAIL=your_email_address
SYSTEM_MAIL_PASS=your_email_address_password
```
- Replace placeholder values in the .env file with actual values.

4. Run the application:
```sh
Expand Down
82 changes: 82 additions & 0 deletions app/api/authenticate/generate/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { NextResponse } from "next/server";
import otpGenerator from "otp-generator";
import { HmacSHA256 } from "crypto-js";
import nodemailer from "nodemailer";
import { cookies } from "next/headers";
import { instituteDetails } from "@/app/utils/institute";

export async function POST(req) {
try {
req = await req.json();

// Form fields:
// i) email – (Institute Email) – Text

const { email, instituteCode } = req;

const institute = await instituteDetails({ instituteCode });

const emailDomain = email.split("@")[1];

if (emailDomain != institute.domain) {
throw new Error("Invalid email. Choose the correct institute.");
}

const otp = otpGenerator.generate(6, { lowerCaseAlphabets: false, upperCaseAlphabets: false, specialChars: false });

const expiryTime = Date.now() + 5 * 60 * 1000; // 5 mins

const data = `${instituteCode}.${email}.${otp}`;

const hashData = HmacSHA256(data, process.env.HASH_SECRET).toString() + "." + expiryTime;

const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.SYSTEM_MAIL,
pass: process.env.SYSTEM_MAIL_PASS
}
});

const mailOptions = {
from: process.env.SYSTEM_MAIL,
to: email,
subject: 'OTP Code for Authentication',
text: `Your OTP code is: ${otp}. This will expire at ${new Date(expiryTime).toLocaleString()}`
};

await transporter.sendMail(mailOptions);

const cookieStore = cookies();

cookieStore.set({
name: "otpData",
value: JSON.stringify({ email, hashData, instituteCode }),
httpOnly: true,
path: "/",
maxAge: 5 * 60, // 5 mins
});

return NextResponse.json(
{
message: "OTP sent successfully! Please check your inbox.",
},
{
status: 200,
}
);

} catch (error) {
console.log(error.message);
return NextResponse.json(
{
message:
error.message ||
"Something went wrong - please try again later!",
},
{
status: 500,
}
);
}
}
65 changes: 65 additions & 0 deletions app/api/authenticate/validate-jwt/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import jwt from "jsonwebtoken";
import { instituteDetails } from "@/app/utils/institute";

export async function GET(req) {
try {

const { searchParams } = new URL(req.url);

const instituteCode = searchParams.get("instituteCode");

const institute = await instituteDetails({ instituteCode });

const { authCookie } = institute;

const cookieStore = cookies();

const cookie = cookieStore.get(authCookie);

if (!cookie) {
throw new Error("No JWT cookie found in the request.");
}

const token = cookie.value;

if (!token) {
throw new Error("No JWT session token found.");
}

const decoded = jwt.verify(token, process.env.JWT_SECRET);

const expiryTime = decoded.exp;
const currentTime = Date.now();

if (currentTime >= expiryTime * 1000) {
cookieStore.delete(cookie);
throw new Error("JWT cookie token has expired.");
}

return NextResponse.json(
{
message: "Authentication successful.",
email: decoded.email,
},
{
status: 200,
}
);

} catch (error) {
console.log(error.message);
return NextResponse.json(
{
message:
error.message ||
"Something went wrong - please try again later!",
},
{
status: 500,
}
);
}

}
103 changes: 103 additions & 0 deletions app/api/authenticate/verify/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

import { NextResponse } from "next/server";
import { HmacSHA256 } from "crypto-js";
import validator from "validator";
import { cookies } from "next/headers";
import { instituteDetails } from "@/app/utils/institute";
import jwt from "jsonwebtoken";

const createJwt = (payload) => {
const secret = process.env.JWT_SECRET;
const token = jwt.sign(payload, secret, { expiresIn: '1h' }); // Set expiration time
return token;
};

export async function POST(req) {
try {
req = await req.json();

// Form fields:
// i) email – (Institute Email) – Text
// ii) hashData – (Hashed Data for OTP verification) – Text
// iii) instituteCode - (Institute Code) – Text
// iv) otp - (Submitted OTP) - Text

const cookieStore = cookies();

const { email, hashData, instituteCode, otp } = req;

if (
validator.isEmpty(email) ||
validator.isEmpty(hashData) ||
validator.isEmpty(otp) ||
validator.isEmpty(instituteCode)
) {
throw new Error("Please fill all the fields!");
}

if (otp.length > 0 && otp.length != 6) {
throw new Error("Please enter a valid 6-digit OTP!");
}

const hash = hashData.split('.');

if (hash.length !== 2) {
throw new Error("Invalid hash data format!");
}

const data = `${instituteCode}.${email}.${otp}`;

const userHash = HmacSHA256(data, process.env.HASH_SECRET).toString();

if (userHash !== hash[0]) {
throw new Error("Verification failed try again.");
}

const expiryTime = parseInt(hash[1]);

const currentTime = Date.now();

if (currentTime > expiryTime) {
cookieStore.delete("otpData");
throw new Error("OTP has expired. Please request a new one.");
}

const institute = await instituteDetails({ instituteCode });

const payload = { email };

const token = createJwt(payload);

cookieStore.set({
name: institute.authCookie,
value: token,
httpOnly: true,
path: "/",
maxAge: 60 * 60, // Temporary maxAge
});

cookieStore.delete("otpData");

return NextResponse.json(
{
message: "OTP verified successfully!",
},
{
status: 200,
}
);

} catch (error) {
console.log(error.message);
return NextResponse.json(
{
message:
error.message ||
"Something went wrong - please try again later!",
},
{
status: 500,
}
);
}
}
30 changes: 26 additions & 4 deletions app/api/register/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import sanitize from "mongo-sanitize";
import validator from "validator";
import { connectToDatabase } from "@/app/lib/mongodb";
import User from "@/app/models/User";
import { cookies } from "next/headers";
import { cookies, headers } from "next/headers";
import Axios from "axios";
import { checkUser } from "@/app/utils/auth";
import { instituteDetails } from "@/app/utils/institute";

export async function POST(req) {
try {
req = await req.json();

// Form fields:
// i) name – (Full Name) - Text
// ii) roll – (Roll Number) – Text
Expand All @@ -20,7 +19,6 @@ export async function POST(req) {
// v) instituteCode - (Institute Code) – Text

let { name, roll, number, instituteCode } = req;

const institute = await instituteDetails({ instituteCode });

let { authCookie, verifyAuthLink } = institute;
Expand Down Expand Up @@ -51,7 +49,26 @@ export async function POST(req) {
);
}

const response = await Axios.get(verifyAuthLink, {
const isExternal = verifyAuthLink.startsWith("http://") || verifyAuthLink.startsWith("https://");

let verifyrouteURL;

if (isExternal) {
verifyrouteURL = verifyAuthLink;
}

else {
const requestHeaders = headers();
const host = requestHeaders.get('host');
const protocol = requestHeaders.get('x-forwarded-proto') || 'http';
verifyrouteURL = protocol + "://" + host + verifyAuthLink;
}

if (!verifyrouteURL || verifyrouteURL === "") {
throw new Error("Invalid authentication verfiy URL.");
}

const response = await Axios.get(verifyrouteURL, {
headers: {
Cookie: Object.entries({
[authCookie]: token,
Expand All @@ -60,6 +77,11 @@ export async function POST(req) {
.join("; "),
},
});

if (!response || !response.data || !response.data.email) {
throw new Error("No JWT session token found.");
}

const email = response.data.email;

if (!email) {
Expand Down
4 changes: 2 additions & 2 deletions app/authenticate/AuthForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";

export default function AuthForm({ institutes }) {
export default function AuthForm({ institutes, redirect_url }) {
const router = useRouter();

const check = () => {
Expand All @@ -28,7 +28,7 @@ export default function AuthForm({ institutes }) {
alert("Please select an institute");
return;
}
router.push("/register?instituteCode=" + institute);
router.push("/register?instituteCode=" + institute + "&redirect_url=" + redirect_url);
return;
};

Expand Down
Loading