Back to Blog
OWASP

Auth Failures (OWASP A07): Examples, Patterns & Fixes

April 25, 202622 min readSecureCodingHub Team
identity::verify()

Identity is the gate every other security control depends on. Strong authorization, hardened cloud configuration, careful input validation — none of it matters if an attacker can sign in as someone else. Owasp authentication failures — A07 in the current Top 10, also called identification and authentication failures — captures the long tail of ways that gate fails: weak password storage, credential stuffing against billion-record breach corpora, broken or absent MFA, predictable session identifiers that survive logout, JWTs accepted with disabled signatures, OAuth flows that skip PKCE on public clients, and recovery paths that quietly downgrade to the weakest factor a user enrolled. Every one of these is a failure at the proof-of-identity step, and every one routes around the rest of an application's security model. This guide walks the formal definition of A07, the patterns developers actually meet at code- and design-review time, the cryptographic and protocol-level mistakes that recur across stacks, and the prevention playbook that turns identity from a recurring incident category into a managed property of the platform.

Why A07 Lives Through Every Top 10 Refresh

The category now called Identification and Authentication Failures has been on the OWASP Top 10 in some form since the list began. In 2017 it sat at A02 as Broken Authentication. In the 2021 revision it was renamed and broadened to A07 — the rename was deliberate, intended to capture failures that occur at the identification step (claiming to be a user) as well as the authentication step (proving you are that user). The 2025 list keeps the position and the framing. The persistence is not an accident; it reflects a structural property of how applications are built. Authentication is the first control on the request path, the one with the most surface area, and the one where a failure has the broadest blast radius. Every other category — broken access control, injection, misconfiguration — assumes the authenticated identity is correct. When it is not, every downstream control is enforcing decisions on the wrong subject.

The findings in 2026 pentest reports are rarely "the login form has no password check" — that class is mostly extinct — and almost always one of: a hashing algorithm too weak for current hardware, a session ID that survives logout, an MFA bypass through a recovery flow, a JWT validation library configured to accept any algorithm, or an OAuth integration missing state or PKCE. The vulnerabilities are subtle, protocol-specific, and often invisible in superficial review. The category persists because the protocols keep evolving (TOTP → push → FIDO2 → passkeys), the threat model keeps shifting (single-credential attacks → credential stuffing → adversary-in-the-middle phishing), and the implementation surface keeps expanding (mobile, IoT, machine-to-machine). Every change creates new failure modes.

The Two Halves: Identification vs Authentication

Most discussion of broken authentication blurs two steps that are formally distinct. The 2021 rename of A07 was meant to surface the distinction, and the distinction is worth getting right because the failures cluster differently on each side.

Identification is the claim — "I am alice@example.com." The most common identification-side failures are username enumeration (the application reveals which addresses exist through error messages or response timing), case-sensitive lookup that creates parallel accounts for ALICE@example.com and alice@example.com, and flows that do not rate-limit unknown identifiers, which lets attackers harvest the user database one guess at a time.

Authentication is the proof — "here is the password / OTP / signed assertion / passkey response that proves the claim." The proof step is where the majority of A07 failures live: weak hashing of stored passwords, missing MFA, MFA bypass through recovery flows, sessions that do not rotate after auth state changes, and tokens accepted without signature verification. A perfectly executed identification step followed by a broken authentication step yields the same outcome: an attacker signed in as the wrong user. The two deserve separate threat modeling — explicit answers for "how do we prevent enumeration of valid identifiers" and "how do we prevent guessing or theft of the proof artifact" — rather than a merged "login security" concern that obscures which control addresses which failure.

Password Storage: bcrypt, scrypt, Argon2 and Nothing Else

The rule for 2026 is short: passwords are stored using a memory-hard, adaptive password hashing function — Argon2id, scrypt, or bcrypt — with a per-user salt, an appropriate cost factor, and ideally an additional pepper held outside the database. MD5, SHA-1, and SHA-256 are not password hashing functions; they are general-purpose digest functions designed to be fast, which is the opposite of what password storage requires. Plain-text storage is malpractice; encrypted-at-rest passwords are malpractice with extra steps because the decryption key is recoverable from the same servers as the ciphertext.

