Authentication and authorization
In this series (16 parts)
- How attackers think: the attacker mindset
- Networking fundamentals for security
- Cryptography fundamentals
- Public key infrastructure and certificates
- Authentication and authorization
- Web application security: OWASP Top 10
- Network attacks and defenses
- Linux privilege escalation
- Windows security fundamentals
- Malware types and analysis basics
- Reconnaissance and OSINT
- Exploitation basics and CVEs
- Post-exploitation and persistence
- Defensive security: hardening and monitoring
- Incident response
- 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
| Algorithm | GPU-resistant | Memory-hard | Recommended |
|---|---|---|---|
| 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:
- User clicks “Login with Google” in your app
- Your app redirects to Google’s authorization endpoint
- User logs in to Google (your app never sees the password)
- Google redirects back to your app with an authorization code
- Your app exchanges the code for an access token (server-to-server)
- 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.
| Role | Read articles | Edit articles | Delete articles | Manage 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.