Authentication in Practice

Last reviewed on 4 May 2026.

The reference page covers what authentication mechanisms exist. This page covers the parts you actually have to get right when you build with them — secret storage, rotation, session lifecycle, error handling, and what to do when things go wrong.

Where credentials should live

The first practical question, ahead of any protocol choice: where does the credential physically live in your client?

Server-side applications

Use environment variables, loaded at process startup from a secrets manager (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, Doppler). Not from a config file checked into the repository — even if the repository is private. Not from a .env file copied to production by hand. The discipline that pays off: your application code reads process.env.API_KEY and doesn't care where the value came from.

The minimal setup for a small project: secrets in your hosting provider's environment-variable UI (Vercel, Netlify, Heroku, Railway). Good enough for a single-developer project. Move to a dedicated secrets manager when the team grows or when you need rotation, audit logging, or fine-grained access control.

Browser applications

You can't keep a secret in a browser. Anything that runs client-side is visible to anyone who opens the dev tools. The pattern: the browser app authenticates the user (via a sign-in flow that goes to your backend), receives a session token, and uses that token for subsequent API calls. The session token is bound to the user, expires quickly, and can be revoked.

If your browser app needs to call a third-party API that requires a secret, the call has to go through your backend. The browser sends the user's session token to your backend; your backend looks up the corresponding stored credentials for that user and makes the third-party call server-side. Never put third-party API keys in client-side code, even temporarily, even "just for testing."

Mobile applications

Native mobile apps have OS-provided secure storage — Keychain on iOS, Keystore on Android. Use it. Do not store tokens in UserDefaults / SharedPreferences in plain text. The OS keystores are designed for exactly this purpose and are accessible only to your app.

For app-bundled secrets (an API key your app uses to identify itself rather than a user), there's a fundamental limit: a determined attacker can extract anything from your app binary. Use the bundled credential as identification, not as a security boundary. If the API key alone authorizes powerful operations, your security model is wrong — those operations need additional checks (per-user authentication, server-side rate limiting per device).

Designing for rotation

Every credential will eventually need to be rotated — because of a leak, because the issuing employee left, because policy requires it. Plan for rotation from day one, because retrofitting it under pressure goes badly.

The two-secret window

The pattern that makes rotation possible without downtime: at any moment, the system accepts two valid credentials (the current one and the new one). Rotation is a sequence:

  1. Issue a new credential. The system now accepts both.
  2. Roll out the new credential to all clients (deploys, config updates, key sharing).
  3. Wait long enough that no in-flight requests are using the old one.
  4. Revoke the old credential. The system now accepts only the new one.

For API keys: the issuing system has to support multiple active keys per identity. For OAuth client secrets: same — most production OAuth providers do. For signing keys (JWT, webhook signatures): publish both during the rotation window and let receivers try both during verification.

Rotation cadence

Conventional wisdom said "rotate everything every 90 days." Modern guidance — including from NIST — is closer to "rotate when there's a reason." Reasons include: you suspect compromise, an employee with access leaves, your detection systems flag suspicious activity, or you've held the same credential for so long you're not sure what depends on it.

Mandatory periodic rotation without compromise often makes things worse: it generates churn, encourages clients to hardcode the rotation in fragile ways, and makes the rotation muscle untested except during emergencies. Set up so rotation is fast and routine, then rotate when there's a real reason.

Session lifecycle for user-facing apps

Login

The user submits credentials (typically email + password, or completes an OAuth flow with an identity provider). On success, the server issues a session token and returns it to the client. The token format depends on your architecture — opaque session ID for stateful sessions, JWT for stateless ones. See the authentication reference for the trade-offs.

Two pieces worth getting right at login:

  • Constant-time comparison. When you check a submitted password against a stored hash, use a constant-time function. String equality is variable-time and leaks information about the prefix through timing.
  • Generic error messages. "Wrong password" and "user doesn't exist" should be the same response. The distinction lets attackers enumerate which email addresses have accounts.

The active session

The client sends the session token with every request. The server validates the token (signature check for JWT, lookup for opaque) and attaches the resulting user identity to the request context for the rest of the request handlers.

Sessions need expiry. The two patterns:

  • Absolute expiry. The session expires N hours after issuance, regardless of activity. Forces periodic re-login. Best for high-value contexts (banking, admin tools).
  • Sliding expiry. The session expires N hours after the last activity. Stays valid while the user is active. Best for general consumer apps where the friction of forced re-login outweighs the security benefit.

Many apps combine both: sliding expiry with an absolute cap (e.g., expires after 8 hours of inactivity OR after 30 days, whichever comes first).

Logout