Slow, memory-hard hashes win on threat model. An attacker who exfiltrates the user table wants to recover plaintext offline, then stuff them against this application or against other services where the user reused them. Fast hashes let modern GPUs check billions of guesses per second; bcrypt and Argon2id are deliberately slow per-guess (tens to hundreds of milliseconds) and resist GPU and ASIC parallelization. Cost factors are not static — the bcrypt work factor reasonable in 2014 is too low for 2026 hardware. Mature programs review parameters annually and rehash on login to upgrade existing users.

// BAD: SHA-256 password "hashing" — fast, GPU-friendly, broken by design
import crypto from 'crypto';
function storePassword(plain) {
  return crypto.createHash('sha256').update(plain).digest('hex');
}

// GOOD: Argon2id with appropriate parameters for 2026
import argon2 from 'argon2';
async function storePassword(plain) {
  return argon2.hash(plain, {
    type: argon2.argon2id,
    memoryCost: 65536,   // 64 MiB
    timeCost: 3,
    parallelism: 4,
  });
}
async function verifyPassword(stored, plain) {
  return argon2.verify(stored, plain);
}

Salt vs pepper. A salt is per-user, stored alongside the hash, and prevents rainbow-table pre-computation. Modern hashing libraries handle salts automatically. A pepper is application-wide, stored separately (secret manager or HSM), and provides defense in depth against database-only compromise: an attacker with the user table but not the pepper cannot perform offline cracking. The pepper is a meaningful additional control for high-value systems, not a substitute for strong parameters.

Migrating from legacy hashes. Plaintext is not recoverable, so the standard pattern is double-hashing: store [Argon2id(legacy_hash(password))] for existing users, verify by chaining legacy then modern, and on next successful login prompt the user to re-authenticate and rehash directly. The legacy digest inside the modern hash inherits the modern hash's slowness, so legacy records gain meaningful resistance immediately while migration completes.

Credential Stuffing and the Breach Corpus Problem

The single most consequential change in the authentication threat landscape over the past decade is the public availability of multi-billion-record credential corpora. Have I Been Pwned alone tracks more than ten billion compromised credential pairs; underground datasets contain materially more. Credential stuffing — replaying these pairs against login forms across the internet — has become the dominant entry vector for account takeover, and "strong password policy" is no defense. A password meeting every complexity requirement is fully exposed if it appeared in any prior breach.

The defenses cluster into three categories. First, breach-corpus checks at registration and password change. HIBP exposes a k-anonymity API: the client sends the first five hex characters of the SHA-1 hash, receives matching suffixes, and checks locally. Rejecting any password that appears in the corpus eliminates the most common stuffing vector for that user. Second, behavior-aware login throttling. Naive rate limiting (N per IP per minute) is bypassed by the distributed botnets attackers use; effective controls combine per-account, per-IP, and per-ASN throttling with device fingerprinting and pattern analysis (Auth0 Bot Detection, Akamai Account Protector, Cloudflare Bot Management). Third, MFA. Stuffing succeeds because the credential is the only factor; even the weakest MFA raises per-account work substantially, and FIDO2 raises it to where stuffing is not economically rational. Risk-based step-up — prompting for MFA when login characteristics deviate from the user's historical pattern — catches the stuffing case (new IP, device, geography) precisely.

Password complexity rules are obsolete. NIST SP 800-63B has formally moved away from arbitrary complexity requirements (must contain uppercase, special character, etc.) and recommends a minimum length plus a breach-corpus check. The complexity rules made passwords harder for users to remember and easier for attackers to guess (they collapse the search space to predictable patterns). Replace them with length + breach check + MFA.

Multi-Factor Authentication: TOTP, FIDO2, Passkeys, Push, SMS

