notAcalculator logo

Security Fundamentals for Web Developers: Authentication, Authorization, and the Attacks You Need to Prevent

Security fundamentals every web developer should know: authentication vs authorization, JWT deep dive, OAuth 2.0, password hashing, XSS/CSRF/SQL injection prevention, and security headers.

The Developer Who Stored Passwords in Plaintext

In 2012, LinkedIn suffered a breach that exposed 6.5 million password hashes. The hashes were unsalted SHA-1 — a hashing algorithm so fast that an attacker with a modern GPU could crack billions of hashes per hour. Within days, 60% of the passwords were recovered in plaintext. The attackers didn't need sophisticated exploits; they just needed a database dump and a rainbow table[owasp-auth-cheatsheet].

The breach was preventable. Salting (adding random data to each password before hashing) and using a slow hashing algorithm (bcrypt, scrypt, or Argon2) would have made cracking infeasible. These weren't obscure techniques — they were well-documented best practices that LinkedIn's engineering team either didn't know or didn't prioritize. The cost: a $1.25 million settlement, a decade of reputational damage, and millions of users whose credentials were compromised[owasp-auth-cheatsheet].

This is the uncomfortable truth about web security: most breaches aren't caused by sophisticated attacks. They're caused by developers who didn't know the basics. SQL injection, cross-site scripting (XSS), and broken authentication have been on the OWASP Top 10 for over 20 years — and they're still the most common vulnerabilities in production applications[owasp-api-security].

