Episode 3 — NodeJS MongoDB Backend Architecture / 3.14 — Authentication and Authorization
3.14 — Authentication & Authorization: Quick Revision
Episode 3 supplement -- print-friendly.
How to use
Skim -> drill weak spots in 3.14.a through 3.14.f -> 3.14-Exercise-Questions.md.
Authentication vs Authorization
| Authentication (AuthN) | Authorization (AuthZ) | |
|---|---|---|
| Question | "Who are you?" | "What can you do?" |
| Analogy | ID card at the door | VIP pass to backstage |
| Failure code | 401 Unauthorized | 403 Forbidden |
| Order | First | Second (depends on knowing identity) |
Flow: Request -> authenticate (who?) -> authorize (what?) -> route handler
Password Security (bcrypt)
const bcrypt = require('bcrypt');
// Hash (registration)
const hash = await bcrypt.hash('password123', 12); // 12 salt rounds
// Compare (login)
const isMatch = await bcrypt.compare('password123', hash); // true/false
| Concept | Key Point |
|---|---|
| Never plain text | DB breach = all passwords exposed |
| Hashing | One-way (cannot reverse); use for passwords |
| Encryption | Two-way (can decrypt); NOT for passwords |
| Salt | Random data per password; prevents rainbow tables |
| Salt rounds | 10-12 for production; higher = slower = more secure |
| bcrypt hash format | $2b$12$[22-char salt][31-char hash] |
Mongoose pre-save hook:
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next(); // critical!
this.password = await bcrypt.hash(this.password, 12);
next();
});
Session vs Token
| Session | Token (JWT) | |
|---|---|---|
| State | Stateful (server stores session) | Stateless (client carries token) |
| Storage | Redis / MongoDB (server) | Cookie / memory (client) |
| Scaling | Shared store needed | Any server verifies |
| Invalidation | Easy (delete session) | Hard (blacklist / wait for expiry) |
| Best for | Server-rendered apps | SPAs, mobile, APIs |
Hybrid: Store JWT in httpOnly cookie = stateless + secure.
JWT Structure
Header.Payload.Signature
│ │ │
│ │ └── HMACSHA256(header + "." + payload, secret)
│ └── { userId, role, iat, exp } (NOT encrypted, just base64)
└── { alg: "HS256", typ: "JWT" }
const jwt = require('jsonwebtoken');
// Create
const token = jwt.sign({ userId, role }, SECRET, { expiresIn: '15m' });
// Verify (always use this, NOT jwt.decode)
const decoded = jwt.verify(token, SECRET); // throws on invalid/expired
Access + Refresh pattern:
Access token: short-lived (15m), sent with every request, in memory
Refresh token: long-lived (7d), sent only to /refresh, in httpOnly cookie
Auth Middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, SECRET);
next();
} catch { res.status(401).json({ error: 'Invalid token' }); }
}
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) return res.status(403).json({ error: 'Forbidden' });
next();
};
}
// Usage
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
RBAC summary
guest -> user -> moderator -> admin -> superadmin
(fewer permissions) (more permissions)
| Model | How | Example |
|---|---|---|
| Role-based | Check req.user.role | authorize('admin') |
| Permission-based | Check req.user.permissions | requirePermission('user:delete') |
| Ownership | Check resource.userId === req.user._id | Users edit own profile only |
Passport.js
// Local strategy: email + password
passport.use(new LocalStrategy({ usernameField: 'email' },
async (email, password, done) => {
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password)))
return done(null, false, { message: 'Invalid credentials' });
return done(null, user);
}
));
// Google OAuth: redirect flow
passport.use(new GoogleStrategy({
clientID, clientSecret, callbackURL
}, async (accessToken, refreshToken, profile, done) => {
let user = await User.findOne({ googleId: profile.id });
if (!user) user = await User.create({ /* from profile */ });
done(null, user);
}));
// Session: serialize/deserialize
passport.serializeUser((user, done) => done(null, user._id));
passport.deserializeUser(async (id, done) => done(null, await User.findById(id)));
| Aspect | Session Passport | JWT Passport |
|---|---|---|
| Serialize/deserialize | Required | Not needed |
passport.session() | Required | Not used |
| Usage | Server-rendered apps | APIs, SPAs |
Cookie Security
| Attribute | Value | Purpose |
|---|---|---|
httpOnly | true | JS cannot read (XSS defense) |
secure | true | HTTPS only |
sameSite | 'lax' or 'strict' | CSRF protection |
maxAge | milliseconds | Lifetime |
One-liners
- AuthN = who; AuthZ = what.
- bcrypt = one-way hash + salt + adjustable cost.
- JWT = Header.Payload.Signature; payload is readable, signature is verifiable.
- Access token = short, in memory. Refresh token = long, httpOnly cookie.
- Middleware order:
authenticate -> authorize -> handler. - Passport = strategy pattern;
done(null, user)on success.
End of 3.14 quick revision.