MFA is the single highest-leverage authentication control a program can deploy. The factors available in 2026 differ in their phishing resistance, recovery characteristics, and user-experience cost. The right choice depends on the threat model.

FactorPhishing-resistantNotes for 2026
SMS OTPNoDeprecated for high-value accounts; SIM-swap and SS7 attacks routine
TOTP (authenticator app)NoBetter than SMS but phishable via real-time relay (evilginx)
Push notificationPartialVulnerable to MFA fatigue / push bombing; require number matching
FIDO2 / WebAuthnYesOrigin-bound assertion; cannot be relayed; the gold standard
Passkeys (synced)YesFIDO2 with cloud-synced credentials; usability transformation

SMS is deprecated. NIST began deprecating SMS as an authentication factor in 2016. SS7 routing flaws let attackers intercept text messages without owning the device, and SIM-swap attacks are routine in financial-account-takeover campaigns. SMS is better than no MFA against opportunistic stuffing but should not be the only second factor for accounts with material privilege.

TOTP is phishable. RFC 6238 generates a six-digit code from a shared secret and the current time. It defeats credential stuffing but not real-time phishing — an attacker proxy (evilginx and similar) sitting between the user and the legitimate site captures both the password and the OTP and replays them within the validity window. Mass phishing campaigns in 2024-2026 routinely include tooling that handles TOTP transparently.

Push with number matching. The user receives a notification and approves it. Vulnerable to "MFA fatigue" attacks where the attacker repeatedly triggers prompts until the user approves one. The mitigation is number matching — the login screen displays a code the user must enter into the push prompt, which prevents blind approval and basic relay. Microsoft, Okta, and Duo all default to number matching now; older configurations should upgrade.

FIDO2 / WebAuthn / Passkeys. The phishing-resistant family. The protocol uses a per-origin keypair where the assertion is signed over the origin and the challenge, so a phishing site at a different origin cannot relay the assertion. Passkeys extend FIDO2 with cloud-synced credentials (iCloud Keychain, Google Password Manager, 1Password) so users do not face the recovery problem of losing a single hardware key. Programs serious about phishing resistance ship passkeys as a primary factor.

Recovery flows are the weakest link. An account with FIDO2 enrolled is only as secure as its recovery path. "Answer a security question and reset by SMS" inherits the security level of SMS regardless of the primary factor. Design recovery at the same threat-model level as primary authentication: recovery codes printed at enrollment, a second registered factor, or in high-security contexts a manual identity verification. A recovery flow an attacker can complete is a recovery flow an attacker will complete.

Session Management Failures

Sessions are how applications remember an authenticated user across requests, and session management failures are persistent in pentest findings because the failure modes are subtle and the safe-pattern ergonomics are sometimes worse than the unsafe ones.

Predictable session identifiers. The session ID must be unguessable — generated from a CSPRNG with at least 128 bits of entropy. Sequential, timestamp-based, or user-ID-derived identifiers are all vulnerable. Modern web frameworks (express-session, Django, Spring Session, Rails) generate IDs correctly by default; the failure mode appears in custom and microservice-handrolled session handling.

Missing rotation on auth state change. When auth state changes — login, logout, privilege elevation, password change — the session identifier must rotate. Failing to rotate enables session fixation: an attacker who plants a known ID in the victim's cookie before login (via XSS, MITM, or a malicious link) shares the post-login session with the victim. The mitigation is one line in most frameworks (req.session.regenerate()) and is frequently missing.

Missing logout invalidation. Logout must invalidate the session server-side, not merely clear the client cookie. Stateless JWT-based sessions need an explicit revocation list or short lifetimes to achieve equivalent semantics. Concurrent session abuse compounds this: a compromised account runs an attacker session alongside the legitimate user's until the application limits concurrent sessions or the user forces logout. Mature applications expose "see all my active sessions" and a "log out everywhere" button.

