Authentication and Authorization: OAuth, JWT, Sessions, RBAC, 2FA
On one project a JWT token with role admin: false could be changed by client to admin: true — server accepted it without signature verification. This isn't hypothetical attack: several npm packages had JWT library vulnerability ignoring algorithm none. Consequence — full admin access for any registered user.
JWT: What You Really Need to Know
JWT consists of three parts: header (algorithm), payload (data), signature (signature). Signature verifies payload hasn't changed. Without checking signature — just base64-encoded JSON anyone can forge.
Errors we see regularly in code:
Storing in localStorage. localStorage accessible to any JS on page — XSS attack reads token and sends to attacker server. Access token in memory (module variable), refresh token in httpOnly cookie — right scheme.
Long-lived access tokens. Access token for 7 days without revocation. Leaked — 7 days access. Standard: 15 minutes for access token, 30 days for refresh token with rotation. Each refresh token use issues new, old invalidated — if old one used again, detected as Token Reuse Attack, whole token family revoked.
Storing secrets in payload. JWT payload not encrypted, only signed — visible in base64. Passwords, payment data, personal info — not in JWT.
RS256 algorithm (asymmetric) preferable to HS256 (symmetric) in microservice architecture: services can verify token with public key without secret access for creation.
OAuth 2.0 and OpenID Connect
OAuth 2.0 — delegated authorization protocol, not authentication. "Sign in with Google" — OpenID Connect on top OAuth 2.0, adding id_token with user data.
Authorization Code Flow with PKCE — only right flow for browser SPA and mobile apps. Implicit Flow outdated and unsafe. PKCE (Proof Key for Code Exchange) protects against authorization code interception.
OAuth server implementation: don't write from scratch. Keycloak (open source, self-hosted), Auth0, Okta — ready solutions. Laravel Passport or Laravel Sanctum for server apps. NextAuth.js for Next.js — supports 50+ providers out of box.
For B2B products with enterprise customers — SAML 2.0 SSO. Corporate IT often requires this instead of OAuth. @boxyhq/saml-jackson — node.js library for SAML → OAuth2 adapter.
Sessions vs Tokens
Sessions store state on server (Redis, database) — server can instantly revoke. Scaling to multiple instances needs shared store (Redis Cluster). Cookie with session ID — httpOnly, Secure, SameSite=Strict.
Stateless JWT don't require server storage, scale horizontally. But revoking token before expiry — only via blacklist (Redis), partially removing stateless advantage.
For most web apps sessions simpler and safer. JWT makes sense for APIs consumed from mobile app and microservice architecture.
RBAC and Access Policies
Role-Based Access Control — user has roles, roles have permissions. Simple implementation: user → roles → permissions. But once resource authorization appears ("user can edit only own posts"), RBAC gets complex.
Spatie Laravel Permission — Laravel standard: polymorphic roles and permissions, caching, super-admin via gate. Eloquent integration: $user->can('edit posts'), $user->hasRole('editor').
ABAC (Attribute-Based Access Control) — policies based on attributes: user, resource, environment. Needed when access rules complex: "manager can view orders of their region, if order created 24+ hours ago". Casbin — popular cross-language library for ABAC.
ReBAC (Relationship-Based Access Control) — Google Zanzibar model. Access determined by relationship graph: "user X is member of team Y, which has access to project Z". OpenFGA — open source Okta implementation.
Two-Factor Authentication
TOTP (Time-based One-Time Password, Google Authenticator, Authy) — standard. Libraries: otplib (Node.js), pragmarx/google2fa (Laravel). QR code on setup — base32-encoded secret, enough to reproduce code on compromise. Store secret encrypted.
SMS verification — weaker TOTP due to SIM-swapping attacks and SMS unreliability. But users enable more readily. Email OTP — compromise between security and UX.
WebAuthn (Passkeys) — biometric or hardware key instead of password. Private key stored on device, public — on server. No password — no leak. iOS 16+, Android 9+, all modern browsers support. @simplewebauthn/server + @simplewebauthn/browser — good Node.js library.
Backup codes on 2FA setup: 10 one-time codes for recovery if phone lost. Store hashed (bcrypt), show only once on generation.
Common Vulnerabilities
Broken Object Level Authorization (BOLA/IDOR): /api/orders/12345 returns order without checking if belongs to current user. Most common API vulnerability per OWASP. Every resource request — verify via $user->can('view', $order).
Mass Assignment: User::create($request->all()) — user sends is_admin: true in request body. Laravel solves via $fillable / $guarded, but often forgotten.
Unsafe CORS: Access-Control-Allow-Origin: * on API with cookie auth — credentials not sent with wildcard, but if someone set Allow-Credentials: true + Allow-Origin: * — that's a hole.
Process
Authorization architecture designed before development starts, not added later. Choose between sessions and JWT, structure roles and permissions, flow for OAuth providers, plan for 2FA. Penetration testing mandatory for products with financial or personal user data.
Timeline
Basic authentication (email/password + OAuth + JWT/sessions): 1–3 weeks. RBAC with detailed access policies: 2–4 weeks. 2FA (TOTP + SMS): 1–2 weeks. WebAuthn/Passkeys: 2–3 weeks. Full SaaS authentication with multi-tenancy: 4–8 weeks.