If sessions are stateful (server-side session store), logout deletes the session record and the token becomes invalid immediately. If sessions are JWT, logout has to either maintain a revocation list or rely on the JWT's expiry time. The cleanest approach: short-lived JWTs (15 minutes) plus refresh tokens, and logout invalidates the refresh token. The next time the access token expires, the user can't get a new one and is effectively logged out. The window of "logged out but still has a valid access token" is bounded by the access token expiry.

Forced logout

Some events should invalidate all of a user's existing sessions everywhere: password change, account compromise, manual security action by an administrator. The pattern: store a token-version number on the user record; include it as a claim in the issued tokens; bump it on the forced-logout event. All previously issued tokens now fail validation because their token-version doesn't match the current one.

OAuth in practice

The redirect URI

OAuth's authorization-code flow returns the user to a redirect URI that you registered with the authorization server. Two practical rules:

  • Exact-match validation. The authorization server must require an exact match (including path and query string) against the registered URI. Wildcard or prefix matching is how authorization codes get stolen.
  • One redirect URI per environment. Don't share a redirect URI between production, staging, and development. A bug in one shouldn't expose the others.

The state parameter

OAuth's state parameter is the CSRF defense. Generate a random value, store it in the user's session, include it in the authorization request, and verify on return that the state matches what you stored. Without this, an attacker can trick a user into authorizing the attacker's account on the user's session.

PKCE

PKCE (Proof Key for Code Exchange) protects against authorization-code interception attacks. The client generates a random code verifier, hashes it, sends the hash with the authorization request, then sends the original verifier when exchanging the code for a token. An attacker who intercepts the authorization code can't exchange it without the verifier. PKCE is required for any client that can't keep a secret (browser apps, mobile apps), and strongly recommended for all clients in 2026.

Handling auth errors well

From the API side

Two failure cases, two distinct status codes (covered in the authentication reference): 401 for "no valid credentials" and 403 for "valid credentials but not allowed". The body should include enough information for the client to recover but not enough to enable attacks:

{
  "error": {
    "code": "token_expired",
    "message": "The access token has expired. Use the refresh token to obtain a new one."
  }
}

Specific failure codes worth distinguishing in your API: missing_credentials, invalid_credentials, token_expired, token_revoked, insufficient_scope. Each one calls for a different client response.

From the client side

Client error handling for auth has three branches:

  • 401 with token_expired. Use the refresh token to get a new access token; retry the original request. If the refresh token also fails, prompt the user to re-authenticate.
  • 401 with invalid_credentials or token_revoked. The credentials aren't recoverable. Drop them; force re-authentication.
  • 403. The user is who they say they are but isn't allowed. Show a "you don't have permission" message; don't try to refresh or re-authenticate (it won't help).

What to do when something leaks

It will happen. A key gets committed to a public repo, an employee's laptop gets stolen, a log line slips through with a token in it. The response procedure should be ready before the incident, not invented during it.

  1. Revoke immediately. Don't wait for a clean rotation. Disable the credential. Yes, this breaks things — that's better than the alternative.
  2. Issue a replacement. Rotate to a new credential. Distribute it to the systems that need it.
  3. Audit usage during the leak window. Check the access logs for the leaked credential between the leak time and the revocation time. Look for activity from unfamiliar IPs, unusual request patterns, requests for resources the legitimate caller wouldn't have asked for.
  4. Determine the blast radius. Did the leaked credential have access to user data? Did it allow writes? Were any writes performed during the leak window? This drives whether you have a customer-notification obligation.
  5. Document what happened and how it leaked. Not for blame; for the postmortem. Most leaks repeat causes (hardcoded secret, bad log, missed scrubbing). Fix the cause, not just the symptom.

Common mistakes

  • Hardcoded secrets in source code. Even in private repositories. Even in branches that "no one will see." Repositories get cloned, forked, exposed in CI logs.
  • Long-lived credentials with broad scope. The longer the lifetime and the wider the scope, the bigger the blast radius when one leaks. Short-lived, narrowly-scoped credentials limit damage.
  • No rotation procedure. The first time you need to rotate is during an incident; that's the worst time to invent the process.
  • Storing passwords with weak hashes. Use bcrypt, scrypt, or argon2 with a sensible work factor. Plain SHA-256 of a password — even with a salt — is unacceptable in 2026.
  • Not invalidating sessions on password change. If the user changed their password because they suspected compromise, the old session shouldn't keep working.
  • Mixing authentication concerns into business logic. Auth checks belong in middleware or a dedicated layer, not scattered through every handler. Centralize so the audit is easy.
  • Logging tokens. A bug in a logger turns into a credential dump. Hash or truncate before logging.

Where to go next

For the protocol-level reference on authentication mechanisms, see API Authentication. For the broader security model, see API security. For rate limiting and authentication failures, see API Rate Limiting Strategies.