Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Secure Credential Verification Flow

Hello! Welcome back.

In our last lesson, we designed the user store for our Identity Provider (IdP), establishing a solid foundation with a multi-tenant data model and, most critically, a strategy for securely storing password hashes using modern algorithms like Argon2.

Now that we have a secure way to store credentials, we must design the process to verify them. This brings us to today's learning outcome:

Architect the credential verification flow, including password hashing, MFA integration, and timing attack protection.

This lesson builds the core logic of authentication. We will architect the end-to-end flow a system follows when a user attempts to log in. Our design will cover three crucial stages:

  1. Password Verification: The process of taking a user-submitted password and safely comparing it against the hash stored in our database.
  2. Timing Attack Protection: A deep dive into a subtle but critical vulnerability in the comparison step, and how to architect a defense using constant-time operations.
  3. Multi-Factor Authentication (MFA) Integration: How to layer an additional factor of authentication, like a one-time password from an authenticator app, on top of the password verification step.

Let's begin by architecting the primary authentication factor: the password.

1. The Password Verification Flow

In the previous lesson, we designed the registration flow where a user's password was transformed into a secure hash string (e.g., $argon2id$v=19$m=65536...) and stored in the password_hash column of our users table. The verification flow is the reverse of this process, but with a critical comparison step.

To understand the standard industry practice, let's review the verification workflow.

Password Hashing Guide 2025: Argon2 vs Bcrypt vs Scrypt vs ...

The following guide, which we consulted in the previous lesson, provides a clear and concise description of the standard password verification workflow. It connects the registration process to the authentication step we are designing now.

Please read the subsection titled 'Password Hashing Workflow' (you can find it near the end of the 'Understanding Password Hashing Fundamentals' section). Focus on the five steps outlined under 'Authentication (Password Verification)'.

As the resource outlines, the verification process can be broken down into a clear sequence of steps. Let's frame this as an architectural flow for our IdP:

Step 1: Retrieve User and Stored Hash
The user submits their credentials (e.g., email and password) to the login endpoint. The first action our system takes is to query the database using the non-secret identifier:
SELECT password_hash FROM users WHERE email = :submitted_email AND tenant_id = :tenant_id;
This retrieves the full hash string for the user. As we established, this string contains not just the hash, but also the algorithm (argon2id), version, cost factors (m, t, p), and the unique salt.

Step 2: Compute Hash of Submitted Password
The system now takes the plaintext password submitted by the user and uses the parameters retrieved from the stored hash string (the salt and cost factors) to compute a new hash.
attempt_hash = Argon2id(submitted_password, salt_from_db, cost_factors_from_db)
It is essential that the exact same salt and parameters are used. This ensures that if the submitted password is correct, the resulting hash will be identical to the one stored in the database.

Step 3: Compare Hashes
The final step is to compare attempt_hash with the password_hash from the database.
IF attempt_hash == password_hash_from_db THEN Success ELSE Failure
This comparison seems simple, but how it's implemented is critical for security. A naive string comparison introduces a severe vulnerability known as a timing attack. Let's explore this in detail.

2. Preventing Timing Attacks with Constant-Time Comparison

A timing attack is a type of side-channel attack where an adversary analyzes the time it takes for a system to respond to different inputs. By measuring these minute differences in response time, they can reverse-engineer secret data.

To understand this threat and its solution from first principles, the following resource provides an excellent deep dive.

Soatok's Guide to Side-Channel Attacks

The 'Soatok's Guide to Side-Channel Attacks' blog post is a fantastic resource for understanding the nuances of timing attacks and the cryptographic principles behind preventing them. It's perfect for a deep-dive, first-principles approach.

Please read the following sections: 'Timing Leaks': This introduces the core concept of how time can leak information, specifically in the context of string comparison. 'What is Constant-Time?': This defines the solution—code whose execution time is not dependent on secret inputs. 'Constant-Time String Comparison' and 'Alternative: “Double HMAC” String Comparison': These sections detail two specific design patterns for implementing a secure comparison. Pay close attention to the bitwise logic in the first pattern, as it demonstrates how to avoid early exit.

Let's break down the key takeaways.

The Vulnerability: Naive Comparison

Most programming languages' default string comparison operators (==) or functions (memcmp in C) are optimized for speed. They compare inputs byte-by-byte and return false as soon as they find the first mismatch.

An attacker can exploit this:

  • If they submit a password that results in a hash that matches the stored hash for 1 byte, the comparison takes T time.
  • If their next guess matches for 2 bytes, the comparison takes T + delta time.
  • By precisely measuring the server's response time, they can guess the correct hash one byte at a time. Over many requests, this can reveal the entire hash, effectively bypassing the security of the hashing algorithm.

The Solution: Constant-Time Comparison

To prevent this, the comparison operation must be constant-time. This means it must take the same amount of time to execute, regardless of whether the inputs match or where the first difference occurs.

This is achieved by avoiding "early exits" or "short-circuiting". A constant-time comparison algorithm will always compare every single byte of both strings. It uses bitwise operations to accumulate any differences found without using a conditional branch (if) that would terminate the loop.

As outlined in the Soatok article, a simple implementation logic is:

  1. Initialize a result variable diff to 0.
  2. Iterate through every byte of both strings.
  3. For each pair of bytes, compute left[i] XOR right[i].
  4. Use a bitwise OR to combine this result with diff: diff |= (left[i] ^ right[i]).
  5. After the loop finishes, the strings are equal if and only if diff is still 0.

This loop always runs for the full length of the strings, and the operations inside are not dependent on the data, making the execution time independent of the inputs and thus secure against timing attacks.

Architectural Best Practice:

