Great questions, and your A/B test plan is exactly the right approach! Yes, it works with custom auth. I'd just add one thing based on our rollout that others haven't fully touched on yet.
You asked about mapping the credential check to your specific request parameters. While `lookup_json_string` is key, the real gotcha for us was handling API versioning. Our login endpoint is something like `/api/v3/auth/login`. Our rule was scoped perfectly to that path. But we completely forgot about our mobile app's legacy support for `/api/v2/auth/login` for a small group of users on older versions. The rule wasn't applied there, creating a blind spot until we added both paths to the rule logic.
So when you scope your rule, don't just think about your current primary endpoint - audit any deprecated or alternate paths that might still be active and accepting logins. It's an easy oversight that can undermine the test!
Clean data, happy life.
Yes, we got it working with our custom API! Thank you for asking about this, I was wondering the same thing.
To answer your second question on mapping: the key was using `lookup_json_string` for both the email AND password fields in the same rule condition. I made the mistake of just mapping the email at first, and it never triggered. Someone else here mentioned that, and it was a lifesaver.
Your A/B test plan is perfect. I'd just add that a log-only phase first was super helpful for us to confirm the field mapping was actually catching data without any risk of blocking. Good luck with your rollout
That's such a great point about the `http.request.method == "POST"` check to cut down executions. We started with that too, but then realized our OAuth token refresh flow also uses POST to the same `/login` endpoint, just with a grant type and a refresh token instead of a password. That would cause the rule to fire and fail the lookup because the password field was null.
We had to add another condition to exclude requests where `grant_type` was `refresh_token`. It's one more example of needing to understand your specific auth flows, not just the endpoint path and method.
If it's not measurable, it's not marketing.
That's a really solid strategy, and it's interesting to hear your results with a static employee base. Your point about the zero false positives for your internal platform makes a lot of sense.
>Starting with a small segment of traffic, like new account registrations
That's a clever segment to choose. It makes me wonder, how do you handle the case of existing employees who are performing a password reset? Our team found that those "set new password" flows can sometimes trip the same check if you're not careful with scoping the rule, since they often use the same endpoint with different parameters. Did you have to consider that in your rollout?
Let's keep it real.
That's a sharp observation, and it was a key part of our rollout. We separated the rules completely for that reason. The exposed credential check only triggers on the initial login endpoint with a specific parameter set. Our password reset flow uses a different endpoint, but more importantly, it uses a `current_password` and `new_password` field pair. We don't map `current_password` to the credential check at all, since it's not a login attempt against a breach database, it's a verification of the old secret.
The bigger issue for us was catching users reusing their *new* password against a known breach, which is a different security control we handle later in the reset process.
Completely agree on the separate endpoint strategy for credential checks versus password resets. Your point about not mapping `current_password` is the correct architectural decision.
We implemented a similar split, but we also ran into a subtle edge case with our mobile SDK. It would occasionally retry a failed login request by reusing the previous POST body, which sometimes still contained the old password field from a prior reset attempt. This caused the WAF rule to parse and trigger on what was technically a login endpoint but with stale, incorrect credential data.
Our fix was to add a timestamp check in the request headers from the client and invalidate any login attempt older than a few seconds, effectively scoping the rule to fresh attempts only. It's an extra layer of complexity, but it eliminated those anomalous log entries.
Mike
That's a clever solution with the timestamp header. We encountered a similar issue, but with cached browser requests. A user might hit the back button after a failed login, and the browser would resend the identical POST body, which could now contain a stale password if they'd since changed it via another tab.
Our mitigation was a nonce sent from the server during the login page load, which the client had to include in the login request. If the nonce didn't match a recently issued one, we treated it as a potentially replayed request and skipped the exposed credential check. It adds state to an otherwise stateless check, but it solved the stale data problem without relying on client clocks.
Ah, the nonce approach is smart. We tried something similar but ran into issues with our SPA because the login page was a single load. The nonce would get stale after a few minutes if the user just left the tab open.
We ended up combining the timestamp header idea with a short lived JWT that gets refreshed on any page interaction. It's a bit more moving parts, but it handles the "left it open overnight" scenario that was biting us. Your nonce method is definitely cleaner for traditional server rendered flows though.
it worked on my machine
It definitely works with custom auth, and your A/B test idea is the perfect way to start. For your question on mapping parameters, using `lookup_json_string` to match your specific field names is key, but based on our setup, I'd also suggest building a mock attacker script first.
Before you even touch production traffic, simulate login attempts with known breached credentials against your staging endpoint. This confirms your rule logic is actually firing and blocking the right stuff. We found a mismatch between our field naming (`user_email` vs `email`) during this test that saved us from a log-only phase full of misses.
Also, watch out for encoded payloads. Our frontend sometimes sends passwords as URL-encoded values within the JSON, and we had to adjust the rule to check for that pattern. The false positive rate has been near zero for us, but only after we nailed the field mapping and encoding. Good luck
security by default
That mock attacker script is a fantastic idea. We did something similar by pointing our integration test suite at a staging endpoint, using a small list of credentials we *knew* were in the breach database. It validated the rule logic in a completely controlled way before any real user saw it.
Your warning about encoded payloads is so real. We also had to handle base64 encoding from one of our legacy mobile clients. The rule's parsing worked fine, but we had to ensure the decoded value was what got sent for the lookup. It's those little client-specific quirks that make a staged test so valuable
We handled it by letting the block happen, but with a very specific error flow. The user sees a standard "invalid credentials" message to avoid leaking information, but internally the event triggers a distinct audit log entry and a low-priority alert to our security operations dashboard. This allows us to track the occurrence without a noisy, immediate page, while still informing the user through our standard password reset process.
Your scenario with old, forgotten passwords is precisely why we didn't route these to a high-severity alert. It's often a genuine user error, not an attack. The operational burden of investigating each one as a potential incident would be unsustainable at scale.
βBJ
It works with custom auth, but the "special configuration" is the entire point. The managed rule is just a trigger. You have to map it perfectly to your login fields, which often means wrangling JSON or form-data payloads that your front-end team didn't build with WAF parsing in mind.
Your A/B test is the right move, but don't just gauge "impact." Watch your logs like a hawk. The false positives often aren't from the breach data being wrong, they're from your own system's quirks, like session replay or mobile app retry logic that sends stale data. The posts about timestamps and nonces aren't academic, they're fixes for real problems you'll likely hit.
Legitimate blocks happen. We see them occasionally from users with very old, recycled passwords. The rate was manageable once we tuned the rule to ignore our password reset flow. Just make sure your error handling doesn't tell the user *why* they were blocked.
Your CRM is lying to you.
Oh, I love that you split the password reset into a separate flow. We took the same approach, and it's the only way to stay sane with these rules. The point about checking the *new* password is so crucial, and a lot of teams miss it during the initial design.
We handle that secondary check with a lightweight call to our own internal API after the reset succeeds but before the final confirmation is sent to the user. It uses the same breach database, but it's completely decoupled from the WAF rule. This way, if the check is slow or fails, it doesn't block the user's immediate reset flow - we just log a warning and send a follow-up security advisory email later.
Have you found any latency issues when calling the breach service synchronously during the reset? We debated moving that to a background job.
null
Yes, it works with custom auth but you have to configure the rule yourself. Don't rely on their managed rulesets.
For mapping parameters, you'll use the WAF expression builder with `lookup_json_string` targeting your specific field names. Like this:
```
http.request.method == "POST" and http.request.uri.path contains "/api/v1/login" and lookup_json_string(http.request.body.raw, "$.email") != "" and cf.waf.exposed_credentials_matched
```
Run it in log-only mode first and validate it's triggering on actual test attempts. I saw 0 false positives after tuning, but legitimate blocks do happen - usually users with old, breached passwords. We let it block and log it separately.
Ship it, but test it first
Great point about the content-type snag, we hit the exact same thing! Our marketing site login uses a standard form POST, but our main web app sends JSON, so we had to create a rule with two separate conditions branching on that header. The log-only phase was essential for catching that mismatch before we went live.
Your comment on public signup flows is key. We're B2B, so our user base is fairly static, and we still started with a 5% A/B test split just to be safe. For a truly public-facing form, I'd probably start at 1% and watch the logs for a full week before ramping up. Did you find a particular threshold that felt safe for your admin portal?
Clean data, happy life.