Skip to content

Cybersecurity Engineering Handbook / Chapter 23

Authentication Implementation

Implement login, password handling, MFA, sessions, OAuth/OIDC, service authentication, and authentication tests without inventing a protocol.

At 02:13, the account owner asks to reset a password. At 02:17, the reset finishes. At 02:18, an existing administrator session refreshes successfully. Which event should make the last one impossible?

The answer cannot live only in the reset screen. It crosses the identity provider, recovery-token store, password verifier, MFA enrollment, session store, token issuer, application validator, and security log. If any one of those components treats recovery as an isolated feature, an attacker may use the old session, race the reset token, replace a factor, or enter through a service route that validates less than the browser route.

Authentication code should therefore implement one lifecycle: every path that creates or extends an actor’s authority must be issued for a named purpose, validated at the point of use, bounded in time, revocable when the risk demands it, and exercised through its rejected paths. Use a maintained identity provider or framework where practical. Delegation reduces protocol code; it does not delegate the application’s obligation to validate what it receives.

Authentication token lifecycle showing login, identity provider, token issuance, validation, refresh, revocation, expiry, logging, and guards for PKCE, nonce, audience, issuer, cookie flags, and MFA.
Authentication is a lifecycle. Issuance, validation, refresh, revocation, expiry, and logging each require a concrete control, or attackers use the weakest step as the login path.

Name every way an actor can appear

Begin with an inventory, not the login form. Password login, passkeys, SSO callbacks, magic links, password reset, factor recovery, support-assisted recovery, refresh tokens, remembered devices, API tokens, workload exchange, and break-glass access can all create or prolong authority. For each path, name:

  • who issues the credential and which actor it represents;
  • the exact purpose, audience, and client allowed to use it;
  • the proof required before issuance;
  • its lifetime, reuse rule, and revocation event;
  • the application boundary that validates it;
  • the event recorded for success and rejection.

This inventory exposes unequal doors. If an administrator normally needs a phishing-resistant authenticator but support can remove that factor after a weak knowledge check, the support flow defines the account’s real assurance.

At the interactive login boundary, apply rate limits and abuse detection by more than one dimension: account, source, device or session, and system-wide pressure where appropriate. Return the same public result for an unknown account and a wrong secret. Keep the internal reason for investigation without putting the submitted password, token, or provider response into a log. Lockout is a policy choice with a denial-of-service cost; throttling, increasing delay, risk challenge, or temporary restriction may be safer than giving an attacker a reliable way to freeze a known account.

Keep password verification narrow

When the application owns password verification, store a salted, adaptive hash produced by a current password-hashing function and an intentionally chosen work factor. Put the implementation behind one maintained component so that a cost change or algorithm migration does not require every caller to understand hash formats. A successful login can rehash an older record under the current policy. Bound the verifier’s concurrency and resource use as well as the attacker’s request rate; an expensive password hash is also a capacity target.

Accept long passwords, compare new passwords against an appropriate blocklist of common or compromised values, and do not impose composition rituals that merely move predictability from one pattern to another. Never place plaintext passwords in logs, metrics, analytics, traces, support tools, or event payloads. Seed non-production rejection and exception paths with canary secrets and prove that the telemetry pipeline does not retain them.

The password verifier should reveal one result to the caller and retain a more precise result internally. It must also avoid an obvious fast path for unknown users. Exact timing equality is rarely realistic across a distributed system, but the implementation must not perform the expensive verification only when an account exists and then claim that identical error text prevents enumeration.

Treat reset and recovery as authentication

A reset request must not prove that an account exists. Return a consistent public response, apply request throttles, and deliver the recovery proof through an independently established channel. A reset token should be generated from a cryptographically secure random source, scoped to one account and one action, short-lived, stored so that disclosure of the reset store does not immediately yield usable tokens, and consumed at most once.

Consumption is an atomic state transition. Two requests presenting the same valid token must not both succeed:

