Most authentication problems are not cryptographic failures. They are design failures: the session is shaped wrong, the token is trusted without verification, or the password is stored in a way that looks fine until it isn’t. The building blocks are not complicated, but the combinations matter.
What authentication actually does
Authentication answers one question: is this person who they claim to be? Authorization is the follow-on question: what are they allowed to do? The two are often conflated in code, but keeping them distinct matters. A user can be authenticated and still lack permission to delete a record. Most access bugs come from treating a valid session as proof of permission it was never meant to grant.
Before writing any auth code, it is worth spending five minutes on the threat model. A public e-commerce store with guest checkout has different requirements than a multi-tenant SaaS with an API. The former needs to protect payment data and prevent account takeover. The latter needs tenant isolation, token revocation, and audit trails. The authentication pattern you pick should match the risk, not just whatever the framework ships by default.
Passwords and how to store them
Passwords are still the default credential for most web apps. The implementation is small but unforgiving.
Store password hashes, never plaintext or reversible encryption. The correct algorithms in 2026 are Argon2id, bcrypt, or scrypt. All three are deliberately slow and memory-intensive, which makes bulk cracking expensive. Argon2id is the current OWASP recommendation; bcrypt is older but still reasonable if the work factor is kept up.
A few practices that compound with a good hash:
- Enforce a minimum length, not a maximum. Complexity rules (at least one capital, one number, one symbol) have weak evidence behind them and push users toward short, predictable substitutions.
- Check new passwords against known-breach lists. The Have I Been Pwned API lets you check a password’s hash prefix without sending the actual value. A one-hour integration that eliminates a large slice of credential-stuffing risk.
- Rate-limit login attempts. A correct hash won’t protect you if an attacker can try ten thousand guesses per second against your endpoint.
Password reset flows deserve the same care as login. A reset link must expire, be single-use, and travel over HTTPS. The reasons why transport security matters here are covered in HTTPS, SSL and TLS explained.
Sessions vs tokens
This is where most teams make their first architectural mistake.
Server-side sessions are the older model. The server generates a random, opaque session ID, stores the session state in a database or cache, and sends the ID to the browser as a cookie. Every request carries the cookie; the server looks up the session. The state lives server-side, so revoking access means deleting the session record. Immediate effect, no stale credentials floating around.
JWTs (JSON Web Tokens) are signed tokens the server issues and the client stores. The server keeps no record. Validity is checked by verifying the signature, which means no database lookup per request. The trade-off: because the server holds no state, revoking a JWT before it expires requires maintaining a blocklist, which adds back the storage you were trying to avoid. A JWT with a 24-hour expiry, issued moments before you revoke a user’s access, stays valid until it expires unless you maintain that blocklist.
The stateless convenience of JWTs doesn’t come free. If you need reliable, immediate revocation, server-side sessions are the simpler answer.
JWTs are well suited for short-lived API access tokens, inter-service calls, and mobile clients where cookie handling is awkward. They are a poor fit as long-lived session mechanisms for web apps where deprovisioning a user needs to take effect right away.
OAuth and delegated login
OAuth 2.0 is the protocol behind “Log in with Google” and “Continue with GitHub.” Understanding what it does, and what it doesn’t, prevents misuse.
OAuth is an authorization protocol, not an authentication protocol. What it provides is a delegated grant: the user tells a third-party identity provider (Google, GitHub, Apple) to give your app a token proving they authenticated there. OIDC (OpenID Connect) is the authentication layer built on top of OAuth that confirms who the user actually is. OAuth without OIDC only proves the user can access the provider, not their identity.
Implementing OAuth correctly means using the authorization code flow rather than the implicit flow (deprecated), validating the state parameter to prevent cross-site request forgery, and verifying the token’s signature against the provider’s published public keys. A well-maintained library, such as Auth.js, Passport, or your framework’s native OAuth support, is much safer than hand-rolling the handshake.
Social login reduces password fatigue and offloads credential storage to a provider with a large security team. The trade-off is dependency on that provider’s availability and their identity decisions. If Google suspends an account, your user is locked out of your app too.
Multi-factor authentication
Passwords alone are not enough for apps where account compromise carries real cost. Multi-factor authentication (MFA) adds a second proof: something the user has, not just something they know.
TOTP (time-based one-time passwords) via an authenticator app is the practical standard. It requires no SMS infrastructure, works offline, and is resistant to SIM-swapping attacks that affect SMS-based verification. Libraries for generating and verifying TOTP codes exist in every major language. Setup is a QR code and a few lines of verification logic.
SMS OTP is better than nothing but has known weaknesses. If you’re protecting a financial product or anything with elevated stakes, direct users toward TOTP or hardware security keys.
Passkeys are worth watching. They replace the password entirely with a device-bound cryptographic credential. Browser support has reached a point where they’re viable as a primary or fallback option. The underlying protocol is phishing-resistant by design, and the authentication flow is simpler for users than a password plus TOTP. If you’re building a new login system today, passkeys deserve a place in your plan.
Choosing where to draw the line
The practical choice most teams face is not which algorithm to use; it’s how much to build versus buy.
Auth providers (Auth0, Clerk, Supabase Auth, AWS Cognito) handle sessions, token rotation, MFA, social login, and a raft of edge cases. They cost money and add a dependency, but they also mean you’re not debugging a JWT expiry edge case at 2 a.m.
Building your own auth from primitives makes sense for teams with security experience, specific compliance requirements, or a product where a third-party handling credentials is a non-starter. It is not a good trade just to avoid a subscription fee.
The risk profile of authentication is asymmetric. Getting it right is invisible. Getting it wrong is a breach, headlines, and the kind of damage to users and reputation that compounds over time.
How Strynal approaches authentication
Authentication is one of the areas where cutting corners at the start costs the most later. When we build websites and apps, our default is to reach for a proven auth library or managed provider for the mechanism, then layer the product-specific rules on top: what constitutes a valid session, when to force re-authentication, how access changes over a user’s lifetime.
The broader question of how your app handles sensitive data sits within the website security basics we think through from the first design session, not as a checklist at the end. And because auth is live code that has to be maintained, the same care carries through into ongoing maintenance: dependency updates on auth libraries matter more than most, because the gap between an old version and a patched one is often an exploitable flaw.
If you’re starting a new build or reviewing an existing auth implementation, we can help with the architecture decisions before a choice gets baked in too deep to change cleanly.