What Is User Authentication? How Login Systems Actually Work
Authentication is how apps verify who you are. Here's how login systems work under the hood — sessions, tokens, verification codes, and roles.
The Problem Every App Has to Solve
Every app that stores user data has the same fundamental problem: how do you know who you’re talking to?
When you type your password and hit “Log in,” something surprisingly complex happens behind the scenes. The app has to verify your identity, remember that you’re logged in across multiple requests, and decide what you’re allowed to do — all without trusting the browser completely or storing passwords in a way that’s dangerous.
That whole process is user authentication. It’s one of the most critical pieces of any full-stack application, and one of the most commonly misunderstood. This article explains how it actually works — from password hashing to session tokens to multi-factor verification — in plain terms.
Authentication vs. Authorization: Two Different Things
These terms get mixed up constantly, so let’s separate them before anything else.
Authentication is about identity. “Are you who you say you are?” It’s the login process — proving you’re the account owner.
Authorization is about permissions. “Are you allowed to do this?” Even after authentication, a logged-in user might not be able to delete records, access admin pages, or view another user’s private data.
Both matter. Both need to be implemented correctly. But they’re separate systems solving separate problems. This article focuses on authentication — the identity verification side. For the permissions side, see how to build a multi-user app with roles and permissions.
How Passwords Actually Work
What apps should never do
First: apps should never store your actual password. If a database gets leaked and passwords are stored in plain text, every affected user’s password is immediately compromised — and since people reuse passwords, the damage spreads beyond the breached app.
Well-built apps don’t store passwords at all. They store a hash of the password.
Hashing and salting
A hash function takes input (your password) and produces a fixed-length string of characters. The key property: it’s a one-way function. You can’t reverse a hash back into the original password.
When you register, the app hashes your password and stores only the hash. When you log in, the app hashes what you typed and compares the hashes. If they match, you’re in.
But there’s a problem: if two users have the same password, they’ll have the same hash. An attacker with a leaked database could precompute hashes for common passwords (“password123” → known hash) and find matches instantly. These precomputed tables are called rainbow tables.
The fix is salting. Before hashing, the app generates a random string (the salt), appends it to your password, then hashes the combination. The salt is stored alongside the hash. Because the salt is unique per user, two users with the same password end up with completely different hashes, and rainbow table attacks don’t work.
Modern apps use dedicated password hashing algorithms like bcrypt, scrypt, or Argon2 — not general-purpose hash functions like SHA-256. The difference is intentional slowness. These algorithms are designed to be computationally expensive, making brute-force attacks impractical even if the database is leaked.
The login check in practice
- User submits email + password via a form.
- The backend looks up the account by email, retrieves the stored hash and salt.
- The backend hashes the submitted password with the stored salt.
- If the result matches the stored hash, authentication succeeds.
- The backend never compares raw passwords — only hashes.
The backend is where all of this happens. If you’re not sure what the backend is doing throughout this process, this plain-English explanation of backends covers the basics.
Sessions: Remembering Who You Are
HTTP is a stateless protocol. Every request is independent — the server doesn’t inherently know who sent it. That means after you log in, the app needs a way to remember that you’re authenticated on every subsequent request.
The classic approach is sessions.
How session-based auth works
- After a successful login, the server creates a session — a record stored on the server (in memory, a database, or a cache like Redis) that says “this session ID belongs to user 42.”
- The server sends a session cookie to the browser containing only the session ID — a long, random string.
- On every subsequent request, the browser automatically sends that cookie.
- The server looks up the session ID, finds the corresponding user, and knows who’s making the request.
The actual session data lives on the server. The cookie is just a lookup key. Even if someone intercepts the cookie value, they can’t decode user information from it (it’s just a random ID).
Cookie security basics
Cookies used for authentication need a few important flags:
- HttpOnly — prevents JavaScript from reading the cookie, blocking a common class of cross-site scripting (XSS) attacks.
- Secure — ensures the cookie is only sent over HTTPS, not plain HTTP.
- SameSite — controls whether the cookie is sent on cross-origin requests, helping prevent cross-site request forgery (CSRF) attacks.
Without these flags, session cookies become a meaningful attack surface.
Tokens: The Stateless Alternative
Sessions require the server to store state — every session record lives somewhere. For large-scale apps or APIs that need to be stateless, tokens are a common alternative.
JSON Web Tokens (JWTs)
A JWT (pronounced “jot”) is a self-contained token that encodes user information directly. It looks like three base64-encoded strings separated by dots:
header.payload.signature
The payload contains claims — things like user ID, email, and expiration time. The signature is created using a secret key held by the server, which makes it tamper-proof. If someone modifies the payload, the signature won’t match.
When a user logs in:
- The server validates credentials.
- The server creates a signed JWT containing the user’s ID and permissions.
- The JWT is sent to the client (usually stored in localStorage or an HttpOnly cookie).
- On every request, the client sends the JWT (typically in an
Authorization: Bearer <token>header). - The server validates the signature — no database lookup required.
Because the token contains everything the server needs, there’s no server-side session storage. This makes JWTs popular for REST API authentication and microservices where multiple backend services need to verify identity without sharing a session store.
The tradeoff
JWTs can’t be invalidated before they expire. If a user’s token is compromised, or a user changes their password, old tokens may still be valid until they expire. Sessions don’t have this problem — you can delete the session record instantly.
Most production systems handle this with short expiration windows (15 minutes to 1 hour) plus a refresh token flow: a longer-lived token used only to get new short-lived access tokens. This limits exposure while preserving stateless architecture.
Multi-Factor Authentication (MFA)
Passwords alone aren’t enough. They get stolen through phishing, reused across breached services, or guessed. Multi-factor authentication adds a second verification step — something you have, not just something you know.
Common second factors
Time-based one-time passwords (TOTP): An app like Google Authenticator generates a 6-digit code every 30 seconds. The code is derived from a shared secret + the current time. The server can reproduce the same calculation and verify the code matches. This is the standard behind the RFC 6238 spec.
SMS verification codes: The server generates a random code and sends it via text message. You enter the code to confirm you control the phone number. It’s convenient but weaker than TOTP — SMS can be intercepted through SIM-swapping attacks.
Email verification codes: Same idea as SMS, but sent to email. Common for “magic link” or “one-time password” login flows that skip passwords entirely.
Hardware security keys: Physical devices (like a YubiKey) that use public key cryptography. The strongest common second factor, resistant to phishing because the key ties verification to a specific domain.
How MFA fits into the login flow
- User submits correct email + password.
- Authentication succeeds, but the server doesn’t fully log the user in yet.
- A partial session or temporary token is issued, flagged as “pending MFA.”
- The user is prompted for a second factor.
- Server verifies the second factor.
- Full session or access token is issued.
If the second factor fails or isn’t submitted, the partial session expires. The user never gains full access.
OAuth and Social Login
Most apps offer “Sign in with Google” or “Sign in with GitHub.” This is OAuth 2.0 — a protocol for delegating authentication to a third party.
How it works
- You click “Sign in with Google.”
- The app redirects you to Google’s login page with a request that includes the app’s client ID and a requested scope (what data the app wants).
- You authenticate with Google and approve the request.
- Google redirects back to the app with an authorization code.
- The app exchanges that code (server-to-server) for an access token.
- The app uses that access token to fetch your profile data from Google.
- The app creates or retrieves your account using your Google ID, then creates its own session.
The app never sees your Google password. Google handles verification. The app just gets a confirmed identity.
Why apps use it
It reduces the burden of credential management. You don’t have to build password reset flows, email verification, or manage leaked credential risks. The tradeoff: you depend on a third-party identity provider, and users need an account with that provider.
Most production apps support both options — traditional email/password and OAuth — because different users have different preferences.
Email Verification and Password Reset
Two flows that confuse developers more than they should.
Email verification
When a user registers with an email address, the app doesn’t know if they actually own that address. Email verification proves it.
The standard approach:
- On registration, generate a random token (a long, unguessable string).
- Store the token in the database, associated with the user account.
- Send an email with a link containing the token (e.g.,
https://app.com/verify?token=abc123). - When the user clicks the link, the server looks up the token, marks the email as verified, and deletes the token.
Tokens should expire (usually after 24 hours) and should be invalidated after use. One-time use only.
Password reset
Same pattern, slightly different purpose:
- User submits their email to the “forgot password” form.
- Server generates a reset token, stores it with an expiration, and sends the link.
- User clicks the link, gets a form to set a new password.
- Server verifies the token is valid and unexpired, updates the password hash, and invalidates the token.
The critical detail: the server should give the same response whether or not the email exists in the database. “If that email is registered, you’ll receive a link” — not “No account found for that email.” The latter lets attackers enumerate valid accounts.
How User Data Is Stored
Authentication data lives in your database. A typical users table includes at minimum:
- id — a unique identifier (auto-increment integer or UUID)
- email — unique, indexed for fast lookups
- password_hash — the bcrypt/Argon2 hash
- email_verified_at — timestamp or null
- created_at — registration timestamp
Related tables handle sessions, reset tokens, OAuth connections, and MFA configuration.
The schema matters. If it’s set up poorly — no unique index on email, tokens that never expire, passwords stored in plain text — the whole system is fragile. Understanding how database schemas structure app data helps you see why these design decisions have real security consequences.
Choosing the right backend platform also matters here. Tools like Supabase come with built-in auth systems that handle most of this correctly out of the box, which is one reason they’ve become popular for new projects.
Roles, Permissions, and What Happens After Login
Authentication answers “who are you?” But right after that, most apps need to answer “what can you do?”
This is authorization, typically implemented through roles.
A user might have a role like admin, editor, or viewer. Each role maps to a set of allowed actions. When a request comes in, the server checks:
- Is the user authenticated? (Valid session or token?)
- Does the user’s role allow this action?
Role checks happen at the backend layer, not the frontend. Hiding a button in the UI isn’t security — the API endpoint still needs to enforce the rule. An attacker making direct API calls won’t care about your hidden buttons.
For a detailed look at implementing this correctly, the guide on building multi-user apps with roles and permissions covers the patterns that hold up in production.
Enterprise Authentication: SSO and Beyond
For apps serving organizations, individual email/password accounts aren’t enough. Companies want centralized identity management — one place to add and remove user access, enforce password policies, and apply security rules.
Single Sign-On (SSO) lets users authenticate once with their company’s identity provider (like Okta, Azure AD, or Google Workspace) and access multiple apps without re-entering credentials. The protocols behind this are typically SAML or OIDC (OpenID Connect).
For enterprise AI applications specifically, SSO integration is often a hard requirement. IT administrators need to be able to deprovision a user from one central place and have that access revoked everywhere. Building this from scratch is complex — it’s a major reason enterprise teams look for platforms with SSO, compliance, and security features built in.
Authentication for AI Agents
Authentication gets more complicated when the user isn’t a human. AI agents — automated systems making API calls on behalf of users or organizations — need their own identity model.
Traditional session/token flows assume a human logging in. But an agent needs credentials that can be scoped, rotated, and audited independently. This is a real gap in current infrastructure — the agent identity problem is something the industry is still actively working out.
At minimum, agent authentication typically involves API keys (long-lived tokens) with scoped permissions, rate limits, and audit logging. But robust agent identity infrastructure goes further — separate credentials per agent, per-task token scoping, and clear audit trails of what each agent accessed and when.
How Remy Handles Authentication
If you’re building an app with Remy, authentication isn’t something you wire up manually. It’s part of what Remy compiles from your spec.
You describe what your app needs — user accounts, email verification, role-based access — in plain, annotated prose. Remy generates a full-stack app with real authentication: bcrypt-hashed passwords, session management, verification code flows, and protected routes. The auth system is integrated with a real SQL database and a real backend, not patched together from disconnected services.
This matters for two reasons. First, auth is one of the most common places where DIY implementations go wrong — not from bad intentions, but from missed edge cases. Second, the hidden infrastructure cost of setting up auth from scratch — email delivery, session storage, token management, security flags — adds up before you’ve built anything that matters to users.
With Remy, you describe the app. The auth layer comes with it.
You can try it at mindstudio.ai/remy.
Frequently Asked Questions
What is the difference between authentication and authorization?
Authentication is identity verification — proving you are who you claim to be (login). Authorization is permission checking — determining what actions you’re allowed to take after you’ve logged in (roles, access control). Both are required in most applications, but they’re separate systems.
Is it safe to store JWTs in localStorage?
It depends on your threat model. localStorage is accessible via JavaScript, which means a cross-site scripting (XSS) attack can read tokens stored there. HttpOnly cookies are generally safer because JavaScript can’t access them. For most apps, storing JWTs in HttpOnly cookies with the Secure and SameSite flags is the recommended approach.
How does “remember me” work?
“Remember me” typically extends session or token lifetime. Without it, a session might expire when the browser closes or after a few hours. With it, the server issues a long-lived session or token (days or weeks) stored in a persistent cookie that survives browser restarts. The tradeoff is longer exposure if the device is stolen or the token is leaked.
What’s the most secure way to handle password resets?
Generate a cryptographically random, single-use token. Store a hash of it (not the token itself) in your database with an expiration timestamp. Send the raw token in the email link. When used, verify the submitted token against the stored hash, update the password, and immediately delete the token. Never reveal whether an email address exists in your system during the reset flow.
Should I build my own authentication system or use a library?
For most projects, use a well-maintained library or managed auth service. Rolling your own from scratch means taking responsibility for every edge case — token rotation, timing attacks, secure cookie handling, MFA, email delivery. Libraries like Passport.js, or services like Auth.js, Supabase Auth, or Clerk, have been battle-tested across thousands of applications. For a practical walkthrough, see how to add authentication to your web app.
What is multi-factor authentication and should every app use it?
MFA requires a second proof of identity beyond a password — usually a time-based code from an authenticator app, an SMS code, or a hardware key. Any app handling sensitive data (financial, medical, personal) should offer MFA, and ideally require it for privileged accounts. It significantly reduces the risk from credential theft, since knowing the password alone isn’t enough to log in.
Key Takeaways
- Authentication verifies identity; authorization controls permissions. They’re related but separate.
- Passwords are hashed and salted — apps store derived values, never plain text.
- Sessions use cookies to persist identity across stateless HTTP requests; tokens encode identity directly in a signed, self-contained string.
- MFA adds a second factor (something you have) on top of something you know, significantly reducing credential-theft risk.
- OAuth delegates authentication to identity providers like Google, reducing credential management burden.
- Enterprise auth (SSO) centralizes identity management so organizations control access from one place.
- Getting auth right is harder than it looks — the edge cases (token expiry, verification flows, CSRF protection) are where most DIY implementations fall short.
If you’re building a full-stack app and want authentication handled correctly from the start, try Remy — it compiles a complete, working auth system directly from your spec.