Skip to content

Commit 1f88cd3

Browse files
committed
secure-auth-api-v1
0 parents  commit 1f88cd3

File tree

14 files changed

+2436
-0
lines changed

14 files changed

+2436
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.env
2+
/node_modules
3+
/logs

README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# 🔐 Secure Authentication API (Node.js + JWT + OAuth2 + 2FA)
2+
3+
A **secure authentication API** using **Node.js, Express, JWT, Google OAuth2, and Two-Factor Authentication (2FA)**.
4+
5+
## 🚀 Features
6+
**User Authentication** (Register, Login, Logout)
7+
**Google OAuth 2.0 Login**
8+
**JWT Token Authentication**
9+
**Two-Factor Authentication (2FA)** with OTP
10+
**Secure Routes** for logged-in users
11+
**Password Hashing with bcrypt**
12+
**Proper Error Handling & Security Measures**
13+
14+
---
15+
16+
## 📂 Folder Structure
17+
```
18+
secure-auth-api-nodejs/
19+
│── config/ # Passport & OAuth Configurations
20+
│── models/ # Mongoose User Model
21+
│── routes/ # API Routes (Auth, Users, Protected)
22+
│── middleware/ # Authentication Middleware
23+
│── controllers/ # Business Logic (User handling)
24+
│── .env # Environment Variables
25+
│── server.js # Main Express App
26+
│── package.json # Dependencies & Scripts
27+
│── README.md # Project Documentation
28+
```
29+
30+
---
31+
32+
## 🚀 Quick Setup Guide
33+
34+
### 1️⃣ Clone the Repository
35+
```bash
36+
git clone https://github.com/your-username/secure-auth-api-nodejs.git
37+
cd secure-auth-api-nodejs
38+
```
39+
40+
### 2️⃣ Install Dependencies
41+
```bash
42+
npm install
43+
```
44+
45+
### 3️⃣ Setup Environment Variables
46+
Create a **.env** file in the root directory and add:
47+
```env
48+
PORT=5000
49+
MONGO_URI=your_mongodb_connection_string
50+
JWT_SECRET=your_jwt_secret
51+
GOOGLE_CLIENT_ID=your_google_client_id
52+
GOOGLE_CLIENT_SECRET=your_google_client_secret
53+
EMAIL_SERVICE=email_service_for_2fa
54+
EMAIL_USER=your_email
55+
EMAIL_PASS=your_email_password
56+
```
57+
58+
### 4️⃣ Start the Server
59+
```bash
60+
npm run dev
61+
```
62+
🚀 Your API will now run on **http://localhost:5000**
63+
64+
---
65+
66+
## 🔗 API Endpoints
67+
68+
### **User Authentication**
69+
| Method | Endpoint | Description |
70+
|--------|----------------------|-----------------------------|
71+
| POST | `/api/auth/register` | Register a new user |
72+
| POST | `/api/auth/login` | Login and get JWT token |
73+
| GET | `/api/auth/logout` | Logout user |
74+
75+
### **Google Authentication**
76+
| Method | Endpoint | Description |
77+
|--------|-------------------------|---------------------------------|
78+
| GET | `/api/auth/google` | Redirects to Google Login |
79+
| GET | `/api/auth/google/callback` | Google OAuth callback |
80+
81+
### **Protected Routes (JWT Required)**
82+
| Method | Endpoint | Description |
83+
|--------|---------------------|----------------------|
84+
| GET | `/api/protected` | Test Protected Route |
85+
86+
---
87+
88+
## 🛡 Security Features Implemented
89+
**JWT Tokens** with expiration
90+
**Password Hashing** using bcrypt
91+
**Two-Factor Authentication (2FA)** via email OTP
92+
**Session Management** for Google OAuth
93+
**Error Handling & Input Validation**
94+
95+
---
96+
97+
## 📜 License
98+
This project is licensed under the **MIT License**.
99+
100+
🔗 **Live Demo:** _Coming Soon_ 🚀
101+
💬 **Need Help?** Create an issue or reach out! 🎯

