Authentication & Security
Enterprise-grade JWT + Refresh Token authentication with Argon2id hashing, per-session tracking, RBAC, and brute-force protection.
Authentication & Security
The SLMS backend implements a production-grade authentication system using JWT Access Tokens, Argon2id-hashed Refresh Tokens, per-session tracking, and a decoupled RBAC permission model.
Architecture
Client Request
│
▼
┌───────────────┐ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐
│ Rate Limiter │────▶│ authenticate │────▶│ authorize │────▶│ Controller │
│ (Redis-backed)│ │ (JWT Verify) │ │ (Role Check) │ │ (Handler) │
└───────────────┘ └───────────────┘ └──────────────┘ └──────────────┘File Structure
backend/
├── src/modules/auth/
│ ├── auth.controller.js # HTTP handlers (setup, login, register, refresh, logout, changePassword)
│ ├── auth.service.js # Business logic (token generation, password verification)
│ └── auth.routes.js # Route definitions with rate limiters
├── middleware/
│ ├── authMiddleware.js # authenticate & authorize middleware
│ └── rateLimiter.js # Distributed rate limiting (Redis + memory fallback)
├── models/
│ ├── User.js # User schema with security fields
│ ├── Role.js # Role collection
│ ├── Permission.js # Permission collection
│ ├── RolePermission.js # Many-to-many mapping table
│ ├── RefreshToken.js # Hashed refresh tokens with session tracking
│ └── BlacklistedToken.js # Revoked access tokens
└── utils/
└── redisClient.js # Redis connection managementToken Architecture
Access Token (JWT)
Short-lived token sent in the Authorization: Bearer <token> header.
{
"sub": "64b8f3a2e7d1c...",
"roleId": "64b8f...",
"roleName": "Admin",
"sid": "550e8400-e29b-41d4-a716...",
"tokenVersion": 0,
"type": "access",
"iat": 1719945600,
"exp": 1719946500
}| Field | Purpose |
|---|---|
sub | MongoDB User _id |
roleId | Reference to the Role document |
roleName | Human-readable role name |
sid | UUID session identifier (unique per login) |
tokenVersion | Enables global session invalidation |
type | Always "access" — prevents token confusion |
Expiry: 15m (configurable via ACCESS_TOKEN_EXPIRES)
Refresh Token
Long-lived opaque token stored in an HttpOnly cookie.
Format: {userId}.{64-byte-random-hex}
Storage: The raw token is never persisted. Only an Argon2id hash is stored in MongoDB alongside session metadata (IP, device, browser, sessionId).
Expiry: 30 days (TTL index on expiresAt auto-deletes expired records).
Authentication Flows
Registration
POST /api/v1/auth/register
→ Validate input
→ Check duplicate email / rollNumber
→ Hash password with Argon2id
→ Assign "Student" role
→ Create User (status: ACTIVE)
→ Audit log + Welcome email
→ 201 { message: "Registration successful" }Login
POST /api/v1/auth/login
→ Rate limit (IP + Email)
→ Find user by email OR rollNumber
→ Check account status (ACTIVE / SUSPENDED / DISABLED)
→ Check brute-force lock (lockUntil > now?)
→ Verify password with Argon2id
→ On failure: increment loginAttempts, lock at 5 failed attempts for 15 min
→ On success: reset loginAttempts, generate tokens
→ Set refreshToken in HttpOnly cookie
→ 200 { accessToken, user }Token Refresh
POST /api/v1/auth/refresh
→ Extract refreshToken from cookie
→ Parse userId from token prefix
→ Find all RefreshToken records for user
→ Argon2id-verify against each hash
→ Validate expiry and revocation status
→ Delete old token record (rotation)
→ Generate new access + refresh token pair
→ 200 { accessToken } + new cookieLogout
POST /api/v1/auth/logout
→ Extract refreshToken from cookie
→ Find and delete matching RefreshToken record
→ Clear cookie
→ 200 { message: "Successfully logged out" }Password Change
POST /api/v1/auth/change-password (requires authenticate middleware)
→ Verify current password
→ Hash new password with Argon2id
→ Increment tokenVersion (invalidates ALL sessions)
→ Delete ALL RefreshToken records for user
→ 200 { message: "Password updated successfully" }RBAC Model
Roles and Permissions are fully decoupled using a mapping table:
Role ──┐
├── RolePermission (mapping) ── Permission
│
User ──┘ (via roleId reference)| Model | Fields |
|---|---|
Role | name, description |
Permission | name, description, module |
RolePermission | roleId, permissionId (compound unique index) |
Default Roles
| Role | Capabilities |
|---|---|
Admin | Full system access, user management, system configuration |
Librarian | Circulation desk, inventory management, catalogue editing |
Staff | Browse catalogue, manage own loans/reservations |
Student | Browse catalogue, view own loans/fines/reservations |
Security Features
| Feature | Implementation |
|---|---|
| Password Hashing | Argon2id (replaced bcrypt) |
| Refresh Token Hashing | Argon2id — raw tokens never stored |
| Brute Force Protection | 5 failed attempts → 15-minute lock via loginAttempts / lockUntil |
| Token Rotation | Old refresh token deleted on every refresh |
| Global Session Kill | Incrementing tokenVersion invalidates all outstanding JWTs |
| Session Tracking | Each login creates a unique sessionId (UUID v4) |
| Device Fingerprinting | IP address and User-Agent stored per session |
| Token Blacklisting | Revoked access tokens checked against BlacklistedToken collection |
| HttpOnly Cookies | Refresh tokens are httpOnly, secure (in production), sameSite: strict |
Middleware Reference
authenticate
Extracts and verifies the JWT from the Authorization header. Checks the token against the blacklist. Attaches the decoded payload to req.user.
authorize(roles)
Higher-order middleware. Accepts an array of role strings (e.g., ['ROLE_ADMIN', 'ROLE_LIBRARIAN']). Returns 403 if req.user.role is not in the allowed list.
Usage
const { authenticate, authorize } = require('../middleware/authMiddleware');
// Public route
router.post('/login', authController.login);
// Authenticated route
router.post('/change-password', authenticate, authController.changePassword);
// Authorized route
router.post('/', authenticate, authorize(['ROLE_ADMIN', 'ROLE_LIBRARIAN']), bookController.createBook);Database Models
Complete reference for all Mongoose schemas — field definitions, types, validations, virtuals, indexes, and relationships.
Rate Limiting & Redis
Distributed, role-aware rate limiting using rate-limiter-flexible with ioredis, including per-endpoint strategies and Cloudflare-aware IP extraction.