Authentication vs Authorization? JWT, OAuth and Session Differences
When you log into an application, two different things are happening — but most developers treat them as one. You can't build a secure system without making this distinction.
The Core Distinction
Authentication — answers the question "Who are you?" It's the process of proving that a user is who they claim to be. Email + password, biometrics, SMS code — these are all authentication mechanisms.
Authorization — answers the question "What can you do?" It's the process of determining which resources and actions an authenticated user can access.
Real-world analogy: showing your ID to a security guard at a building — that's authentication. The card system that determines which floors and rooms you can enter after you're inside — that's authorization. Your identity was verified, but you can't go everywhere.
Authentication: Is this user really ali@example.com? ✓
Authorization: Can ali@example.com access the admin panel? ✗
Session-Based Authentication
Traditional web applications use session-based authentication. When a user logs in, the server creates a session, stores it in a database or Redis, and sends the user a session ID — typically stored in a cookie.
1. User sends email + password
2. Server validates → creates session (session_id: "abc123")
3. Session written to Redis
4. Browser receives Set-Cookie: session_id=abc123
5. Browser automatically sends this cookie with every subsequent request
6. Server looks up session_id in Redis → finds the user
// Session example with Express.js app.post('/login', async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user || !await bcrypt.compare(password, user.passwordHash)) { return res.status(401).json({ error: 'Invalid credentials' }); } req.session.userId = user.id; req.session.role = user.role; res.json({ message: 'Login successful' }); }); // Middleware: is the user logged in? const requireAuth = (req, res, next) => { if (!req.session.userId) { return res.status(401).json({ error: 'Unauthorized' }); } next(); };
The advantage of session-based auth: logout is instant — when the session is deleted, the user is immediately logged out. The disadvantage: the server holds state, every request requires a trip to the session store. Session sharing can be problematic with horizontal scaling (multiple servers).
JWT: Stateless Authentication
JWT (JSON Web Token) is a stateless authentication solution where the server stores nothing. The token itself carries the user information, and the server verifies it each time.
A JWT consists of three parts, separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6ImFsaUBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header — algorithm information (base64 encoded) Payload — user data (base64 encoded, not encrypted!) Signature — header + payload signed with the secret key
// Creating and verifying JWTs import jwt from 'jsonwebtoken'; // Create token const token = jwt.sign( { userId: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: '24h' } ); // Verify token const decoded = jwt.verify(token, process.env.JWT_SECRET); // decoded: { userId: "123", email: "ali@example.com", role: "user", iat: ..., exp: ... }
JWT's big advantage: the server stores nothing. The token carries itself. In microservice architectures, different services can verify the token with the same secret.
Critical warning: JWT payload is not encrypted, only encoded. It's readable with base64 decode. Never put sensitive data like passwords or credit card numbers in the payload.
JWT's biggest disadvantage: invalidating a token is hard. You banned a user, but their token is still valid for 23 more hours. The solution: short-lived access tokens combined with long-lived refresh tokens.
// Access + Refresh Token strategy const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' }); const refreshToken = jwt.sign({ userId }, refreshSecret, { expiresIn: '7d' }); // Refresh token saved to DB (so it can be revoked) await RefreshToken.create({ token: refreshToken, userId });
OAuth 2.0: Third-Party Authorization
When you click "Sign in with Google," OAuth 2.0 kicks in. OAuth is an authorization protocol that allows an application to gain limited access to another service on your behalf.
The OAuth flow (Authorization Code Flow):
1. User clicks "Sign in with Google"
2. App redirects user to Google:
https://accounts.google.com/oauth/authorize?
client_id=YOUR_APP_ID&
redirect_uri=https://yourapp.com/callback&
scope=email profile&
response_type=code
3. User grants permission on Google
4. Google returns to the app with an authorization code
5. App sends this code to Google to receive an access token
6. Access token is used to call Google APIs
Keep this in mind to understand OAuth: OAuth is not an authentication protocol — it's an authorization protocol. It answers "do you allow this app to access your Google data?"
For authentication, OpenID Connect (OIDC) built on top of OAuth is used. When you use social login with Google, GitHub, or Microsoft, OIDC is running in the background.
RBAC: Role-Based Authorization
The most common authorization implementation is Role-Based Access Control. Users are assigned roles, and roles define permissions.
// Role and permission definitions const permissions = { admin: ['read', 'write', 'delete', 'manage_users'], editor: ['read', 'write'], viewer: ['read'] }; // Authorization middleware const requirePermission = (permission) => { return (req, res, next) => { const userPermissions = permissions[req.user.role] || []; if (!userPermissions.includes(permission)) { return res.status(403).json({ error: 'Forbidden: insufficient permissions' }); } next(); }; }; // Usage on routes app.delete('/articles/:id', requireAuth, // first verify who they are requirePermission('delete'), // then check what they can do deleteArticleHandler );
Note the 401 vs 403 distinction: 401 Unauthorized means authentication failed (not logged in), 403 Forbidden means no permission (logged in but not allowed). These two status codes are frequently confused.
Security Best Practices
Never store passwords in plain text. Hash with bcrypt or argon2. Remember that bcrypt is intentionally slow — that's a feature, not a bug. It makes brute force attacks harder.
// Password hashing const hash = await bcrypt.hash(password, 12); // 12 = work factor // Password verification const isValid = await bcrypt.compare(password, hash);
Use HTTPS. Tokens and cookies sent over HTTP can be read by anyone sniffing network traffic.
Use HttpOnly and Secure flags on cookies. HttpOnly prevents JavaScript from accessing the cookie (XSS protection). Secure forces the cookie to only be sent over HTTPS.
res.cookie('session_id', sessionId, { httpOnly: true, // JS cannot access secure: true, // HTTPS only sameSite: 'strict' // CSRF protection });
No application is secure without authentication and authorization set up correctly. Clearly distinguishing these two concepts prevents the majority of security vulnerabilities from the start.