config/passport.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const passport = require("passport");
2+
const GoogleStrategy = require("passport-google-oauth20").Strategy;
3+
const User = require("../models/User");
4+
5+
passport.use(
6+
new GoogleStrategy(
7+
{
8+
clientID: process.env.GOOGLE_CLIENT_ID,
9+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
10+
callbackURL: "/api/auth/google/callback",
11+
},
12+
async (accessToken, refreshToken, profile, done) => {
13+
try {
14+
// Check if user already exists
15+
let user = await User.findOne({ email: profile.emails[0].value });
16+
17+
if (!user) {
18+
// Create new user
19+
user = new User({
20+
username: profile.displayName,
21+
email: profile.emails[0].value,
22+
googleId: profile.id,
23+
isVerified: true,
24+
});
25+
await user.save();
26+
}
27+
28+
done(null, user);
29+
} catch (error) {
30+
done(error, null);
31+
}
32+
}
33+
)
34+
);
35+
36+
passport.serializeUser((user, done) => {
37+
done(null, user.id);
38+
});
39+
40+
passport.deserializeUser(async (id, done) => {
41+
try {
42+
const user = await User.findById(id);
43+
done(null, user);
44+
} catch (error) {
45+
done(error, null);
46+
}
47+
});

controllers/authController.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const bcrypt = require("bcryptjs");
2+
const User = require("../models/User");
3+
const jwt = require("jsonwebtoken");
4+
const speakeasy = require("speakeasy");
5+
const QRCode = require("qrcode");
6+
7+
// 📌 How this works:
8+
9+
// Throws an error if the user is not found.
10+
// Passes it to errorHandler.js.
11+
exports.getUserProfile = async (req, res, next) => {
12+
try {
13+
const user = await User.findById(req.user.id);
14+
if (!user) {
15+
throw { status: 404, message: "User not found" }; // Throw an error
16+
}
17+
res.json(user);
18+
} catch (error) {
19+
next(error); // Pass error to middleware
20+
}
21+
};
22+
23+
exports.verify2FA = async (req, res) => {
24+
try {
25+
const { token } = req.body;
26+
const user = await User.findById(req.user.id);
27+
28+
if (!user || !user.isTwoFactorEnabled) {
29+
return res.status(400).json({ message: "2FA is not enabled" });
30+
}
31+
32+
const isValid = speakeasy.totp.verify({
33+
secret: user.twoFactorSecret,
34+
encoding: "base32",
35+
token,
36+
});
37+
38+
if (!isValid) {
39+
return res.status(400).json({ message: "Invalid OTP" });
40+
}
41+
42+
res.json({ message: "2FA verification successful" });
43+
} catch (error) {
44+
res.status(500).json({ message: "Error verifying 2FA", error });
45+
}
46+
};
47+
48+
// Generate and return a 2FA secret and QR code
49+
exports.enable2FA = async (req, res) => {
50+
try {
51+
const user = await User.findById(req.user.id);
52+
if (!user) {
53+
return res.status(404).json({ message: "User not found" });
54+
}
55+
56+
const secret = speakeasy.generateSecret({ length: 20 });
57+
58+
// Save the secret in the database
59+
user.twoFactorSecret = secret.base32;
60+
user.isTwoFactorEnabled = true;
61+
await user.save();
62+
63+
// Generate QR code for Google Authenticator
64+
const otpAuthUrl = `otpauth://totp/secure_auth_api?secret=${secret.ascii}&issuer=secure_auth_api`;
65+
QRCode.toDataURL(otpAuthUrl, (err, imageUrl) => {
66+
if (err)
67+
return res.status(500).json({ message: "QR Code generation failed" });
68+
69+
res.json({
70+
message: "2FA enabled",
71+
secret: secret.base32, // Show only for testing
72+
qrCode: imageUrl, // Send QR Code image URL
73+
});
74+
});
75+
} catch (error) {
76+
res.status(500).json({ message: "Error enabling 2FA", error });
77+
}
78+
};
79+
80+
exports.register = async (req, res) => {
81+
try {
82+
const { username, email, password } = req.body;
83+
84+
// Check if user already exists
85+
const existingUser = await User.findOne({ email });
86+
if (existingUser) {
87+
return res.status(400).json({ message: "Email already in use" });
88+
}
89+
90+
// Hash the password
91+
const salt = await bcrypt.genSalt(10);
92+
const hashedPassword = await bcrypt.hash(password, salt);
93+
94+
// Create a new user
95+
const user = new User({
96+
username,
97+
email,
98+
password: hashedPassword,
99+
});
100+
101+
await user.save();
102+
103+
res.status(201).json({ message: "User registered successfully" });
104+
} catch (error) {
105+
res.status(500).json({ message: "Server error", error: error.message });
106+
}
107+
};
108+
109+
exports.login = async (req, res) => {
110+
try {
111+
const { email, password } = req.body;
112+
113+
// Check if user exists
114+
const user = await User.findOne({ email });
115+
if (!user) {
116+
return res.status(400).json({ message: "Invalid email or password" });
117+
}
118+
119+
// Verify password
120+
const isMatch = await bcrypt.compare(password, user.password);
121+
if (!isMatch) {
122+
return res.status(400).json({ message: "Invalid email or password" });
123+
}
124+
125+
// Generate JWT token
126+
const payload = { userId: user._id };
127+
const token = jwt.sign(payload, process.env.JWT_SECRET, {
128+
expiresIn: "1h",
129+
});
130+
131+
res.json({ message: "Login successful", token });
132+
} catch (error) {
133+
res.status(500).json({ message: "Server error", error: error.message });
134+
}
135+
};

