// feedback Sign in to leave feedback about this page. Sign In →
all topics

Concepts

Identity Management: Concepts & Approaches

What a login system has to provide, how pages become available or not per user, the real threat model, and roll-your-own vs. federated vs. hosted identity.

TL;DR

This series assumes you already have a working LAMP server — see Setting Up a Lightsail Web Server if you don’t yet.

Almost every dynamic site eventually has to answer one question on every request: who is this, and what are they allowed to see? That’s identity management — and it’s worth treating as its own subsystem rather than something you bolt onto each page as you write it. Get the shape right once and every page after it is a one-line check. Get it wrong and you’re fixing the same bug in twenty different files.

What an Identity System Has to Provide

Strip away the specific technology and a login system is really a short list of services:

  • Signup and email verification — proving the account belongs to a real, reachable address before it can do anything.
  • Login and logout — and “sign out everywhere,” not just this browser.
  • Session management — the cookie is a pointer to server-side state, never the credential itself.
  • Password / passkey reset — recovery when the primary credential is lost.
  • Per-page access control — guest, member, and admin tiers, checked consistently.
  • Abuse resistance — rate limiting, lockout, and an audit trail of security-relevant events.
  • Self-service account controls — change password, review or revoke active sessions, delete the account.
Five-box flow diagram: Sign Up (email and password submitted, bcrypt hashes it immediately) leads to Verify Email (confirms the address is real, blocks throwaway signups) leads to Log In (password checked against the hash, session ID regenerated) leads to Session Cookie (httponly, secure, samesite, points to server-side state not a secret) leads to Protected Page (session checked against a role: guest, member, or admin).
The cookie itself is never the secret — it's a pointer to state the server controls.

How Pages Become Available, or Not

The session is the single source of truth for who’s asking. The mistake that causes real bugs isn’t the check itself — if (empty($_SESSION['signed_in'])) is easy to write correctly once. It’s writing that check by hand, on every page, where one missed page is a silent security hole and one changed rule means editing files you forgot existed.

The fix is a small registry: one place that maps a route to the role it requires, checked by one shared function every page calls at the top. Adding a page or changing its access level becomes a one-line edit in one file, not a search-and-replace across the codebase.

// includes/require-role.php
function require_role($role) {
  if ($role === 'admin' && empty($_SESSION['admin'])) {
    header('Location: /sign-in.php');
    exit;
  }
  if ($role === 'member' && empty($_SESSION['signed_in'])) {
    header('Location: /sign-in.php');
    exit;
  }
}

// at the top of any gated page
require_role('member');

Three tiers cover most sites: guest (public), member (signed in), and admin (elevated). That’s the shape Part 2 builds.

The Threat Model

Take this seriously: a login form on the public internet is attacked from the hour it goes live, automatically, by bots that don’t care how small your site is. None of the following requires a targeted attacker — all of it is commodity tooling running at volume.

  • Credential stuffing. An attacker replays email/password pairs leaked from an unrelated breach against your login form, at volume, via bot — betting that some fraction of your users reused a password. See OWASP’s Credential Stuffing Prevention Cheat Sheet.
  • Brute force / password spraying. The same idea with guessed rather than leaked passwords — why rate limiting and account lockout exist at all. Same cheat sheet as above.
  • Session hijacking. Stealing the session cookie — via XSS, a shared or compromised network, or a leaked server log — lets an attacker act as the user with no password needed at all. See OWASP’s Session Management Cheat Sheet.
  • Session fixation. The attacker plants a known session ID before login, then rides it in once the victim authenticates — why regenerating the session ID on login matters, not just setting a cookie. Same cheat sheet as above.
  • CSRF. A logged-in user’s browser is tricked into submitting a state-changing request — change password, transfer funds — to your site from a page they’re viewing elsewhere, riding their existing session. See OWASP’s CSRF Prevention Cheat Sheet.
  • Account enumeration. A login or reset form that responds differently for “wrong password” versus “no such account” hands attackers a free list of valid targets. Covered in OWASP’s Authentication Cheat Sheet.
  • Phishing. The one attack password-based auth structurally can’t stop — a convincing fake login page just collects the password, no exploit required. The concrete reason passkeys are phishing-resistant by design, covered below.
