Creating a MERN stack application with JWT authentication #199
Replies: 5 comments
-
Advanced FeaturesGiven the complexity and the number of features, implementing all of them in a single response would be quite extensive. I'll start with a few of the advanced features and provide detailed code and explanations for each. Let's begin with JWT Revocation, Token Rotation, and Role-Based Access Control (RBAC). 1. JWT Revocation:Context: JWTs are stateless, meaning once issued, they can't be invalidated until they expire. However, in some scenarios, you might need to revoke a token (e.g., user changes password or logs out). Setup:
npm install redisImplementation:
const redis = require('redis');
const client = redis.createClient();
// Middleware to check if token is blacklisted
function checkBlacklist(req, res, next) {
const token = req.header('auth-token');
client.exists(token, function(err, reply) {
if (reply === 1) {
return res.status(401).send('Token is blacklisted');
}
next();
});
}
app.use(checkBlacklist);To blacklist a token (e.g., on logout): router.post('/logout', verifyToken, (req, res) => {
const token = req.header('auth-token');
client.set(token, true); // Blacklist token
res.send('Logged out');
});2. Token Rotation:Context: Refresh tokens are used to obtain a new access token when the current one expires. This allows users to stay authenticated without logging in again. Implementation:
// Generate and send a new access token using the refresh token
router.post('/token', (req, res) => {
const refreshToken = req.body.token;
if (!refreshToken) return res.sendStatus(401);
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
const accessToken = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken });
});
});3. Role-Based Access Control (RBAC):Context: RBAC allows you to set permissions based on user roles. For instance, an admin might have different access levels compared to a regular user. Setup:
const userSchema = new mongoose.Schema({
// ... other fields ...
role: { type: String, default: 'user' } // 'user' or 'admin'
});Implementation:
function verifyRole(role) {
return function(req, res, next) {
if (req.user.role !== role) {
return res.status(403).send('Access denied');
}
next();
};
}Use the middleware in routes: router.get('/admin', verifyToken, verifyRole('admin'), (req, res) => {
res.send('Admin content');
});These are just three of the advanced features. Implementing all the features would be a large project in itself. If you're looking to integrate all the features, it might be best to break down the implementation into phases or consider working with a development team. Alright, let's continue with the next set of advanced features: Multi-Factor Authentication (MFA), JWT Key Rotation, and Token Compression. 4. Multi-Factor Authentication (MFA):Context: MFA adds an extra layer of security. After the initial login, prompt the user for a second factor like an OTP. Setup:
npm install speakeasy qrcodeImplementation:
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate an OTP token and QR code for the user
router.post('/generate-otp', verifyToken, async (req, res) => {
const secret = speakeasy.generateSecret({ length: 20 });
const user = await User.findById(req.user._id);
user.tempSecret = secret.base32;
await user.save();
QRCode.toDataURL(secret.otpauth_url, (err, data_url) => {
res.json({ qrCode: data_url });
});
});
// Verify the OTP provided by the user
router.post('/verify-otp', verifyToken, async (req, res) => {
const user = await User.findById(req.user._id);
const verified = speakeasy.totp.verify({
secret: user.tempSecret,
encoding: 'base32',
token: req.body.token
});
if (verified) {
user.isMFAEnabled = true;
user.secret = user.tempSecret;
await user.save();
res.send('MFA setup successful');
} else {
res.status(400).send('Invalid OTP');
}
});5. JWT Key Rotation:Context: Regularly rotate the signing key without disrupting the service. Use JWKS (JSON Web Key Set) where multiple keys are available for verifying the signature of a JWT. Setup:
Implementation:
6. Token Compression:Context: If the payload of your JWT becomes too large, consider compressing it. Setup:
npm install pakoImplementation:
const pako = require('pako');
// When generating a token
const payload = { data: 'your data' };
const compressedPayload = pako.deflate(JSON.stringify(payload), { to: 'string' });
const token = jwt.sign({ data: compressedPayload }, process.env.JWT_SECRET);
// When verifying a token
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
const decompressedPayload = JSON.parse(pako.inflate(decodedToken.data, { to: 'string' }));These implementations provide a deeper dive into advanced JWT features. However, always ensure to test thoroughly and consider the security implications of each feature. If you're looking to integrate all these features, consider breaking down the implementation into phases or working with a development team for a more streamlined process. Alright, let's delve into the next set of advanced features: Embedding Session State, Rate Limiting on Token Generation, and Token Binding. 7. Embedding Session State:Context: Instead of storing session data server-side, you can embed session state within the JWT. This can reduce database lookups but increases the JWT size. Implementation: When generating a token, embed the session data:
const sessionData = {
lastLogin: Date.now(),
userPreferences: {
theme: 'dark',
language: 'en'
}
};
const token = jwt.sign({ _id: user._id, sessionData }, process.env.JWT_SECRET);When verifying a token, you can access the session data directly from the decoded token. 8. Rate Limiting on Token Generation:Context: Implement rate limiting on endpoints that generate tokens (like login and refresh token endpoints) to prevent abuse. Setup:
npm install express-rate-limitImplementation:
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: "Too many login attempts, please try again later."
});
app.use('/api/auth/login', loginLimiter);9. Token Binding:Context: Token binding ensures that tokens are only usable by the client they were issued to. This can prevent token theft. Implementation: When generating a token, bind it to a client property, like a TLS unique channel ID (if supported) or a cookie:
const binder = req.cookies.binder || req.headers['unique-channel-id'];
const token = jwt.sign({ _id: user._id, binder }, process.env.JWT_SECRET);When verifying the token, ensure the binder matches:
const tokenBinder = decodedToken.binder;
const requestBinder = req.cookies.binder || req.headers['unique-channel-id'];
if (tokenBinder !== requestBinder) {
return res.status(403).send('Token binding does not match');
}These advanced features further enhance the security and efficiency of your JWT implementation. However, it's crucial to understand the trade-offs and complexities associated with each feature. Testing is paramount, and in a real-world scenario, it's advisable to have security experts review your implementation. If you're integrating all these features, consider a modular approach, implementing and testing each feature one by one. Let's continue with the next set of advanced JWT features: Cross-Origin Resource Sharing (CORS), Logging and Monitoring, and Stateful JWTs. 10. Cross-Origin Resource Sharing (CORS):Context: If your API and frontend are on different domains, you need to set up CORS correctly, especially if you're sending tokens via headers. Setup:
Implementation:
const corsOptions = {
origin: 'http://your-frontend-domain.com',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
};
app.use(cors(corsOptions));11. Logging and Monitoring:Context: Logging JWT generation, expiration, and validation failures can help in debugging and monitoring for any suspicious activities. Setup:
npm install winstonImplementation: Create a logger instance:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;Use the logger in your routes and middleware:
const logger = require('../logger');
// ... other code ...
router.post('/login', async (req, res) => {
// ... other code ...
logger.info(`User ${username} logged in`);
});
// ... other code ...12. Stateful JWTs:Context: While JWTs are typically stateless, you can make them stateful by storing JWT IDs ( Implementation: When generating a token, add a
const jwtId = new mongoose.Types.ObjectId().toString();
const token = jwt.sign({ _id: user._id, jti: jwtId }, process.env.JWT_SECRET);
// Store jwtId in the database
const session = new Session({ jwtId });
await session.save();When verifying the token, ensure the
const jwtId = decodedToken.jti;
const session = await Session.findOne({ jwtId });
if (!session) {
return res.status(403).send('Token is no longer valid');
}These features further refine the JWT implementation, adding layers of security, monitoring, and flexibility. As always, it's essential to thoroughly test each feature in a real-world environment. Implementing all these features requires careful planning and a deep understanding of the underlying principles. If you're integrating all these features, consider a phased approach, ensuring each feature's stability and security before moving on to the next. Let's proceed with the next set of advanced JWT features: Audience and Issuer, Token Compression, and Integrate with OAuth2. 13. Audience and Issuer:Context: The Implementation: When generating a token, include the
const token = jwt.sign({
_id: user._id,
aud: 'myApp',
iss: 'myServer'
}, process.env.JWT_SECRET);When verifying the token, validate the
if (decodedToken.aud !== 'myApp' || decodedToken.iss !== 'myServer') {
return res.status(403).send('Token audience or issuer is invalid');
}14. Token Compression:Context: If the payload of your JWT becomes too large, consider compressing it. Setup:
Implementation: When generating a token:
const payload = { data: 'your extensive data' };
const compressedPayload = pako.deflate(JSON.stringify(payload), { to: 'string' });
const token = jwt.sign({ data: compressedPayload }, process.env.JWT_SECRET);When verifying a token:
const decompressedPayload = JSON.parse(pako.inflate(decodedToken.data, { to: 'string' }));15. Integrate with OAuth2:Context: OAuth2 is an authorization framework that enables third-party applications to obtain limited access to user accounts on an HTTP service. JWT can be used as bearer tokens in OAuth2 flows. Setup:
npm install passport passport-oauth2 passport-jwtImplementation: Configure Passport with OAuth2 strategy:
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2');
const JwtStrategy = require('passport-jwt').Strategy;
passport.use(new OAuth2Strategy({
authorizationURL: 'https://authorization-server/auth',
tokenURL: 'https://authorization-server/token',
clientID: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
callbackURL: 'http://localhost:5000/auth/callback'
},
(accessToken, refreshToken, profile, done) => {
// Use the accessToken to fetch the user's profile
// Save or update the user in your database
// Call done with the user object
}));
passport.use(new JwtStrategy({
jwtFromRequest: req => req.cookies.jwt,
secretOrKey: process.env.JWT_SECRET
}, (jwtPayload, done) => {
// Find the user based on the JWT payload
// Call done with the user object
}));These advanced features further enhance the robustness and security of your JWT implementation. As always, it's essential to understand the intricacies of each feature and test them thoroughly in real-world scenarios. Implementing all these features requires a comprehensive understanding of JWT, OAuth2, and related technologies. Ensure each feature is stable and secure before integrating the next, and consider seeking expert advice or reviews for best results. Let's delve into the next set of advanced JWT features: HTTPS, Refresh Tokens, and Logging and Monitoring Enhancements. 16. HTTPS:Context: To ensure that JWTs (and all other sensitive data) are transmitted securely, always use HTTPS in production. Setup:
npm install https fsImplementation:
const https = require('https');
const fs = require('fs');
const privateKey = fs.readFileSync('path-to-private-key.pem', 'utf8');
const certificate = fs.readFileSync('path-to-certificate.pem', 'utf8');
const ca = fs.readFileSync('path-to-ca.pem', 'utf8');
const credentials = {
key: privateKey,
cert: certificate,
ca: ca
};
const httpsServer = https.createServer(credentials, app);
httpsServer.listen(443, () => {
console.log('HTTPS Server running on port 443');
});17. Refresh Tokens:Context: Implement refresh tokens to get a new access token without asking the user to log in again. Implementation: When generating tokens, also generate a refresh token:
const refreshToken = jwt.sign({ _id: user._id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' });Store the refresh token in the database: user.refreshToken = refreshToken;
await user.save();Endpoint to refresh the access token: router.post('/refresh', async (req, res) => {
const refreshToken = req.body.token;
if (!refreshToken) return res.sendStatus(401);
const user = await User.findOne({ refreshToken });
if (!user) return res.sendStatus(403);
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, userData) => {
if (err) return res.sendStatus(403);
const accessToken = jwt.sign({ _id: userData._id }, process.env.JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken });
});
});18. Logging and Monitoring Enhancements:Context: Enhance logging to capture more granular details and use monitoring tools to set up alerts for suspicious activities. Setup:
Implementation: Enhance the logger to capture more details:
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message }) => {
return `${timestamp} [${level}]: ${message}`;
})
)
}));Log more granular details in your routes and middleware:
router.post('/login', async (req, res) => {
// ... other code ...
logger.info(`User ${username} logged in from IP: ${req.ip}`);
});
router.post('/refresh', async (req, res) => {
// ... other code ...
logger.warn(`Refresh token used for user ${user.username}`);
});These advanced features and enhancements further solidify the security and robustness of your JWT implementation. As always, it's crucial to stay updated with best practices and potential vulnerabilities related to JWTs and related technologies. Regularly audit your code, and consider seeking expert reviews to ensure the highest level of security and functionality. |
Beta Was this translation helpful? Give feedback.
-
TestingTesting is a crucial aspect of software development, especially for authentication and authorization systems. Let's break down the testing process for the 1. Setup and Configuration:Setup:
npm install jest supertest mongoose-memory-server @shelf/jest-mongodb
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['./tests/setup.js'],
testPathIgnorePatterns: ['/node_modules/', '/client/'],
transform: {
"^.+\\.js?$": "babel-jest"
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1'
},
testMatch: ['**/?(*.)+(test).js'],
verbose: true
};
jest.setTimeout(30000);2. Unit Testing:Unit tests focus on individual parts of the code, like functions or methods.
const User = require('../models/User');
describe('User Model Test', () => {
it('should create a new user', async () => {
const user = new User({ username: 'test', password: 'password123' });
const savedUser = await user.save();
expect(savedUser.username).toBe('test');
});
});3. Integration Testing:Integration tests focus on the interaction between parts of the code, like routes.
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
describe('Auth Routes Test', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({ username: 'test', password: 'password123' });
expect(res.statusCode).toEqual(200);
expect(res.body.username).toEqual('test');
});
// ... other tests for login, JWT validation, etc.
});4. Smoke Testing:Smoke tests ensure the most crucial functions of the app work.
const request = require('supertest');
const app = require('../server');
describe('Smoke Test', () => {
it('should return 200 for the main endpoint', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toEqual(200);
});
});5. Postman Testing:Postman is a tool for API testing. Here's a basic flow:
6. Other Kinds of Testing:
Running the Tests:To run the tests, add the following script to your "scripts": {
"test": "jest"
}Then, in your terminal, run: npm testRemember, while these tests provide a good starting point, always consider edge cases and potential vulnerabilities. Regularly update your tests as you add new features or make changes to the application. Let's delve deeper into each type of testing for the 1. Unit Testing:Unit tests focus on individual parts of the code, like functions or methods. User Model:
const User = require('../models/User');
describe('User Model Test', () => {
it('should create a new user', async () => {
const user = new User({ username: 'test', password: 'password123' });
const savedUser = await user.save();
expect(savedUser.username).toBe('test');
});
it('should not save a user without a username', async () => {
const user = new User({ password: 'password123' });
let err;
try {
await user.save();
} catch (error) {
err = error;
}
expect(err).toBeInstanceOf(mongoose.Error.ValidationError);
});
// ... other tests related to user model validations, methods, etc.
});2. Integration Testing:Integration tests focus on the interaction between parts of the code, like routes. Auth Routes:
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
describe('Auth Routes Test', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({ username: 'test', password: 'password123' });
expect(res.statusCode).toEqual(200);
expect(res.body.username).toEqual('test');
});
it('should not register a user with an existing username', async () => {
await request(app)
.post('/api/auth/register')
.send({ username: 'test', password: 'password123' });
const res = await request(app)
.post('/api/auth/register')
.send({ username: 'test', password: 'password456' });
expect(res.statusCode).toEqual(400);
});
it('should login a registered user', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'test', password: 'password123' });
expect(res.statusCode).toEqual(200);
expect(res.body.token).toBeDefined();
});
it('should not login with incorrect credentials', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'test', password: 'wrongpassword' });
expect(res.statusCode).toEqual(400);
});
// ... other tests for JWT validation, user data retrieval, etc.
});3. Smoke Testing:Smoke tests ensure the most crucial functions of the app work.
const request = require('supertest');
const app = require('../server');
describe('Smoke Test', () => {
it('should return 200 for the main endpoint', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toEqual(200);
});
it('should return 404 for an invalid endpoint', async () => {
const res = await request(app).get('/invalid-endpoint');
expect(res.statusCode).toEqual(404);
});
});4. Postman Testing:For Postman, you'd typically create a collection of requests to test each endpoint. Here's a breakdown:
5. End-to-End Testing:Using a tool like Cypress, you'd simulate user behavior to test the entire flow of the application. This would involve creating scripts that mimic user registration, login, accessing protected routes, etc. 6. Load Testing:Using a tool like Artillery, you'd simulate multiple users accessing the application simultaneously. This would involve creating scenarios where hundreds or thousands of users register, login, and access routes within a specific timeframe. 7. Security Testing:Using a tool like OWASP ZAP, you'd identify vulnerabilities in your application. This would involve scanning your application's endpoints for common vulnerabilities like SQL injection, XSS, CSRF, etc. Remember, while these tests provide a comprehensive coverage, always consider edge cases and potential vulnerabilities. Regularly update your tests as you add new features or make changes to the application. Testing is an ongoing process, and it's crucial to ensure the highest level of security and functionality. Let's create tests for JWT validation, accessing protected routes, and ensuring the JWT is working as expected in an end-to-end flow. 1. Integration Testing:JWT Validation and Accessing Protected Routes:
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');
describe('JWT and Protected Routes Test', () => {
let token;
// Register and login to get a valid JWT for subsequent tests
beforeAll(async () => {
await request(app)
.post('/api/auth/register')
.send({ username: 'test', password: 'password123' });
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'test', password: 'password123' });
token = res.body.token;
});
it('should access a protected route with a valid JWT', async () => {
const res = await request(app)
.get('/api/user')
.set('auth-token', token);
expect(res.statusCode).toEqual(200);
expect(res.body.username).toEqual('test');
});
it('should not access a protected route without a JWT', async () => {
const res = await request(app).get('/api/user');
expect(res.statusCode).toEqual(401);
});
it('should not access a protected route with an invalid JWT', async () => {
const res = await request(app)
.get('/api/user')
.set('auth-token', 'invalidtoken');
expect(res.statusCode).toEqual(403);
});
});2. End-to-End Testing:Using a tool like Cypress, you'd simulate user behavior to test the entire flow of JWT validation.
describe('JWT End-to-End Flow', () => {
it('should register, login, and access protected route', () => {
// Register a user
cy.visit('http://localhost:3000/register');
cy.get('input[name=username]').type('test');
cy.get('input[name=password]').type('password123');
cy.get('button[type=submit]').click();
// Login the user
cy.visit('http://localhost:3000/login');
cy.get('input[name=username]').type('test');
cy.get('input[name=password]').type('password123');
cy.get('button[type=submit]').click();
// Access a protected route
cy.visit('http://localhost:3000/user');
cy.contains('Welcome, test'); // Assuming the protected route displays a welcome message
});
it('should not access protected route without logging in', () => {
cy.visit('http://localhost:3000/user');
cy.contains('Please login to access this page'); // Assuming this message is shown for unauthorized access
});
});To run the Cypress tests, you'd typically use the following command: npx cypress openThis will open the Cypress Test Runner, and you can select the test file ( These tests ensure that JWT validation is working correctly, and only authenticated users with valid JWTs can access protected routes. It's essential to run these tests regularly, especially after making changes to the authentication or authorization logic, to ensure the system's integrity. |
Beta Was this translation helpful? Give feedback.
-
|
amazing👌 |
Beta Was this translation helpful? Give feedback.
-
|
Best tutorial for JWT tokens in MERN |
Beta Was this translation helpful? Give feedback.
-
|
very helpful |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This will be a basic app where users can register, log in, and view a protected resource using JWT for authentication.
Backend Setup:
1. Initialize a new Node.js project:
mkdir mern-jwt-demo cd mern-jwt-demo npm init -y2. Install necessary packages:
3. Setup MongoDB:
4. Create a
.envfile for environment variables:5. Setup Express and MongoDB:
server.js:6. Create User model:
models/User.js:7. Create routes for registration and login:
routes/auth.js:8. Add routes to
server.js:9. Create middleware to verify JWT:
middleware/verifyToken.js:10. Create a protected route:
routes/posts.js:Add this route to
server.js:Frontend Setup:
1. Create a new React app:
npx create-react-app client cd client npm install axios2. Create a Login and Register component:
Login.js:Register.js:3. Create a component to display the protected resource:
Posts.js:4. Add components to
App.jsand setuprouting:
Install react-router-dom:
App.js:5. Setup proxy for the client:
In
client/package.json, add:Running the Application:
cd client npm startThis is a basic MERN stack application with JWT authentication. You can register a user, log in, and access a protected resource. Remember to handle errors and edge cases in a real-world application.
Enhancements and Best Practices:
Backend:
Rate Limiting: Use packages like
express-rate-limitto prevent brute-force attacks.Logging: Use
morganorwinstonfor logging requests and errors.Validation: Use
express-validatororjoito validate user input.Frontend:
State Management: Consider using
ReduxorContext APIfor state management, especially when your app grows.Protected Routes: Use a higher-order component or a custom route to protect frontend routes.
Usage:
Error Handling: Handle API errors gracefully. Show user-friendly error messages.
Loading States: Show a spinner or a loading message while fetching data.
Logout: Implement a logout feature that clears the JWT token from local storage.
Token Expiry: Handle JWT token expiry. If a token is expired, prompt the user to log in again.
HTTPS: Always use HTTPS in production to ensure the JWT token is transmitted securely.
Refresh Tokens: Implement refresh tokens to get a new access token without asking the user to log in again.
Styling: Use libraries like
styled-componentsor frameworks likeBootstraporMaterial-UIto improve the UI.Testing: Write unit and integration tests using libraries like
JestandReact Testing Library.Conclusion:
This guide provides a basic understanding of setting up JWT authentication in a MERN stack application. However, when building a real-world application, always consider security best practices, handle edge cases, and test thoroughly.
Beta Was this translation helpful? Give feedback.
All reactions