User authentication and permission management
Basic Concepts of User Authentication and Permission Management
User authentication and permission management are core functionalities of any modern application. Authentication addresses the question of "Who are you?", while permission management determines "What can you do?". In Mongoose, we can implement these functionalities through schema design, middleware, and plugins.
User Model Design in Mongoose
The user model is the foundation of an authentication system. A typical user schema might include the following fields:
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
validate: [validator.isEmail, 'Invalid email']
},
password: {
type: String,
required: true,
minlength: 8,
select: false
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
lastLogin: Date,
passwordChangedAt: Date,
passwordResetToken: String,
passwordResetExpires: Date
}, {
timestamps: true
});
Password Encryption and Secure Storage
Storing passwords in plaintext is extremely dangerous. We should use libraries like bcrypt to hash passwords:
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
this.passwordChangedAt = Date.now() - 1000;
next();
});
Implementing the Authentication Flow
Implementing a basic login authentication flow:
userSchema.methods.correctPassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
userSchema.methods.createAuthToken = function() {
return jwt.sign(
{ id: this._id, role: this.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
};
// Example login controller
exports.login = async (req, res) => {
const { email, password } = req.body;
// 1) Check if email and password exist
if (!email || !password) {
return res.status(400).json({
status: 'fail',
message: 'Please provide email and password'
});
}
// 2) Check if user exists and password is correct
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.correctPassword(password))) {
return res.status(401).json({
status: 'fail',
message: 'Incorrect email or password'
});
}
// 3) Generate token
const token = user.createAuthToken();
// 4) Send response
res.status(200).json({
status: 'success',
token,
data: {
user
}
});
};
Permission Control and Role Management
Role-Based Access Control (RBAC) is a common approach to permission management:
// Role permission mapping
const rolePermissions = {
user: ['read:own_profile', 'update:own_profile'],
moderator: ['read:any_profile', 'update:any_profile', 'delete:comments'],
admin: ['read:any_profile', 'update:any_profile', 'delete:any_profile', 'manage:users']
};
// Permission check middleware
exports.restrictTo = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
status: 'fail',
message: 'You do not have permission to perform this action'
});
}
next();
};
};
// Usage example - protecting routes
router.get(
'/admin/users',
authController.protect,
authController.restrictTo('admin'),
userController.getAllUsers
);
Advanced Permission Control
For more complex permission requirements, Attribute-Based Access Control (ABAC) can be used:
// Check if user has permission to access a resource
userSchema.methods.canAccessResource = function(resource) {
if (this.role === 'admin') return true;
// Users can only access their own resources
if (resource.user && resource.user.equals(this._id)) return true;
// Specific conditional access permissions
if (this.role === 'moderator' && resource.type === 'comment') return true;
return false;
};
// Usage example
exports.getUser = async (req, res) => {
const user = await User.findById(req.params.id);
if (!req.user.canAccessResource(user)) {
return res.status(403).json({
status: 'fail',
message: 'You do not have permission to access this resource'
});
}
res.status(200).json({
status: 'success',
data: {
user
}
});
};
Password Reset and Account Security
Implementing a secure password reset flow:
userSchema.methods.createPasswordResetToken = function() {
const resetToken = crypto.randomBytes(32).toString('hex');
this.passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
this.passwordResetExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
return resetToken;
};
// Password reset controller
exports.forgotPassword = async (req, res) => {
// 1) Get user
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(404).json({
status: 'fail',
message: 'No user found with this email'
});
}
// 2) Generate reset token
const resetToken = user.createPasswordResetToken();
await user.save({ validateBeforeSave: false });
// 3) Send email
const resetURL = `${req.protocol}://${req.get('host')}/api/v1/users/resetPassword/${resetToken}`;
try {
await sendEmail({
email: user.email,
subject: 'Your password reset token (valid for 10 minutes)',
message: `Please visit the following link to reset your password: ${resetURL}`
});
res.status(200).json({
status: 'success',
message: 'Reset link sent to email'
});
} catch (err) {
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save({ validateBeforeSave: false });
return res.status(500).json({
status: 'error',
message: 'Error sending email. Please try again later'
});
}
};
Session Management and JWT Refresh
Implementing a secure JWT refresh mechanism:
// Generate refresh token
userSchema.methods.createRefreshToken = function() {
return jwt.sign(
{ id: this._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
};
// Refresh token endpoint
exports.refreshToken = async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
status: 'fail',
message: 'No refresh token provided'
});
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
status: 'fail',
message: 'User does not exist'
});
}
const newAccessToken = user.createAuthToken();
res.status(200).json({
status: 'success',
accessToken: newAccessToken
});
} catch (err) {
return res.status(403).json({
status: 'fail',
message: 'Invalid refresh token'
});
}
};
Multi-Factor Authentication Implementation
Adding an extra layer of security:
userSchema.add({
twoFactorEnabled: {
type: Boolean,
default: false
},
twoFactorSecret: String
});
// Enable two-factor authentication
exports.enableTwoFactor = async (req, res) => {
const secret = speakeasy.generateSecret({ length: 20 });
const user = await User.findByIdAndUpdate(
req.user.id,
{
twoFactorEnabled: true,
twoFactorSecret: secret.base32
},
{ new: true }
);
res.status(200).json({
status: 'success',
data: {
otpauthUrl: secret.otpauth_url,
secret: secret.base32
}
});
};
// Verify two-factor code
exports.verifyTwoFactor = async (req, res) => {
const { token } = req.body;
const user = await User.findById(req.user.id);
const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token
});
if (!verified) {
return res.status(400).json({
status: 'fail',
message: 'Invalid verification code'
});
}
// Mark as verified to continue login flow
req.session.twoFactorVerified = true;
res.status(200).json({
status: 'success',
message: 'Two-factor authentication successful'
});
};
Audit Logging and Security Monitoring
Recording critical operations for auditing:
const auditSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.ObjectId,
ref: 'User'
},
action: String,
entityType: String,
entityId: mongoose.Schema.Types.Mixed,
metadata: Object,
ipAddress: String,
userAgent: String,
timestamp: {
type: Date,
default: Date.now
}
});
// Audit logging middleware
userSchema.post('save', function(doc) {
if (doc.isNew) {
AuditLog.create({
user: doc._id,
action: 'user_created',
entityType: 'User',
entityId: doc._id,
metadata: {
email: doc.email,
role: doc.role
}
});
}
});
// Delete operation auditing
userSchema.pre('remove', function(next) {
AuditLog.create({
user: this._id,
action: 'user_deleted',
entityType: 'User',
entityId: this._id
});
next();
});
Performance Optimization and Best Practices
Optimizing the performance of the authentication system:
// Use indexes to speed up queries
userSchema.index({ email: 1 });
userSchema.index({ passwordResetToken: 1 }, { expireAfterSeconds: 0 });
// Optimizing batch operations
exports.deactivateInactiveUsers = async () => {
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const result = await User.updateMany(
{
lastLogin: { $lt: thirtyDaysAgo },
isActive: true
},
{
$set: { isActive: false }
}
);
return result.nModified;
};
// Using lean() to improve query performance
exports.getUserProfile = async (userId) => {
return User.findById(userId)
.select('-password -passwordResetToken -passwordResetExpires')
.lean();
};
Testing and Debugging
Writing test cases to ensure the reliability of the authentication system:
describe('User Authentication', () => {
beforeEach(async () => {
await User.deleteMany();
});
test('Successfully register new user', async () => {
const res = await request(app)
.post('/api/v1/auth/signup')
.send({
username: 'testuser',
email: 'test@example.com',
password: 'password123',
passwordConfirm: 'password123'
});
expect(res.statusCode).toBe(201);
expect(res.body.token).toBeDefined();
const user = await User.findOne({ email: 'test@example.com' });
expect(user).toBeTruthy();
expect(user.password).not.toBe('password123');
});
test('Login failure - wrong password', async () => {
await User.create({
username: 'testuser',
email: 'test@example.com',
password: await bcrypt.hash('correctpassword', 12)
});
const res = await request(app)
.post('/api/v1/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(res.statusCode).toBe(401);
expect(res.body.message).toMatch(/Incorrect email or password/);
});
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:与Express.js的集成
下一篇:日志与审计功能实现