generate.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log(require("crypto").randomBytes(64).toString("hex"));

middleware/authMiddleware.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const jwt = require("jsonwebtoken");
2+
3+
exports.authenticateJWT = (req, res, next) => {
4+
const token = req.header("Authorization");
5+
6+
if (!token) {
7+
return res
8+
.status(401)
9+
.json({ message: "Access denied. No token provided." });
10+
}
11+
12+
try {
13+
const decoded = jwt.verify(
14+
token.replace("Bearer ", ""),
15+
process.env.JWT_SECRET
16+
);
17+
req.user = decoded;
18+
next();
19+
} catch (error) {
20+
return res.status(403).json({ message: "Invalid token" });
21+
}
22+
};

middleware/errorHandler.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// 📌 How this works:
2+
3+
// Logs errors using Winston.
4+
// Returns a JSON error response.
5+
// Hides error stack in production for security.
6+
7+
const logger = require("../utils/logger");
8+
9+
const errorHandler = (err, req, res, next) => {
10+
logger.error(err.message); // Log error
11+
12+
res.status(err.status || 500).json({
13+
message: err.message || "Internal Server Error",
14+
error: process.env.NODE_ENV === "development" ? err.stack : null,
15+
});
16+
};
17+
18+
module.exports = errorHandler;

middleware/rateLimiter.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const rateLimit = require("express-rate-limit");
2+
3+
const limiter = rateLimit({
4+
windowMs: 15 * 60 * 1000, // 15 minutes
5+
max: 100, // Limit each IP to 100 requests per windowMs
6+
message: "Too many requests, please try again later.",
7+
headers: true,
8+
});
9+
10+
module.exports = limiter;

models/User.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const mongoose = require("mongoose");
2+
3+
const UserSchema = new mongoose.Schema(
4+
{
5+
username: { type: String, required: true, unique: true },
6+
email: { type: String, required: true, unique: true },
7+
googleId: { type: String, unique: true, sparse: true }, // Store Google OAuth ID
8+
password: {
9+
type: String,
10+
required: function () {
11+
return !this.googleId;
12+
},
13+
}, // Required only if not using Google OAuth
14+
isVerified: { type: Boolean, default: false }, // Email verification
15+
twoFactorEnabled: { type: Boolean, default: false }, // 2FA status
16+
twoFactorSecret: { type: String, default: null }, // 2FA secret key
17+
},
18+
{ timestamps: true }
19+
);
20+
21+
module.exports = mongoose.model("User", UserSchema);

0 commit comments

Comments
 (0)