Search…

Authentication and authorization

In this series (16 parts)
  1. How attackers think: the attacker mindset
  2. Networking fundamentals for security
  3. Cryptography fundamentals
  4. Public key infrastructure and certificates
  5. Authentication and authorization
  6. Web application security: OWASP Top 10
  7. Network attacks and defenses
  8. Linux privilege escalation
  9. Windows security fundamentals
  10. Malware types and analysis basics
  11. Reconnaissance and OSINT
  12. Exploitation basics and CVEs
  13. Post-exploitation and persistence
  14. Defensive security: hardening and monitoring
  15. Incident response
  16. CTF skills and practice labs

Authentication asks “Who are you?” Authorization asks “What are you allowed to do?” These are two separate concerns, and confusing them is a common source of security vulnerabilities. This article covers both, from password storage to access control models.

Prerequisites

You should understand cryptography fundamentals, specifically hashing and asymmetric encryption.

Password hashing: doing it right

Passwords should never be stored in plaintext. They should be hashed with a purpose-built algorithm that is deliberately slow.

Why not SHA-256?

SHA-256 is fast. A modern GPU can compute billions of SHA-256 hashes per second. If an attacker steals your database of SHA-256 password hashes, they can try every common password in seconds.

bcrypt

bcrypt is designed for password hashing. It has a configurable work factor that makes it deliberately slow:

# Generate a bcrypt hash (using Python)
python3 -c "
import bcrypt
password = b'MySecretPassword123'
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
print(f'Hash: {hashed.decode()}')
print(f'Rounds: 12 (2^12 = 4096 iterations)')
"

Output:

Hash: $2b$12$LJ3m4ys4Rz5bW8l0kXqJXeN1w2v7q5y9z0aB3cD4eF5gH6iJ7kL8m
Rounds: 12 (2^12 = 4096 iterations)

The hash format: $2b$12$... where 2b is the algorithm version and 12 is the cost factor.

Argon2

Argon2 is the newer recommendation. It is memory-hard, meaning it requires a significant amount of RAM per hash. This makes it resistant to GPU cracking (GPUs have many cores but limited memory per core).

python3 -c "
from hashlib import scrypt
import os
password = b'MySecretPassword123'
salt = os.urandom(16)
# time_cost=3, memory_cost=65536 (64MB), parallelism=4
hashed = scrypt(password, salt=salt, n=65536, r=8, p=1)
print(f'Hash length: {len(hashed)} bytes')
print(f'Salt: {salt.hex()}')
"

Password hashing comparison

AlgorithmGPU-resistantMemory-hardRecommended
MD5✗ Never
SHA-256✗ Not for passwords
bcrypt✓ Good
scrypt✓ Good
Argon2id✓ Best

Multi-factor authentication (MFA)

Something you know (password) + something you have (phone, hardware key) + something you are (biometrics).

MFA dramatically reduces the impact of stolen passwords. Even if an attacker has the password, they need the second factor to log in.

Common second factors:

  • TOTP (Time-based One-Time Password): apps like Google Authenticator generate a 6-digit code every 30 seconds
  • SMS codes: better than nothing, but vulnerable to SIM swapping
  • Hardware keys: FIDO2/WebAuthn devices like YubiKey (strongest option)
  • Push notifications: approve/deny on your phone

OAuth 2.0

OAuth 2.0 is an authorization framework that lets users grant third-party applications access to their resources without sharing their credentials. When you click “Sign in with Google,” you are using OAuth 2.0.

The authorization code flow

sequenceDiagram
  participant U as User
  participant C as Client App
  participant A as Auth Server
  participant R as Resource Server
  U->>C: Click "Login with Google"
  C->>A: Redirect to /authorize
  A->>U: Show login page
  U->>A: Enter credentials
  A->>C: Redirect with auth code
  C->>A: Exchange code for tokens
  A->>C: Access token + refresh token
  C->>R: Request with access token
  R->>C: Protected resource

Step by step:

  1. User clicks “Login with Google” in your app
  2. Your app redirects to Google’s authorization endpoint
  3. User logs in to Google (your app never sees the password)
  4. Google redirects back to your app with an authorization code
  5. Your app exchanges the code for an access token (server-to-server)
  6. Your app uses the access token to access the user’s data

The authorization code flow is the most secure because the access token is never exposed to the browser.

JWT: JSON Web Tokens

A JWT is a compact, URL-safe way to represent claims between two parties. It is commonly used as the access token in OAuth 2.0.

JWT structure

A JWT has three parts separated by dots: header.payload.signature

# Decode a JWT (using Python)
python3 -c "
import base64, json

