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?"
AnalogyID card at the doorVIP pass to backstage
Failure code401 Unauthorized403 Forbidden
OrderFirstSecond (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
ConceptKey Point
Never plain textDB breach = all passwords exposed
HashingOne-way (cannot reverse); use for passwords
EncryptionTwo-way (can decrypt); NOT for passwords
SaltRandom data per password; prevents rainbow tables
Salt rounds10-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

SessionToken (JWT)
StateStateful (server stores session)Stateless (client carries token)
StorageRedis / MongoDB (server)Cookie / memory (client)
ScalingShared store neededAny server verifies
InvalidationEasy (delete session)Hard (blacklist / wait for expiry)
Best forServer-rendered appsSPAs, 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)
ModelHowExample
Role-basedCheck req.user.roleauthorize('admin')
Permission-basedCheck req.user.permissionsrequirePermission('user:delete')
OwnershipCheck resource.userId === req.user._idUsers 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)));
AspectSession PassportJWT Passport
Serialize/deserializeRequiredNot needed
passport.session()RequiredNot used
UsageServer-rendered appsAPIs, SPAs

Cookie Security

AttributeValuePurpose
httpOnlytrueJS cannot read (XSS defense)
securetrueHTTPS only
sameSite'lax' or 'strict'CSRF protection
maxAgemillisecondsLifetime

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.