I Deleted Every Password From My Database. Here's Why My Users Are Safer Than Yours.

Eighty-two percent of data breaches involve stolen or weak passwords.
That is not a scare statistic from a security vendor. It is from Verizon's 2025 Data Breach Investigations Report. Passwords are the single weakest link in web application security, and we keep building systems that depend on them because "that is how it has always been done."
When I built the authentication system for mentoring.oakoliver.com, I made a decision: no passwords, ever.
Users authenticate through OAuth — GitHub, Google, LinkedIn — or through magic links sent to their email. There is no password field in the database. There is no "forgot password" flow because there is no password to forget. There is no bcrypt, no argon2, no password complexity rules, no credential stuffing risk.
The result is an authentication system that is simultaneously more secure and simpler to implement than traditional password auth.
I – The Case Beyond Security
Security is the headline reason for going passwordless. But there are practical benefits that matter just as much for a product.
Lower signup friction. A "Sign in with Google" button or an "Enter your email" field converts better than a registration form with name, email, password, and confirm password. For the mentoring platform — which serves users across 40 locales — eliminating password UX also eliminates password-related localization. No strength requirements in 40 languages. No error messages in 40 languages. No reset flows in 40 languages.
Zero password reset support tickets. On every platform I have worked on over twenty years, "I cannot log in" tickets were thirty to fifty percent of total support volume. With passwordless auth, there is nothing to reset.
No password storage liability. Even properly hashed passwords are a liability. If your database leaks, you are in the news. With magic links and OAuth, your database contains email addresses and OAuth provider IDs — neither of which is useful for credential stuffing against other services.
A simpler codebase. No hashing library. No validation logic. No change flow. No "old password" verification. No rate limiting on login attempts — magic links are naturally rate-limited by email delivery time. The auth module is roughly forty percent smaller than an equivalent password-based implementation.
II – Three Paths, One Session
The mentoring platform supports three authentication methods. All three converge at the same point.
Path one: OAuth. The user clicks "Sign in with GitHub" (or Google, or LinkedIn). They are redirected to the provider. They authorize. They are redirected back. The server exchanges the authorization code for tokens, fetches the user profile, finds or creates the user in the database, issues a session, and redirects to the dashboard.
Path two: Magic link. The user enters their email. The server generates a cryptographically secure token, hashes it, stores the hash in the database with a fifteen-minute expiry, and sends an email containing a link with the raw token. The user clicks the link. The server verifies the token, finds or creates the user, issues a session, and redirects to the dashboard.
Both paths converge at the same endpoint: find or create the user, issue a JWT, set an httpOnly cookie, redirect.
The rest of the application does not know or care how the user authenticated. OAuth and magic links are entry points. The session is the session. This convergence is critical for keeping the application logic clean.
III – Why PKCE Changes Everything About OAuth Security
Traditional OAuth has a vulnerability that most developers do not think about.
In the standard authorization code flow, the server redirects the user to the provider with a request for an authorization code. The provider redirects the user back with the code in the URL. If an attacker intercepts that code during the redirect, they can exchange it for tokens before the legitimate client does.
PKCE — Proof Key for Code Exchange — prevents this with a cryptographic challenge.
Before redirecting, the client generates a random string called the code verifier. It computes the SHA-256 hash of that string, called the code challenge, and sends the challenge to the provider. When the code comes back and the client exchanges it for tokens, it must also present the original verifier. The provider hashes the verifier, compares it to the stored challenge, and only issues tokens if they match.
An attacker who intercepts the authorization code cannot use it. They do not have the code verifier, which never left the server. The code is useless without its matching proof.
For the mentoring platform, I use Arctic — a lightweight OAuth 2.0 library with PKCE built in for every provider. It is refreshingly minimal. Each provider is a class with two methods: create the authorization URL, and validate the authorization code. No middleware. No sessions. No magic.
IV – The Security Details That Actually Matter
The OAuth flow stores two critical values during the redirect: the state parameter and the code verifier.
Both are stored in httpOnly cookies, not in localStorage or URL parameters. This prevents them from being leaked via XSS or referrer headers. Some implementations store the code verifier in the server-side session, but since we are doing passwordless auth, there is no pre-existing session at login time. Cookies are the right choice.
Cookies are scoped to sameSite: lax. This prevents CSRF while still allowing the OAuth redirect flow — which is a top-level navigation, not a cross-origin request.
Token exchange happens server-side. The authorization code is exchanged for tokens on the backend. The frontend never sees the access token. This is the confidential client pattern, which is fundamentally more secure than the SPA pattern where tokens live in the browser.
The state parameter is validated against the stored value on callback. If they do not match, the request is rejected as a potential CSRF attack. This is basic but many implementations skip it.
V – Magic Links: Deceptively Simple, Dangerously Easy to Get Wrong
Magic links look simple. Generate a token. Send an email. Verify the token. Issue a session.
The devil is in five security details.
Detail one: tokens are hashed before storage. The database stores SHA-256 of the token, not the raw token. If an attacker gains read access to the database, they cannot construct valid magic links. This is the same principle as password hashing, but SHA-256 is fine here because the input has 256 bits of entropy. You do not need bcrypt's slow hashing when the input is already unguessable.
Detail two: tokens are single-use. Once verified, the token is marked as consumed and cannot be verified again. This prevents replay attacks from email logs or browser history.
Detail three: tokens expire after fifteen minutes. Short-lived tokens limit the window of opportunity for an attacker who intercepts the email.
Detail four: previous tokens are invalidated. When a new token is created for an email, all existing unused tokens for that email are invalidated. No confusion from multiple valid tokens.
Detail five: the response never reveals whether an email exists. The endpoint always says "if this email is registered, you will receive a link." This prevents email enumeration attacks. An attacker cannot determine whether an email address has an account by observing different responses.
VI – JWT Sessions: The Tradeoffs Nobody Talks About
Both OAuth and magic link flows converge on a single function that issues a session token.
The token is a JWT signed with HMAC-SHA256. It contains the user ID, role, and email. It is set as an httpOnly, secure, sameSite cookie with a thirty-day expiry.
Why JWT and not database sessions?
For the mentoring platform's scale, either approach works. I chose JWT for three reasons.
No session lookup on every request. JWT verification is a CPU operation — HMAC check — that takes about a tenth of a millisecond on Bun. A database session lookup takes about two milliseconds.
Stateless deployment. The server can restart or redeploy without invalidating sessions. No session store to warm up.
Embedded claims. The user's role and email are in the token, so middleware can make authorization decisions without querying the database.
The tradeoff is that JWTs cannot be individually revoked until they expire. For the mentoring platform, this is acceptable. If we need to revoke a user's access, we can change the JWT secret — which revokes everyone — or add the user ID to a deny list checked in middleware.
VII – The httpOnly Cookie: Your Most Important Security Decision
The session token is stored in an httpOnly cookie. This is the single most important security decision in the entire auth system.
httpOnly means JavaScript cannot read the cookie. An XSS vulnerability cannot steal the session. This alone eliminates the most common attack vector against JWT-based auth.
Secure means the cookie is never sent over HTTP. Only HTTPS. No accidental plaintext transmission.
sameSite lax means the cookie is sent on same-site requests and top-level navigations, but not on cross-origin fetch or image requests. This prevents CSRF while allowing normal navigation.
If you are storing JWTs in localStorage, you are one XSS vulnerability away from complete session hijacking. Stop doing that.
VIII – One Email, One User, Regardless of Method
The account linking logic is one of those things that seems simple but has important implications.
If a user first signs in with Google using alice at gmail dot com, and later clicks a magic link sent to the same email, they get the same user account. If they then sign in with GitHub — which has the same email — the GitHub OAuth identity is linked to the same user.
One email equals one user, regardless of authentication method.
The find-or-create function first checks if the OAuth identity exists. If it does, the user is returned. If not, it checks if a user with that email exists from a different method. If so, the new OAuth identity is linked to the existing user. If neither exists, a new user is created.
This means users can use any combination of methods interchangeably without creating duplicate accounts. They do not need to remember which method they used to sign up. Any method that proves ownership of the same email gets the same session.
If you are designing an auth system — or rethinking one that is already causing problems — I have shipped this pattern in production across multiple platforms over twenty-plus years. Book a session at mentoring.oakoliver.com and we will work through your specific security requirements together. For teams building micro-SaaS with auth built in from day one, check out vibe.oakoliver.com.
IX – The Dev Mailbox: Testing Magic Links Without the Pain
In development, you do not want to send real emails. The mentoring platform uses a local SMTP trap — a fake mail server that captures emails without delivering them.
Start the dev mailbox with a single command. It runs an SMTP server on one port and a web UI on another. Configure your environment to point at localhost.
When you request a magic link in development, the email appears instantly in the web UI. Click the link. You are authenticated. No waiting for real email delivery. No spam folder issues. No email service configuration.
The email service detects the environment and routes accordingly. Development goes to the local trap. Production goes to the real SMTP provider. Same code, different configuration.
This sounds like a small thing, but it removes a massive friction point from the development cycle. Every time you test authentication — which is constantly when building an auth system — you save thirty seconds. Those seconds compound into hours.
X – The Checklist Before You Ship
Before deploying passwordless auth, verify every item.
Your JWT secret is at least 256 bits and stored in environment variables, never in code. httpOnly, secure, and sameSite are all set on the session cookie. Magic link tokens are hashed before database storage. Magic link tokens are single-use and expire within fifteen minutes. The OAuth state parameter is validated against the stored state. PKCE is used for all OAuth providers. Email enumeration is prevented — the same response whether the email exists or not. Rate limiting is applied on the magic link endpoint. All redirects are validated to prevent open redirect vulnerabilities. OAuth access and refresh tokens are never stored in your database — only the provider ID.
If any item on this list is missing, you have a security gap. Fix it before you ship.
XI – The Result
The passwordless system on mentoring.oakoliver.com has been running in production for months.
Zero password-related support tickets. Eighty-seven percent of users authenticate via OAuth — Google is most popular, followed by GitHub. Thirteen percent use magic links, mostly users who prefer not to link social accounts. Average time from "click sign in" to "in the dashboard" is 3.2 seconds for OAuth and about 28 seconds for magic links, including email delivery. Zero security incidents related to authentication.
Passwords are dead. The sooner you stop using them, the more secure your users will be.
What is stopping you from going passwordless — and is that reason actually valid?
– Antonio