阿里云主机折上折
  • 微信号
Current Site:Index > User authentication and permission management

User authentication and permission management

Author:Chuan Chen 阅读数:7354人阅读 分类: MongoDB

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.