Cookie attribute misconfigurations. The session cookie must carry HttpOnly (mitigates XSS theft), Secure (HTTPS only), SameSite=Lax or Strict (mitigates CSRF), and explicit Path/Domain scoping. Cookies missing any of these expose the session to attacks the attribute would have prevented. The configuration is a one-liner but ships incorrectly often enough to be a recurring pentest finding. See the security misconfiguration guide for the full cookie and header default set.

# BAD: Flask session cookie with no security attributes
app.config['SESSION_COOKIE_HTTPONLY'] = False
app.config['SESSION_COOKIE_SECURE'] = False
# SameSite default is None on older Flask versions

# GOOD: Hardened cookie configuration for 2026
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_SAMESITE='Lax',
    SESSION_COOKIE_NAME='__Host-session',  # __Host- prefix forces Secure + Path=/ + no Domain
    PERMANENT_SESSION_LIFETIME=timedelta(hours=8),
)

JWT Pitfalls

JSON Web Tokens have become the default session and authorization token format for modern API stacks, and the flexibility that made them popular is also their leading source of vulnerabilities.

The alg:none vulnerability. The JWT spec permits an "alg" of "none" — meaning the token is unsigned. Some libraries historically accepted such tokens as valid. An attacker crafts a token with alg: none and arbitrary claims, and the server treats the claims as authenticated. Modern libraries default to safe behavior, but configuration mistakes still occur. The mitigation is an explicit algorithm allowlist on every verification call.

// BAD: accepts any algorithm the token specifies, including "none"
const decoded = jwt.verify(token, secret);

// GOOD: explicit algorithm allowlist; rejects alg:none and confusion attacks
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],     // explicit, single algorithm
  issuer: 'https://auth.example.com',
  audience: 'api.example.com',
  clockTolerance: 5,
});

Algorithm confusion (HS256 vs RS256). A JWT issuer signs with RS256 (asymmetric). A misconfigured verifier accepts HS256 (symmetric) and uses the issuer's public key as the shared secret. An attacker fetches the public key from JWKS, signs an arbitrary token with HS256 using that key as the secret, and the verifier accepts it. The fix is the same explicit algorithm allowlist.

Missing exp/iat/nbf validation. The standard time-bound claims must be validated on every verification. A token with no exp, or one whose validation does not check exp, is effectively eternal — a stolen token retains access until the signing key rotates, which may be never.

Accepting JWTs from query strings. A JWT in a URL parameter is logged by every proxy, CDN, server access log, and browser history along the path. JWTs belong in the Authorization header or an HttpOnly cookie; query parameters are a leak waiting to happen. Sensitive claims in payload compound the risk: the payload is base64url-encoded, not encrypted, and anything in it is readable by anyone who obtains the token — PII, internal user IDs that aid further attack, occasionally actual secrets the developer believed were "encoded." Sensitive data belongs in a server-side session store keyed by an opaque token.