# Example JWT
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlByYXRpayIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxNjM2NDgwMCwiZXhwIjoxNzE2MzY4NDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'

parts = token.split('.')

# Decode header
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
print('Header:', json.dumps(header, indent=2))

# Decode payload
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
print('Payload:', json.dumps(payload, indent=2))
"

Output:

Header: {
  "alg": "HS256",
  "typ": "JWT"
}
Payload: {
  "sub": "1234567890",
  "name": "Pratik",
  "role": "admin",
  "iat": 1716364800,
  "exp": 1716368400
}

Common JWT mistakes

1. Not verifying the signature

The payload is base64-encoded, not encrypted. Anyone can read it. The signature prevents tampering. If your server does not verify the signature, an attacker can change "role": "user" to "role": "admin".

2. Using algorithm “none”

Some JWT libraries accept "alg": "none", which means no signature. An attacker changes the algorithm to none and removes the signature. The server accepts the token without verification.

3. Short or weak signing keys

If the secret key is short or predictable, an attacker can brute force it. Use at least 256 bits of random data for HMAC keys.

4. Not checking expiration

JWTs have an exp claim. If your server does not check it, tokens are valid forever.

Example 1: Decode a JWT and find the vulnerability

You intercepted this JWT from a web application:

# The token
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiam9obiIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzE2MzY4NDAwfQ.abc123signature"

# Decode it
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null

Output:

{"user":"john","role":"user","exp":1716368400}

The vulnerability: the role is stored in the JWT itself. If the server does not verify the signature properly, an attacker can forge a token:

# Create a malicious payload
echo -n '{"user":"john","role":"admin","exp":1716368400}' | base64 | tr -d '=' | tr '/+' '_-'

Output:

eyJ1c2VyIjoiam9obiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxNjM2ODQwMH0

The attacker replaces the payload portion of the JWT with this admin payload. If the server does not verify the signature, the attacker now has admin access.

Defense: Always verify JWT signatures server-side. Use established libraries (jose, jsonwebtoken). Never implement JWT verification from scratch.

Example 2: Trace an OAuth 2.0 authorization code flow

Let’s walk through a real OAuth 2.0 flow with a hypothetical app:

Step 1: Start the flow

# Your app constructs this URL and redirects the user
echo "https://accounts.google.com/o/oauth2/v2/auth?\
client_id=YOUR_CLIENT_ID&\
redirect_uri=https://yourapp.com/callback&\
response_type=code&\
scope=openid%20email%20profile&\
state=random_csrf_token_abc123"

The state parameter prevents CSRF attacks. Your app generates a random value, stores it in the session, and verifies it matches when Google redirects back.

Step 2: User authenticates with Google

Google shows a login page. The user enters their credentials. Your app never sees them.

Step 3: Google redirects back with a code

# Google redirects to:
# https://yourapp.com/callback?code=AUTH_CODE_XYZ&state=random_csrf_token_abc123

Your app verifies the state matches, then exchanges the code for tokens:

Step 4: Exchange code for tokens

curl -X POST https://oauth2.googleapis.com/token \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "code=AUTH_CODE_XYZ" \
  -d "grant_type=authorization_code" \
  -d "redirect_uri=https://yourapp.com/callback"

Response:

{
  "access_token": "ya29.a0AfH6SM...",
  "expires_in": 3600,
  "refresh_token": "1//0eXyz...",
  "scope": "openid email profile",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiI..."
}

Step 5: Use the access token

curl -H "Authorization: Bearer ya29.a0AfH6SM..." \
  https://www.googleapis.com/oauth2/v3/userinfo

Response:

{
  "sub": "1234567890",
  "name": "Pratik Tiwari",
  "email": "pratik@example.com",
  "picture": "https://lh3.googleusercontent.com/..."
}

RBAC vs ABAC

Once you know who someone is (authentication), you need to decide what they can do (authorization).

RBAC (Role-Based Access Control): assign users to roles, roles have permissions.

RoleRead articlesEdit articlesDelete articlesManage users
Viewer
Editor
Admin

Simple, widely used. Works well when access rules map cleanly to job functions.

ABAC (Attribute-Based Access Control): decisions based on attributes of the user, resource, and environment.

Example rule: “A user can edit a document if they are in the same department as the document owner and it is during business hours.”

ABAC is more flexible but more complex. Use RBAC unless you have a specific need for fine-grained attribute-based decisions.

What comes next

The next article covers Web application security and the OWASP Top 10, where you will see how these authentication mechanisms are attacked in practice.

For the practical side of authentication on Linux, see users, groups, and permissions.

Start typing to search across all content
navigate Enter open Esc close