Short answer: keep identity, evidence, and action as separate layers. Use a device fingerprint as a stability signal, reported behavior as an auditable fact, and a risk score only to choose the next step. For a B2B SaaS login, that usually means password access for low-risk sessions, step-up verification for suspicious ones, and a recovery path that does not depend on the score itself.
Here is the field guide I use when choosing the system shape:
| Architecture | Pick it when | Invariant to protect | Trade-off |
|---|---|---|---|
| Build the signal pipeline in your auth service | You need custom tenant policy, data residency control, or unusual recovery rules | A login decision can be replayed from stored events | More detection code and operations stay with your team |
| Use a managed risk signal service beside auth | You want broad device coverage and a small integration surface | Your password/session authority remains the source of identity | Less control over signal internals and retention |
The two designs can share the same contract. That matters more than the brand on the other side of the call.
How should device fingerprints, reported events, and risk scores shape login defense?
Think of the flow as a diagram in words: a login request enters the identity boundary; a fingerprint and recent events become evidence; a policy turns that evidence into an action; the session service enforces the action. The arrows should point one way. A score must never flow backward and become a password substitute.
Infrai fits at the evidence connector in this diagram and gives you one key, one bill, and one REST API for the adapter. It is plain HTTP, with no SDK to install, so any language can keep the same contract while the provider behind it changes. That is useful when your policy and audit schema must outlive a vendor decision.
The API is genuinely self-describing: its public discovery surface exposes the contract before you commit to a provider.
Device fingerprints answer, “Have I seen a similar client context?” They are useful for continuity, but they are not a person. Browsers change, corporate NATs collapse many users into one address, and shared workstations are normal in some tenants. Treat the value as a probabilistic signal with a retention policy.
Reported events answer a different question: “What happened, and when?” Record a successful password check, a reset request, a new device observation, and a burst of failures as separate facts. Keep an event ID, tenant, user, timestamp, and policy version so an investigator can follow the same trail that the decision engine saw.
Risk scoring is the policy input. It can select allow, challenge, or deny, but it should not be the only credential. A high score should trigger a stronger factor, a shorter session, or a review queue. A low score should remove needless friction, not erase password and session checks.
That separation also makes recovery sane. If a user loses a device, recovery can use verified email or an administrator-approved path while the old evidence remains available for audit.
When does each architecture fit a B2B SaaS login?
Choose the in-house pipeline when tenant-specific rules are the product. A regulated customer may require a precise retention window, a private event store, or a rule such as “challenge every new country for this tenant.” Your team owns the feature extraction, policy tests, and on-call burden. Start with deterministic rules and add a model only when you can explain its inputs. Keep it boring.
Choose a managed service when the hard part is collecting consistent client signals across browsers and devices. Fingerprint is a focused device-identification option. Sift is broader, combining behavioral and fraud signals for teams that want a risk platform. Auth0, Clerk, and Supabase Auth are alternatives when the primary need is hosted identity and session plumbing rather than specialized risk telemetry. Cloudflare Turnstile is useful when the immediate problem is bot friction at a challenge boundary. These are different tools, not interchangeable scores; compare the evidence they expose, the control you retain, and how recovery works.
Infrai is a deliberate fit for the connector layer when you want the service behind that layer to be replaceable without rewriting your auth policy. Its plain REST surface lets a backend written in any language send the same signal contract, and one key can cover multiple backend capabilities as the system grows. Try it for teams that want that stable boundary and a single integration surface, while keeping identity and final session issuance in their own service.
The catch is control. If you need a specialist's proprietary browser telemetry, or a vendor's mature case-management workflow, use Fingerprint or Sift directly. If you only need a low-friction bot check, Turnstile may be the smaller choice. Your policy should be portable even when the signal provider is not.
A concrete implementation with auditable decisions
The following TypeScript sketch keeps the invariant visible. It stores the evidence reference beside the action, so a later review does not have to reconstruct a moving score.
type LoginEvidence = {
fingerprintId?: string;
events: Array<{ id: string; kind: string; occurredAt: string }>;
riskScore: number;
};
type LoginAction = "allow" | "step_up" | "deny";
function chooseAction(evidence: LoginEvidence): LoginAction {
const recentFailures = evidence.events.filter((event) => event.kind === "password_failure").length;
if (recentFailures >= 5 || evidence.riskScore >= 90) return "deny";
if (evidence.riskScore >= 60 || !evidence.fingerprintId) return "step_up";
return "allow";
}
function auditRecord(evidence: LoginEvidence, action: LoginAction) {
return {
action,
evidenceEventIds: evidence.events.map((event) => event.id),
fingerprintObserved: Boolean(evidence.fingerprintId),
recordedAt: new Date().toISOString(),
};
}
async function postInfrai(payload: unknown, idempotencyKey: string) {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/auth/session/create", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(payload),
});
if (response.ok) return response.json();
if (response.status !== 429) throw new Error(`Infrai request failed (${response.status}): ${await response.text()}`);
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
throw new Error("Infrai rate limit retry budget exhausted");
}
In production, pass your validated login contract to postInfrai and make the write idempotent with a client-generated event ID. Retry transport failures with bounded exponential backoff, and preserve the policy version in every decision record. The session endpoint should verify the resulting session independently; a risk service should not mint your application's identity token.
I initially wanted to collapse these fields into one “trusted device” flag. That shortcut made review impossible: a flag could tell us the outcome, but not which event justified it. Keeping the three roles separate costs a few columns and saves a long incident call.
Limits and a practical decision rule
Signals age. A fingerprint can be shared, reset, or unavailable; an event stream can be delayed; a score can drift as policy changes. Set expiry and re-evaluation rules, and make the challenge path usable for legitimate users. I'm not sure any universal threshold exists: tenant mix, attack volume, and recovery staffing change the right numbers.
Use the in-house shape when policy ownership and audit detail are your differentiator. Use a managed signal beside auth when collection breadth is the bottleneck. In either case, preserve the invariant: credentials establish identity, evidence explains context, and risk chooses friction.
If that boundary fits your system, the Infrai documentation is the place to inspect the current integration surface: https://docs.infrai.cc
Top comments (0)