Missing key rotation. Signing keys must rotate. A key in use for years has had time to leak through configuration mistakes, log captures, or insider exposure. The JWKS pattern (publishing current and previous public keys with a key ID in each token's header) supports rotation without downtime. For how token-validation mistakes feed authorization failures, see the broken access control deep-dive — A07 and A01 are paired categories.

OAuth 2.0 / OIDC Common Bugs

OAuth 2.0 and OpenID Connect are the dominant protocols for delegated authentication and federated identity. The surface area of "an OAuth integration" produces a recurring set of integration bugs that account for a substantial share of A07 findings.

Missing or misused state parameter. The state parameter binds the authorization request to the callback. Without it, an attacker can initiate a flow and trick a victim into completing it, linking the victim's account to the attacker's session (login CSRF) or capturing an authorization code the victim's browser inadvertently delivers. The state must be a per-flow nonce stored in the user's session and verified on callback. Echoing it back without verifying is equivalent to having no state at all.

Open redirect on redirect_uri. If the relying party accepts redirect_uri values from the request without strict allowlisting, an attacker injects an attacker-controlled origin, completes the flow, and captures the authorization code. The mitigation is an exact-match allowlist of registered redirect URIs at the authorization server. PKCE skipped on public clients compounds this: PKCE (RFC 7636) protects the code from interception in flows where the client cannot keep a secret. The client generates a per-flow code verifier, sends a transformed challenge, and presents the verifier on token exchange. PKCE is mandatory for public clients in OAuth 2.1; as of 2026, every client should use PKCE — public or confidential.

# BAD: OAuth callback that doesn't validate state, no PKCE
@app.route('/oauth/callback')
def callback():
    code = request.args.get('code')
    token = exchange_code_for_token(code)  # vulnerable
    login_user(token)

# GOOD: state validation + PKCE verifier on exchange
@app.route('/oauth/callback')
def callback():
    code = request.args.get('code')
    state = request.args.get('state')
    if not state or state != session.pop('oauth_state', None):
        abort(400, 'state mismatch')
    code_verifier = session.pop('pkce_verifier', None)
    if not code_verifier:
        abort(400, 'pkce verifier missing')
    token = exchange_code_for_token(
        code,
        code_verifier=code_verifier,
        redirect_uri=REGISTERED_REDIRECT_URI,  # exact match
    )
    login_user(token)

Refresh token theft. Refresh tokens are long-lived and mint new access tokens without user interaction. A stolen refresh token grants indefinite access. Mitigations include refresh-token rotation (each use issues a new token and invalidates the old, so replays are detectable), sender-constrained tokens (DPoP or mTLS), and shortened lifetimes for high-risk contexts.

Scope inflation and token confusion. Integrations that request more scopes than needed create oversized credentials; if stolen, the attacker inherits scopes the application did not actually require. The discipline is least-privilege with incremental authorization. Separately, the ID token (OIDC, authenticates the user to the relying party) and access token (OAuth 2.0, authorizes the relying party against the resource server) are not interchangeable — treating one as the other is a common bug that produces authorization failures.

Passwordless Authentication and Passkeys

The most consequential authentication change in the past three years is the broad rollout of passkeys — FIDO2/WebAuthn credentials synced across a user's devices via cloud keychain. Apple, Google, and Microsoft all support passkey synchronization, and major sites (GitHub, Google, Microsoft, Amazon, banks) have shipped passkey login.

WebAuthn implementation realities. The protocol is well-specified and major browser implementations are mature. Integration is typically: add a server-side library (SimpleWebAuthn, py_webauthn, WebAuthn4J), wire up registration ceremony (server challenge → client-generated keypair → server stores public key), wire up authentication ceremony (server challenge → client signs → server verifies), and handle account recovery. The complexity is in recovery, not the cryptographic ceremony.

Attestation: when to require it. WebAuthn supports attestation — cryptographic proof of the authenticator type. Most consumer applications should not require it; doing so excludes legitimate users with non-allowlisted authenticators and has privacy implications. Attestation matters in regulated environments (banking with hardware-key requirements) and is the wrong default for general consumer applications.

Account recovery without weakening. The classic mistake rolling out passkeys is "if you lose your passkey, we'll email you a recovery link," which reduces security to the email account's security — turning a phishing-resistant primary factor into a phishing-vulnerable recovery factor. The right pattern is recovery codes generated at enrollment, a secondary registered authenticator (a second passkey on a different device), or in high-security contexts a manual identity verification.

Synced vs device-bound passkeys. Synced passkeys (iCloud Keychain, Google Password Manager) are user-friendly because losing a device does not lose the credential. Device-bound (hardware keys like YubiKey) cannot be exfiltrated but require backup-key management. Synced is usually the right default for consumer applications; device-bound for enterprise with hardware-key policies. The choice should be deliberate, not whatever the user happens to enroll first. Fallbacks should not be unauthenticated — "if WebAuthn fails, log in with email link" defeats the security improvement.

The Mitigation Playbook + Detection Strategy

The patterns above converge on a concrete engineering playbook. Identity is one category where the right answer is to use proven libraries and providers, not to handroll, and where the playbook is mostly about which battle-tested implementation to choose.

Adaptive password hashing with parameters tuned to current hardware and revisited annually. Breach-corpus check at registration and password change via HIBP k-anonymity. MFA with phishing-resistant factors as primary — passkeys and FIDO2 for the strongest factor, TOTP as portable fallback, push with number matching as the usability win, SMS as last-resort opt-in. Recovery flows designed at the same security level as primary auth.

Session rotation on auth state change, server-side invalidation on logout, hardened cookie defaults (HttpOnly, Secure, SameSite=Lax, __Host- prefix) baked into base configuration. JWT validation through a vetted library with explicit algorithm allowlist, verified issuer and audience claims, enforced expiration, JWKS-published rotated keys, tokens never accepted from query strings, no sensitive data in payload. OAuth/OIDC integration through a vetted client library with state on every flow, PKCE for every client, exact-match redirect URI allowlisting, minimized scopes, and no ID-token/access-token confusion.

Detection in production. Authentication-system logs are themselves a security control. Monitor failed logins above per-account and per-IP thresholds, successful logins from new geographies or device fingerprints, token validation failures (often indicating token manipulation), session anomalies (concurrent sessions from impossible-distance IPs, fixation patterns), MFA enrollment changes, and recovery-flow completions. The signal is meaningful and the cost is low compared to the incidents it catches.

Library, not handroll. The single highest-impact discipline in the category is "do not handroll authentication." Use Auth0, Okta, Cognito, Firebase Auth, Clerk, WorkOS, or a comparable vetted platform. The complexity of getting authentication right at production scale exceeds the resources of nearly every individual application team. For the testing methodology that catches authentication bugs in CI, see the SAST/DAST/IAST comparison — DAST is particularly effective because the failure modes are observable from outside the application.

· OWASP A07 · DEVELOPER ENABLEMENT ·

Identity Is the Gate. Train Developers to Recognize What Strong Auth Looks Like.

A01 broken access control is what an attacker exploits after they get past authentication. A07 is what lets them get past in the first place. The patterns that distinguish strong identity engineering from weak — adaptive hashing, breach-corpus checks, FIDO2 over SMS, session rotation, JWT algorithm allowlists, OAuth state plus PKCE — are all things a developer should recognize at code-review and design-review time. SecureCodingHub builds the authentication-aware fluency that turns A07 from a recurring pentest finding into something developers catch themselves at authoring time. If your last audit produced another "weak password storage" or "missing MFA enforcement" finding, we'd be glad to show you how our program changes the input side of that pipeline.

See the Platform

Closing: Identity Is the Gate Every Other Control Depends On

Authentication and identification failures persist on the OWASP Top 10 not because the protocols are unsolved — they are extensively solved, with mature libraries and well-documented patterns — but because the implementation surface keeps expanding and the protocols keep evolving. Every new client (mobile, IoT, machine-to-machine), every new identity provider integration, every new authentication factor creates new opportunities for misconfiguration.

The organizations that manage A07 well share a small set of practices: established identity platforms rather than handrolling, current hashing parameters, breach-corpus checks, phishing-resistant MFA on privileged accounts, rotated session IDs with hardened cookies and server-side logout invalidation, JWT validation through vetted libraries with explicit algorithm allowlists, OAuth integrations with state and PKCE and scoped redirect URIs, recovery flows designed at the same security level as primary auth, and authentication telemetry monitored as a first-class production signal.

None of these practices is novel in 2026; all of them require sustained engineering investment. The teams that take identity seriously treat it as the foundational engineering discipline it actually is — and reap the compound benefit of an authenticated session that means what it says, on which every downstream control can rely. For the broader Top 10 context and how A07 connects to the rest of the list, see the OWASP Top 10 2025 changes overview.