This guide covers the security fundamentals that every web developer should know: the difference between authentication and authorization, how JWTs actually work (and how they're misused), how to hash passwords correctly, how to prevent the OWASP Top 10 vulnerabilities, and which security headers to include in every response. The goal isn't to make you a security engineer — it's to make you a developer who doesn't ship the vulnerabilities that attackers are counting on.

Authentication vs Authorization: Knowing Who vs Knowing What They Can Do

These two terms are confused so often that many developers use them interchangeably. They are fundamentally different questions[owasp-auth-cheatsheet]:

  • Authentication answers: "Who are you?" — verifying identity (username/password, biometrics, API key)
  • Authorization answers: "Are you allowed to do this?" — checking permissions (is this user an admin? does this token have the read:users scope?)

A user who logs in with correct credentials is authenticated. Whether they can delete other users' accounts is an authorization decision. Confusing the two leads to vulnerabilities: a user who is authenticated but not authorized to access an admin endpoint can still access it if you only check "is logged in?"[owasp-authz-cheatsheet].

The principle of least privilege: every user, service, and token should have exactly the permissions it needs — no more. A background job that only reads from a database should not have write access. A frontend API token should not have admin scopes. A user who can view their own profile should not be able to view others'[owasp-authz-cheatsheet].

JWT Deep Dive: What's Inside the Token

JSON Web Tokens (JWTs) are the most common mechanism for stateless authentication in modern web applications. A JWT is a self-contained token that encodes claims (user ID, permissions, expiration) in a JSON payload, signed by the server. The client sends the token in the Authorization: Bearer <token> header, and the server verifies the signature to trust the claims[rfc7519].

A JWT has three parts, each Base64URL-encoded and separated by dots:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNjg3MzA1NjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • Header (eyJhbGciOiJIUzI1NiJ9): algorithm (HS256) and token type
  • Payload (eyJ1c2VyX2lkIjo0MiwiZXhwIjoxNjg3MzA1NjAwfQ): claims — user_id, expiration, roles
  • Signature (SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c): HMAC of header+payload with server secret

JWT ≠ encryption. The payload is signed, not encrypted — anyone who intercepts the token can read the claims (that's why you should never put secrets in a JWT). The signature only guarantees that the token was issued by someone with the server secret and hasn't been tampered with[rfc7519].

The critical claim is exp (expiration). A JWT without an expiration is valid forever — if leaked, it's a permanent access token. Set short expirations (15 minutes to 1 hour) and use refresh tokens for longer sessions. The JWT Decoder parses tokens client-side, showing the header, payload, and signature — useful for debugging authentication flows and verifying that your tokens contain the expected claims[auth0-jwt].

Common JWT mistakes:

  1. Storing sensitive data in the payload. JWTs are signed, not encrypted. Anyone can read them.
  2. Not validating the signature on the server. If you trust the payload without verifying the signature, anyone can forge tokens.
  3. Using alg: "none". Some JWT libraries accept tokens with no algorithm, bypassing signature verification entirely. Always pin the allowed algorithms server-side.
  4. Long expirations without refresh. A token that's valid for 30 days is a 30-day window if leaked.

Password Hashing: Why Plaintext Is a Crime

Storing passwords in plaintext is the most basic security mistake — and it still happens. When a database is breached (and databases are breached), plaintext passwords are immediately usable. The fix is hashing: a one-way function that converts a password into a fixed-length string that cannot be reversed[owasp-auth-cheatsheet].

Not all hashing is equal. Fast hash functions (MD5, SHA-1, SHA-256) are designed for speed — which is exactly what you don't want for passwords. An attacker with a modern GPU can compute billions of SHA-256 hashes per second. A slow hash function (bcrypt, scrypt, Argon2) is designed to be computationally expensive, making brute-force attacks infeasible[owasp-auth-cheatsheet].

The algorithm comparison:

MD5~180 billion❌ NeverBroken, too fast
SHA-1~6 billion❌ NeverBroken, too fast
SHA-256~2 billion❌ NeverToo fast for passwords
bcrypt~100,000✅ YesAdaptive cost factor
scrypt~10,000✅ YesMemory-hard
Argon2~1,000✅ Yes (best)Memory-hard, winner of Password Hashing Competition

Salting adds random data to each password before hashing, so two users with the same password have different hashes. This defeats rainbow tables (precomputed hash databases). Modern algorithms (bcrypt, Argon2) handle salting automatically[owasp-auth-cheatsheet].

The Password Generator creates strong random passwords with configurable length and character sets — useful for generating initial passwords, API keys, or test data. But remember: the generator creates the password; bcrypt/Argon2 protects it in storage.

The OWASP Top 10: Attacks Every Developer Should Prevent

The OWASP Top 10 lists the most critical web application security risks. The ones every developer should know how to prevent[owasp-api-security]:

Broken Object-Level Authorization (BOLA)

The most common API vulnerability. If GET /users/42 returns user 42's data, but the server doesn't check that the authenticated user is user 42 (or an admin), then any authenticated user can access any other user's data by changing the ID in the URL. Every endpoint that takes an ID must check authorization[owasp-api-security].

Fix: Always verify that the authenticated user is authorized to access the specific resource they're requesting. Don't rely on clients to only request their own data.

Cross-Site Scripting (XSS)

An attacker injects malicious JavaScript into a page that other users view. The script runs in the victim's browser, stealing cookies, session tokens, or performing actions on their behalf. Three types:

  • Reflected XSS: the malicious script is part of the URL (e.g., ?search=<script>...</script>)
  • Stored XSS: the script is stored in the database (e.g., in a comment or post)
  • DOM-based XSS: the vulnerability is in client-side JavaScript that unsafely manipulates the DOM

Fix: Escape all user input before rendering it in HTML. Use a framework that auto-escapes by default (React, Vue, Angular). Set the Content-Security-Policy header to restrict which scripts can execute[mdn-web-security].

Cross-Site Request Forgery (CSRF)

An attacker tricks a user's browser into making a request to a site where the user is authenticated. If a bank transfer is triggered by POST /transfer?to=attacker&amount=1000, and the user visits a malicious page while logged in, the browser sends the request with the user's cookies — and the transfer happens[mdn-web-security].

Fix: Use CSRF tokens — a unique, unpredictable value embedded in every form and verified on the server. Modern frameworks (Rails, Django, Spring) include CSRF protection by default. For APIs, require a custom header (e.g., X-Requested-With) that cross-origin requests cannot set[mdn-web-security].

SQL Injection

An attacker injects SQL code through user input. If a query is built by string concatenation — SELECT * FROM users WHERE name = ' + userInput + ' — an attacker can input ' OR '1'='1 to bypass authentication, or '; DROP TABLE users; -- to destroy data[mdn-web-security].

Fix: Use parameterized queries (prepared statements) exclusively. Never concatenate user input into SQL. ORMs (Sequelize, SQLAlchemy, Entity Framework) use parameterized queries by default — but raw SQL queries are still vulnerable if not parameterized[mdn-web-security].

Security Misconfiguration

Default credentials, exposed error messages, unnecessary features enabled, missing security headers. The 2017 Equifax breach was caused by an unpatched Apache Struts vulnerability — a known exploit for which a patch had been available for months[nist-cyberframework].

Fix: Harden your deployment: disable default accounts, remove unused features, keep dependencies updated, and include security headers in every response.

Security Headers: The Headers Every Response Should Include

HTTP security headers tell the browser how to handle your content. They're free protection — one line of configuration each[mdn-web-security]:

Strict-Transport-SecurityForce HTTPS for all future requestsmax-age=31536000; includeSubDomains
X-Content-Type-OptionsPrevent MIME type sniffingnosniff
X-Frame-OptionsPrevent clickjacking (your page in an iframe)DENY or SAMEORIGIN
Content-Security-PolicyRestrict which resources can loaddefault-src 'self'; script-src 'self'
X-XSS-ProtectionEnable browser XSS filter (legacy)1; mode=block
Referrer-PolicyControl how much referrer information is sentstrict-origin-when-cross-origin
Cache-ControlPrevent caching of sensitive datano-store for authenticated responses

The most important: Strict-Transport-Security (HSTS) ensures that browsers always use HTTPS for your domain, preventing SSL stripping attacks. Once set, a user's browser will refuse to connect via HTTP for the duration of max-age[mdn-web-security].

Session Management: Stateful vs Stateless

Stateful sessions store session data on the server (in memory, Redis, or a database) and send the client a session ID cookie. The server looks up the session on every request. Pros: easy to invalidate, can store large data. Cons: requires shared session storage in a multi-server setup, memory overhead[owasp-auth-cheatsheet].

Stateless sessions (JWTs) encode all session data in the token itself. The server doesn't store anything. Pros: no shared storage needed, works across services. Cons: cannot invalidate individual tokens before expiration, token size grows with claims[rfc7519].

The hybrid approach (most common in production): use a short-lived JWT (15-60 minutes) for stateless authentication, plus a long-lived refresh token (stored server-side) to issue new JWTs. This gives you the scalability of stateless tokens with the ability to revoke access via the refresh token[auth0-jwt].

Practical Tips for Secure Development

  1. Never roll your own crypto. Use established libraries (bcrypt, Argon2, libsodium). Cryptography is easy to get wrong in ways that are invisible until you're breached.
  2. Use HTTPS everywhere. No exceptions. Even internal APIs should use TLS. Use Let's Encrypt for free certificates.
  3. Validate input on the server. Client-side validation is for UX; server-side validation is for security. Never trust client input.
  4. Keep dependencies updated. Use npm audit, Snyk, or Dependabot to flag known vulnerabilities in your dependencies.
  5. Log security events. Failed logins, permission denied errors, and unusual patterns should be logged and monitored.
  6. Use environment variables for secrets. Never commit API keys, database passwords, or JWT secrets to version control.
  7. Implement rate limiting on authentication endpoints. Brute-force attacks try thousands of passwords per second. Rate limit to 5-10 attempts per minute per IP[owasp-auth-cheatsheet].

Limitations: What This Guide Doesn't Cover

This guide covers the fundamentals — the vulnerabilities that every developer should prevent. It does not cover:

  • Penetration testing — actively trying to break your own application
  • Threat modeling — systematically identifying attack vectors
  • Compliance — GDPR, HIPAA, PCI-DSS requirements
  • Infrastructure security — network segmentation, firewalls, intrusion detection
  • Cryptography engineering — designing custom protocols (don't)

For production applications handling sensitive data, hire a security professional. The fundamentals prevent the most common attacks, but determined attackers with resources require defense in depth[nist-cyberframework].

Frequently Asked Questions

What is the difference between authentication and authorization?
Authentication answers 'who are you?' (verifying identity). Authorization answers 'are you allowed to do this?' (checking permissions). A user can be authenticated (logged in) but not authorized (not an admin).
What is a JWT?
A JSON Web Token is a self-contained token that encodes claims (user ID, permissions, expiration) in a JSON payload, signed by the server. JWTs are signed, not encrypted — never put secrets in them.
How should I store passwords?
Hash them with a slow algorithm: bcrypt, scrypt, or Argon2. Never use fast hashes (MD5, SHA-1, SHA-256) for passwords. Never store plaintext.
What is the most common web vulnerability?
Broken Object-Level Authorization (BOLA) — every endpoint that takes an ID must check that the authenticated user is authorized to access that specific resource.
What is XSS?
Cross-Site Scripting — an attacker injects malicious JavaScript into a page that other users view. Fix by escaping all user input and setting Content-Security-Policy headers.
What is CSRF?
Cross-Site Request Forgery — an attacker tricks a user's browser into making a request to a site where they're authenticated. Fix with CSRF tokens and SameSite cookies.
What is SQL injection?
An attacker injects SQL code through user input. Fix by using parameterized queries (prepared statements) exclusively — never concatenate user input into SQL.
What security headers should I include?
At minimum: Strict-Transport-Security (force HTTPS), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Content-Security-Policy to restrict resource loading.

References

  1. [1]OWASP. API Security Top 10.
  2. [2]OWASP. Authentication Cheat Sheet.
  3. [3]OWASP. Authorization Cheat Sheet.
  4. [4]Jones, Michael, John Bradley, and Nat Sakimura. "JSON Web Token (JWT)." RFC 7519, 2015.
  5. [5]MDN Web Docs. Web Security.
  6. [6]Auth0. JSON Web Tokens.
  7. [7]National Institute of Standards and Technology. Cybersecurity Framework.
Give us your feedback! Was this useful?
1b

UnByte — Independent Software Engineering

All reference data cites its sources — Editorial policy