Smart Library Docs

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 management

Token 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
}
FieldPurpose
subMongoDB User _id
roleIdReference to the Role document
roleNameHuman-readable role name
sidUUID session identifier (unique per login)
tokenVersionEnables global session invalidation
typeAlways "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 cookie

Logout

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)
ModelFields
Rolename, description
Permissionname, description, module
RolePermissionroleId, permissionId (compound unique index)

Default Roles

RoleCapabilities
AdminFull system access, user management, system configuration
LibrarianCirculation desk, inventory management, catalogue editing
StaffBrowse catalogue, manage own loans/reservations
StudentBrowse catalogue, view own loans/fines/reservations

Security Features

FeatureImplementation
Password HashingArgon2id (replaced bcrypt)
Refresh Token HashingArgon2id — raw tokens never stored
Brute Force Protection5 failed attempts → 15-minute lock via loginAttempts / lockUntil
Token RotationOld refresh token deleted on every refresh
Global Session KillIncrementing tokenVersion invalidates all outstanding JWTs
Session TrackingEach login creates a unique sessionId (UUID v4)
Device FingerprintingIP address and User-Agent stored per session
Token BlacklistingRevoked access tokens checked against BlacklistedToken collection
HttpOnly CookiesRefresh 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);

On this page