How We Handle Authentication

Authentication and authorization patterns for securing web applications

How We Handle Authentication

Security isn't a feature you add later — it's a mindset that shapes every decision. This guide covers how to think about protecting your application and your users.

The Core Distinction

Authentication answers: "Who are you?"

  • Verifying identity (login)
  • Managing credentials
  • Issuing sessions or JWTs

Authorization answers: "What can you do?"

  • Checking permissions
  • Verifying ownership
  • Enforcing access rules

These are different concerns. Someone can be authenticated (we know who they are) but not authorized (they can't access this resource).

Principles

1. Default to Closed

Every route is protected unless explicitly made public.

The mistake: Building features first, adding security later. You forget one endpoint, and it's exposed.

The principle: Start with everything locked down. Make a conscious decision to open each public route.

How to implement: Use middleware that blocks by default. Maintain a whitelist of public paths, not a blacklist of protected ones.

2. Trust Nothing from the Client

The client is not your friend. It's a potential attacker.

Never trust:

  • User IDs sent in request bodies ("I'm user 123")
  • Role claims from the client ("I'm an admin")
  • Hidden form fields
  • URL parameters for authorization decisions

Always verify on the server:

  • Get the user ID from the session, not the request
  • Check permissions against your database
  • Validate ownership before every operation

3. Filter Data at the Source

Don't fetch all data and filter in application code. Filter in the database query.

The mistake:

Get all projects → Filter to user's projects in code

What if the filter has a bug? All projects leak.

The principle: Every query includes the user filter. The database never returns data the user shouldn't see.

The benefit: Even if application code has bugs, the database layer is a safety net.

4. Hide What You Can't Show

When a user tries to access something they shouldn't, what do you reveal?

"403 Forbidden" says: "This exists, but you can't have it."

"404 Not Found" says: "This doesn't exist (or you can't know it does)."

For resources that belong to users, prefer 404. Don't confirm that user X has a resource the requester was fishing for.

5. Verify Ownership, Not Just Authentication

Being logged in isn't enough. Verify the user owns what they're accessing.

The pattern:

  1. Check: Is the user logged in?
  2. Check: Does this resource belong to them?
  3. Only then: Perform the action

The common mistake: Checking authentication but forgetting ownership. User A can access User B's data by changing an ID in the URL.

Decision Framework

"How should users log in?"

Email/Password when:

  • You need full control over the experience
  • Users expect to create an account with you
  • You can invest in proper password security

OAuth/Social Login when:

  • You want to reduce friction
  • You don't want to manage passwords
  • Your users already have Google/GitHub/etc accounts

Passwordless (magic links, SMS) when:

  • Passwords are too much friction
  • You have a reliable email/SMS delivery
  • Security requirements allow it

Multi-factor when:

  • High-value accounts or sensitive data
  • Compliance requires it
  • The user population will tolerate the friction

"Sessions or tokens?"

Sessions (cookie-based) when:

  • Traditional web app
  • You want simple revocation (delete the session)
  • Server-side storage is acceptable

Tokens (JWT) when:

  • API-first architecture
  • Multiple clients (web, mobile, CLI)
  • Stateless servers are important

The trade-off: Sessions are easier to revoke but require storage. Tokens are stateless but hard to revoke before expiry.

"How long should sessions last?"

Consider:

  • Sensitivity: Bank = short. Blog = long.
  • User friction: How annoying is re-login?
  • Risk window: Longer sessions = longer exposure if compromised

Common patterns:

  • Sensitive apps: Hours, with re-auth for critical actions
  • Normal apps: Days to weeks
  • "Remember me": Weeks to months, with reduced privileges

Sliding expiration: Reset the timer on activity. Users who are active stay logged in.

"Where should I check authorization?"

In middleware for:

  • Route-level access (this page requires login)
  • Role-based access (this section requires admin)

In the route handler for:

  • Resource ownership (this project belongs to you)
  • Action-specific permissions (you can view but not edit)

In the database layer for:

  • Ultimate safety net (queries always filter by user)

Best: All three. Defense in depth.

Common Mistakes

Client-Side Only Checks

Hiding a button doesn't secure the action.

The mistake: "Users can't see the delete button, so they can't delete."

Reality: Anyone can call your API directly. The button is just UI.

The fix: Every security check happens on the server. Client checks are for UX, not security.

Trusting User-Supplied IDs

Letting users tell you who they are.

The mistake: Reading user_id from the request body or URL to decide what data to return.

Reality: Attackers will try every ID.

The fix: Get the user ID from the session. Never from the request.

Inconsistent Enforcement

Some routes are protected, some aren't.

The mistake: Adding auth checks ad-hoc. Missing some routes.

The fix: Centralized middleware that protects by default. Explicit whitelist for public routes.

Verbose Error Messages

Helping attackers understand your system.

The mistake: "User 'admin' exists but password is wrong."

Reality: Now they know the username is valid.

The fix: Generic messages. "Invalid credentials." Let legitimate users figure it out; make attackers guess.

Over-Permissive Tokens

Tokens that can do everything, forever.

The mistake: One token with full access that never expires.

The fix:

  • Scope tokens to specific permissions
  • Short expiration
  • Rotation and revocation capability

How to Evaluate Your Security

Your auth is working if:

  • You can explain where every auth check happens
  • Logged-out users can't access protected routes
  • User A can't access User B's data
  • Changing IDs in URLs doesn't leak data
  • Error messages don't reveal system details

Your auth needs work if:

  • Some routes might be missing protection
  • You're not sure where ownership is verified
  • Users have reported seeing others' data
  • API endpoints work without authentication
  • Tokens never expire

Security Mindset

Assume breach: What's the blast radius if one thing fails? Can you limit damage?

Least privilege: Give the minimum access needed. Expand permissions deliberately.

Defense in depth: Multiple layers. If one fails, others catch it.

Audit trail: Log authentication events. Know who did what, when.

Regular review: Security isn't done. Review auth code regularly. Update dependencies. Rotate secrets.