consume_reset(token_digest, now, new_password_hash):
  begin transaction
    reset = find_unconsumed_reset(token_digest)
    require reset.expires_at > now
    update password where account_id = reset.account_id
    mark reset consumed
    increment account.session_version
    revoke or mark affected refresh-token families
    record password_reset_completed
  commit

The transaction boundary will differ by system, but the invariant does not: there is at most one redemption, and the authority retained after redemption is deliberate. Do not automatically turn a reset token into an authenticated session. Notify the account through an established channel, require a normal login, and decide whether all sessions or a risk-defined subset must end. A privileged or suspected-compromise flow should not leave the 02:18 administrator refresh alive.

Recovery from a lost factor deserves the same treatment. Recovery codes are high-value authenticators: generate them securely, reveal them deliberately, store verifiers rather than recoverable values where the design permits, make each one single-use, and revoke the remaining set when it is replaced. A human support path needs bounded operator authority, strong evidence, separation or approval for exceptional accounts, notification, delay where appropriate, and a durable audit trail. Security questions and easily discovered personal facts must not become a privileged account’s back door.

Before release, exercise unknown accounts, repeated reset requests, expired and already-consumed tokens, two concurrent redemptions, token use against another account or action, host-header and forwarded-host manipulation in generated links, session behavior after completion, factor-recovery abuse, and secret leakage through every failure path.

Enroll strong factors without creating a weak bypass

Use phishing-resistant authentication for privileged actors and other consequential access where practical. A method that asks a person to type a one-time code into an arbitrary page can still be relayed by a phishing site; factor count alone does not establish phishing resistance.

Adding, replacing, or removing a factor changes future authority. Require a recent authenticated session and step-up proof appropriate to the account; do not treat possession of the session being strengthened as sufficient by itself. Bind enrollment to the intended account and ceremony, expire unfinished enrollments, notify the owner through an established channel, and record the old and new factor classes without logging their secrets.

Step-up policy belongs beside the protected operation. Sensitive exports, payment changes, credential or factor changes, impersonation, break-glass use, and high-risk session signals are common triggers. The application must enforce the required authentication context at every API, job, and alternate client that can perform the operation. A browser prompt that a direct API request can bypass is not a control.

Carry authentication into a bounded session

Authentication creates a session; it does not finish the security decision. Generate a fresh session identifier after login and after privilege changes so that a value fixed before authentication cannot become authoritative. Browser cookies should normally be Secure and HttpOnly, use the narrowest practical domain and path, and select a SameSite policy that matches the real cross-site flow. Keep session meaning and sensitive data on the server when immediate revocation or policy change must take effect.

Define idle and absolute lifetimes from the consequence of theft. Privileged sessions generally warrant tighter bounds and recent step-up for consequential actions. Logout, password or factor recovery, account disablement, suspected compromise, role change, and administrative revocation need explicit semantics: which browser sessions end, which refresh families end, and how quickly each application observes the change.

If the session is represented by a signed token, signature verification is only one step. The validator must constrain the accepted algorithm and trusted keys, then check issuer, intended audience, time bounds, and token type or purpose. Validate the claims required by the local application; do not accept a token because a generic library returned valid. An ID token, access token, reset token, and email-verification token are not interchangeable containers even if they share a signing key and claim syntax.

The rejection suite should alter one property at a time: no signature, untrusted algorithm, unknown key, wrong issuer, wrong audience, expired or not-yet-valid time, wrong token type, stale session version, revoked refresh family, and replayed one-time proof. Run these cases through the actual middleware and endpoint, not only through a token helper.

Preserve the browser ceremony in OAuth and OIDC

Prefer a maintained OAuth/OIDC client over a hand-built exchange. Use the authorization code flow with PKCE for public clients; current OAuth security practice extends PKCE to other client types as well. A mobile, desktop, browser, or other public client cannot keep a distributed client secret, so embedding one in the application does not make it confidential.