While understanding the bitwise logic is valuable, you should never implement this yourself. Cryptography is notoriously difficult to get right. Always use the constant-time comparison function provided by a trusted, well-vetted cryptographic library.

  • In PHP: hash_equals()
  • In Python: hmac.compare_digest()
  • Libraries like bcrypt or argon2 provide their own verification functions (e.g., bcrypt.checkpw()) that handle this internally. This is the preferred method.

So, to correct Step 3 of our flow:
IF constant_time_compare(attempt_hash, password_hash_from_db) THEN Success ELSE Failure

3. Integrating Multi-Factor Authentication (MFA)

Once the user's password (the first factor) is successfully verified, we can significantly increase security by requiring a second factor. The most common method for this is a Time-based One-Time Password (TOTP), typically generated by an authenticator app like Google Authenticator or Authy.

Let's architect the flow for integrating TOTP.

How TOTP Works

TOTP is an open standard (RFC 6238) derived from HOTP (HMAC-based One-Time Password, RFC 4226). This video provides an excellent first-principles explanation of how the algorithm works.

How does the Google Authenticator Work? HOTP TOTP Difference | 2FA Authentication

To understand how to integrate MFA, we first need to understand how the codes are generated and verified. This video from Gabriel Zimmermann does a fantastic job of breaking down the TOTP algorithm from the ground up.

Please watch the video, focusing on these key concepts: (00:17 - 01:41) The core idea of HOTP: combining a shared secret and a counter. (04:42 - 06:57) How TOTP evolves HOTP by using time as the counter, divided into discrete steps (e.g., 30 seconds). (02:33 - 04:42) The truncation process that converts a long SHA-1 hash into a 6-digit code. (07:08 - 08:10 & 15:54 - 16:40) How the server verifies the code and uses a window (e.g., current, previous, and next step) to account for clock drift. \nFor your interest in deep internals, the segment from 11:05 to 15:54 provides a bit-by-bit explanation of the dynamic truncation logic from the RFC, which is a great example of low-level data manipulation in a cryptographic context.

Architecting the MFA Flow

Integrating TOTP transforms our stateless login flow into a stateful, multi-step process.

MFA Setup Flow (one-time process per user):

  1. The user indicates they want to enable MFA.
  2. The server generates a cryptographically secure shared secret (e.g., 160 bits).
  3. This secret is stored in the database, associated with the user. It must be encrypted at rest.
  4. The secret is presented to the user as a QR code. The QR code contains a URI like otpauth://totp/YourApp:user@example.com?secret=SHARED_SECRET&issuer=YourApp.
  5. The user scans the QR code with their authenticator app, which stores the secret.
  6. To verify, the user enters a code from their app, and the server validates it. MFA is now enabled.

MFA Verification Flow (at each login):

  1. User authenticates with their password (factor 1), as described in the previous sections. The password verification must succeed.
  2. Upon success, the system does not immediately grant a session. Instead, it transitions to an "Awaiting MFA" state. It should generate a temporary, short-lived token (e.g., a signed JWT) that encodes the user's ID and the fact that factor 1 was completed. This token is sent back to the client.
  3. The client is redirected to an MFA entry page and submits the temporary token along with the 6-digit TOTP code from their authenticator app.
  4. The server validates the temporary token to confirm factor 1 was completed for that user. It then retrieves the user's shared secret from the database.
  5. The server computes the expected TOTP codes for the current time window, as well as the previous and next windows to account for clock skew.
    • expected_code_now = TOTP(shared_secret, time_now)
    • expected_code_past = TOTP(shared_secret, time_now - 30s)
    • expected_code_future = TOTP(shared_secret, time_now + 30s)
  6. The server performs a constant-time comparison of the user's submitted code against these three generated codes.
  7. If a match is found, the authentication is fully successful, the temporary state is cleared, and a full-fledged session is granted.

This sequence diagram illustrates a similar custom challenge flow in a real-world system (AWS Cognito with Duo MFA), showing the back-and-forth between the client, the authentication service, and the MFA provider.

Duo MFA Integration with AWS Cognito Custom Authentication Flow
This sequence diagram shows a custom MFA authentication flow. The user first signs in (step 1). The authentication service (Cognito) then defines and creates a custom challenge (steps 2-5), which is sent to the client. The client interacts with the MFA provider (Duo) and sends the response back (steps 6-10). Finally, the authentication service verifies the challenge response (step 11), completing the multi-factor authentication.

Conclusion

In this lesson, we have architected the complete credential verification flow for a modern IdP. We moved from simply storing credentials to actively using them for authentication, layering crucial security measures at each step.

Key Takeaways:

  • Password Verification is a Multi-Step Process: It involves retrieving the user's stored hash and salt, re-computing the hash with the submitted password, and performing a secure comparison.
  • Timing Attacks are a Real Threat: Standard string comparisons are insecure. You must use constant-time comparison functions, typically provided by your chosen cryptographic library, to prevent attackers from leaking the hash via response time analysis.
  • MFA Creates a Stateful Flow: Integrating MFA (like TOTP) requires managing state between authentication steps. This is often handled with a short-lived token issued after the first factor is verified.
  • TOTP Relies on a Shared Secret and Time: Both the server and the authenticator app compute the same code using a shared secret and the current time. The server must check a small window of time steps to handle clock drift.

We have now designed a robust system to confirm a user is who they say they are. But what happens after they are successfully authenticated? How do they maintain their logged-in state as they navigate between applications?

Preview of the Next Lesson:

In our next lesson, we will tackle this question by addressing the outcome: "Design a session management system, comparing stateful (session ID) vs. stateless (JWT) approaches for secure issuance, validation, and revocation." We will design the token or session that is granted upon successful login and explore the architectural trade-offs between server-side sessions and client-side JSON Web Tokens (JWTs).

Can't find a good explanation? Sign up and we'll make it for you

Sign up