Five rows pairing an attack with its defense: Credential stuffing (leaked pairs replayed at volume) maps to Rate limiting and lockout (cap attempts per IP and per account); Session hijacking (cookie stolen via XSS or a shared network) maps to httponly and secure cookies (script and network sniffing can't read it); Session fixation (attacker plants a session ID before login) maps to regenerating the session ID on login (the planted ID becomes worthless); CSRF (victim's browser submits a hidden request using their session) maps to a per-form CSRF token checked with hash_equals on every POST; Account enumeration (different errors reveal valid emails) maps to generic error messages that respond the same way whether or not the account exists.
Every row here is defended against in Part 2 — none of it is optional.

NIST’s current authentication guidance is worth reading once, even skimmed: it’s the source of the now-standard advice to stop forcing periodic password rotation and arbitrary complexity rules, and instead favor length and checking against known-breached password lists. See NIST SP 800-63B-4, Digital Identity Guidelines: Authentication and Authenticator Management.

Three Approaches, Compared

Roll your own

PHP sessions, MariaDB, bcrypt password hashing. Low effort, no new infrastructure, and it’s already what this site runs. You own every line, which is exactly the right size of security surface for a site like this one — small enough to actually review, unlike a framework's login stack.

Federated / social login

“Sign in with Google/GitHub/Apple” via OAuth. Low-to-medium effort — you write a client flow, not a credential store — and it delegates password security entirely to the provider. The tradeoff: your site now depends on that provider being up, and on the visitor already having an account there.

Hosted identity service

Amazon Cognito, Auth0, Clerk, and similar. Most “secure by default” option, but it's the heaviest to integrate — a new vendor, a cost line, and an SDK dependency — and a poor fit for a no-build-step, no-framework PHP site at this scale.

For this series: roll-your-own is the right default. It matches the site's existing conventions, stays simple at this scale, and is small enough to actually secure end to end — which Part 2 does.

Passwordless: Where Passkeys Fit

A passkey is a WebAuthn/FIDO2 credential: the browser generates a public/private keypair scoped to your site's exact origin. The private key never leaves the device — or its platform sync, such as iCloud Keychain or Google Password Manager. Your server only ever stores a public key, a credential ID, and a signature counter; there is no password equivalent to leak, because there is no shared secret at all.

The “no explicit login” behavior comes from conditional UI: the browser offers a saved passkey as an autofill suggestion the moment a username field is focused, so a return visit is a Face ID or Windows Hello tap, not typing a password. See the W3C WebAuthn Level 3 specification and the FIDO Alliance's passkeys overview.

Side-by-side comparison. Password column: user types password (secret entered on the page), sent to server over TLS (the password itself leaves the device), server hashes and compares (a convincing fake page can just collect it), session created (phishing risk exists at every step above). Passkey column: browser prompts Face ID or PIN (nothing typed or transmitted yet), device signs the server's challenge (the private key never leaves the device), server verifies with public key (bound to this exact site's origin), session created (a fake site can't produce a valid signature).
Origin-binding is why passkeys are phishing-resistant, not just more convenient.

The real complexity lives server-side: challenge/response nonces, verifying the signature counter to catch a cloned authenticator, and supporting multiple credentials per user (phone, laptop, security key). PHP has no WebAuthn support in core — a library is required. And not every visitor has a passkey-capable browser yet, so a fallback path can’t be dropped for a while. Given that, this series treats passkeys as an enhancement layer on top of the password-based system, not a replacement for it — Part 3's job, once Part 2's system exists to add it to.

What This Series Builds

Part 2 builds the system described above end to end: signup, verification, login, logout, bcrypt, CSRF tokens, session cookie flags, rate limiting, and the role registry. Part 3 adds WebAuthn on top of it for passwordless, near-invisible sign-in. Part 4 covers the different problem of authorizing machine clients — API keys, bearer tokens, and OAuth client-credentials — rather than browsers.

Checklist

Understand the service list:
  [ ] Signup, verification, login, logout, sign-out-everywhere
  [ ] Session is a pointer to state, not a credential
  [ ] Password/passkey reset and self-service account controls

Gate pages with a registry, not scattered checks:
  [ ] One shared require_role() function
  [ ] Three tiers: guest, member, admin

Know your threat model:
  [ ] Credential stuffing and brute force
  [ ] Session hijacking and fixation
  [ ] CSRF and account enumeration
  [ ] Phishing — the one password auth can't stop

Pick an approach:
  [ ] Roll-your-own for this series (PHP + MariaDB + bcrypt)
  [ ] Federated login as a lower-effort alternative
  [ ] Hosted identity service only at real scale/compliance need

Passkeys as an enhancement, not a v1 requirement:
  [ ] Conditional UI enables near-invisible re-authentication
  [ ] Origin-binding makes them phishing-resistant
  [ ] Still needs a password fallback during transition
top