The client must bind the callback to the ceremony it started. Use PKCE with an appropriate challenge method, prevent cross-site request forgery with the protocol mechanism appropriate to the deployment, and validate an OIDC nonce when used. Require exact registered redirect matching except where the relevant standard defines a narrow native-loopback exception. Do not put an open redirector beside the callback. A client that supports multiple authorization servers must also bind the response to the issuer it selected so that a code from one provider is not sent to another provider’s token endpoint.

At the token boundary, validate the expected issuer, audience or authorized party as applicable, signature and allowed algorithm, time claims, nonce where required, and the intended use of the token. Minimize scopes and downstream audiences. Refresh tokens are long-lived authority: protect them in transit and storage, bind them to the client and granted scope, expire them under an explicit policy, and use sender constraint or rotation with reuse detection where the threat model and client class require replay detection.

Negative tests should start a real ceremony and then swap state, verifier, nonce, issuer, redirect, client, code, scope, and token type one at a time. Also replay an authorization code and an old rotated refresh token. The expected result is rejection without a new session, a safe response to the browser, and an event precise enough to distinguish configuration drift from attack.

Give workloads their own identity

A service should authenticate as a workload, not by borrowing a person’s session or sharing one static secret across a fleet. Prefer platform-issued workload identity, mutually authenticated TLS, or short-lived, audience-bound tokens where the environment supports them. Network location may inform policy; it is not proof of identity by itself.

Trace the issuance chain. Which platform or authority attests to the workload? Which deployment, namespace, service account, binary, or environment can obtain the credential? Which exact service accepts it? How does a new instance obtain one without copying a long-lived secret into an image? How do rotation, revocation, and emergency distrust reach live processes?

CI jobs, batch workers, administrative scripts, data pipelines, and AI agents are workloads too. Give each the minimum audience and purpose it needs. Test an expired credential, a valid credential for another service or environment, a revoked workload, an unapproved source workload, and a token presented to the wrong protocol boundary. When mTLS is used, test the application identity and trust policy derived from the certificate, not merely the presence of a TLS connection.

Prove the lifecycle, not the helper library

Unit tests are useful for parsers, claim rules, and state transitions. They do not prove that every route calls the validator, that logout reaches every session store, or that recovery revokes the credential an application actually accepts. Keep a small end-to-end suite around the boundaries where authority is created or extended.

For the 02:13 recovery, capture a legitimate administrator session and refresh token before the reset. Complete the reset, then attempt both again. Race two redemptions. Try to enroll a factor through an alternate API. Send a correctly signed token with the wrong audience. Replay the old refresh token. Present a workload token to the user endpoint. Finally, inspect the evidence: it should connect request, reset issuance, redemption, session-version change, refresh revocation, rejection, and notification without containing a secret.

Before release, the implementation record should let a reviewer point to:

  • the complete inventory of login, recovery, factor, session, token, callback, and workload paths;
  • the provider or framework choice and the protocol behavior still owned by the application;
  • password hashing and migration policy, blocklist decision, verifier resource bounds, throttling, and enumeration tests;
  • reset and factor-recovery issuance, one-time atomic consumption, notification, session and refresh consequences, and support controls;
  • privileged factor policy, enrollment proof, recovery-code handling, step-up triggers, and alternate-route tests;
  • cookie scope, session rotation and lifetimes, revocation events, token claim policy, trusted keys and algorithms, and rejection cases;
  • PKCE, callback binding, issuer binding, exact redirects, scope and audience limits, and refresh replay behavior;
  • workload issuance authority, audience, lifetime, rotation, revocation, and cross-environment negative tests;
  • security events for issuance, success, refresh, step-up, recovery, revocation, and suspicious rejection, with secret-redaction evidence.

The chapter began with a password reset because recovery reveals whether the system truly has one authentication lifecycle. At 02:18, the old administrator refresh must either fail or remain valid for a reason the design names and the tests prove. If nobody can answer which component enforces that result, the system has implemented several login features, not authentication.

Once the actor and authentication context are trustworthy, Chapter 24 follows them to the object, field, and operation where authorization must still hold.