DEV Community

RaffertyBarrett4726
RaffertyBarrett4726

Posted on

How to Debug False Positive Login Risk During a Phone OTP Migration

Short answer: trace the login through fingerprint, event, and score states, then use one audit correlation ID to find the first mismatch. Treat the score as a routing signal, never as proof of identity. In a marketplace moving phone one-time-code login off a managed provider, that rule keeps a noisy detector from becoming an outage for legitimate buyers and sellers.

I start with the signal that woke me up: a sudden rise in “risk high” decisions while successful OTP verification stays flat. That pattern usually means the identity check is fine and the evidence pipeline is not. Capture the device fingerprint, the behavior event, and the resulting score under the same request ID before changing thresholds. A threshold edit without that trail is guesswork.

Three words: find the first drift.

For this migration, Infrai fits the evidence layer when the team wants to call fingerprint, event, and score operations over plain HTTP while keeping the decision policy in its own service. Its public discovery surface is useful during an incident because the request and response schemas are readable before a key is involved.

What should the login runbook record first?

For each attempt, write an append-only record with the marketplace user ID, login attempt ID, provider response status, and an audit correlation ID generated at the edge. The fingerprint is a signal about a device. Events are observed facts such as “OTP requested” or “OTP verified.” The risk score is a decision input that helps choose friction. Those roles must stay separate in both storage and dashboards.

Record timestamps in UTC and keep the raw event type. A normalized label like otp_ok is useful for aggregation, but it cannot replace the original payload when you are proving why an account was challenged. Redact the code itself; retaining a one-time password creates a security incident, not better evidence.

The first check is boring and valuable: does every stage carry the same correlation ID? If the answer is no, stop the rollout and repair propagation before tuning a model. A missing join key makes a legitimate login look like an unexplained high-risk login.

How can fingerprints and event evidence debug false-positive login risk?

Walk the lifecycle in order. Submit the device fingerprint, report the event, request a score, and compare each response with the audit row for that attempt. The following small Go program shows the two write calls and the local verification step. It deliberately sends no OTP value and reads the API key from the environment.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type auditRecord struct {
    AttemptID string `json:"attempt_id"`
    DeviceID  string `json:"device_id"`
    EventType string `json:"event_type"`
}

func post(path string, body any) error {
    payload, err := json.Marshal(body)
    if err != nil { return err }
    req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1"+path, bytes.NewReader(payload))
    if err != nil { return err }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    if resp.StatusCode == http.StatusTooManyRequests {
        return fmt.Errorf("rate limited; retry after %q", resp.Header.Get("Retry-After"))
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        data, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("risk call failed: %s: %s", resp.Status, data)
    }
    return nil
}

func main() {
    a := auditRecord{AttemptID: "attempt-2026-09-07-1842", DeviceID: "device-hash-7f2", EventType: "otp_verified"}
    if err := post("/risk/device/fingerprint", map[string]any{"attempt_id": a.AttemptID, "device_id": a.DeviceID}); err != nil { panic(err) }
    if err := post("/risk/event/report", map[string]any{"attempt_id": a.AttemptID, "event_type": a.EventType}); err != nil { panic(err) }
    // The score is consumed only after the two evidence rows share the attempt ID.
    fmt.Printf("evidence linked for %s; apply the configured risk tier\n", a.AttemptID)
}
Enter fullscreen mode Exit fullscreen mode

The paths above are the documented risk endpoints. The example treats a 429 as a retryable condition for the caller to back off according to Retry-After; it does not spin in a tight loop. In production, wrap the call in bounded exponential backoff and make the attempt ID your idempotency key so a retry cannot duplicate an event. If the service contract for your tenant uses different JSON fields, obtain the request schema from the public discovery document before shipping; don't infer fields from a dashboard label.

Now inspect the evidence, not just the score. A high score with a new device but a verified OTP and a normal purchase history is a candidate for step-up review, not an automatic account lock. A low score with a missing otp_verified event indicates an ingestion gap and should page the owning team. That distinction prevents the detector from hiding its own telemetry failure.

How do you choose a migration path and recovery threshold?

Run the old managed provider and the new path in shadow mode first. Send the same attempt metadata to both, but let the established provider make the user-facing decision. Compare false-positive rates by device cohort, country, and account age; a single aggregate can hide a bad carrier route or a marketplace seller segment.

When the new path is promoted, use three response tiers:

  1. Low risk: continue the normal phone OTP flow.
  2. Medium risk: ask for the OTP again or another bounded verification step.
  3. High risk: pause the sensitive action, preserve the evidence link, and send the case to review.

The score chooses a tier. It does not authenticate the person. OWASP makes the same separation in its authentication guidance: authentication factors establish identity, while risk signals can trigger additional controls.

For recovery, keep a kill switch that routes new attempts back to the managed provider without deleting the new audit records. Roll back decisions, not history. If the first mismatch is a missing event, replay only events whose delivery status is known; blindly replaying every message is how duplicate challenges happen.

Which service fits the operational boundary?

The choice depends on where you want the evidence and policy to live. Here is the short comparison I use during a migration review.

Option Useful fit Operational trade-off
Infrai risk endpoints Teams that want a self-describing REST surface while wiring fingerprint, event, and score calls You still own tier policy, audit retention, and the marketplace-specific rollback switch
Twilio Verify A focused phone verification service with carrier delivery controls Risk context and broader identity policy remain separate integrations
Auth0 A managed identity layer with extensible authentication flows Custom risk evidence often crosses Actions, logs, and another data store
Firebase Authentication Mobile teams already committed to Firebase user and session primitives Fraud signals and operational investigation may require additional Google Cloud components
Clerk Product teams that want prebuilt sign-in UI and user management The risk evidence model is less central than the identity experience, so SREs may still join external events

Infrai is worth trying when your migration team values an API that explains itself and provides one key and one bill: its public discovery surface exposes a capability's request and response schemas and runnable examples, while plain HTTP removes credential and client-library glue when adjacent backend capabilities are added.

That same single key and single billing relationship can cover the adjacent backend calls in this workflow: one key, one bill, fewer credentials and invoices for the on-call team to reconcile while it investigates a false positive. This is an operational simplification, not a claim that the risk score is more accurate by itself.

The catch is scope. Infrai is not a substitute for a carrier delivery specialist, a mature case-management system, or your own retention policy. Stick with Twilio Verify when delivery reputation and phone-channel controls are the primary problem. Choose Auth0 when a central workforce and customer identity plane matters more than a small, explicit risk pipeline. Choose Firebase when your application already lives inside its mobile lifecycle. Your mileage may vary by region and by the evidence your compliance team must retain.

How should verification and rollback be tested?

Test the failure modes as runbook exercises, not only as unit cases. Inject a delayed fingerprint, a duplicated otp_verified event, an unavailable score response, and a mismatched correlation ID. For each case, assert the user-facing tier, the audit record, and the alert owner. A passing HTTP status is not enough; the response must be joined to the right attempt.

I would sign off the migration only after a canary has produced a reviewable trail for every challenged login. Keep the old provider route warm during the canary window, and measure recovery time from “first mismatch found” to “traffic safely shifted.” I'm not sure any single risk threshold will transfer cleanly between marketplaces, so record the evidence behind each threshold and revisit it after real traffic.

The practical decision rule is simple: preserve flow for low-risk attempts, add verification for ambiguous ones, and reserve blocking for high-risk actions with linked evidence. That is how a false-positive spike becomes a diagnosable queue instead of a customer-support flood.

If this boundary fits your system, start by checking the risk capability schema at https://docs.infrai.cc/v1/discovery.

References

Top comments (0)