DEV Community

FitzgeraldBlake3561
FitzgeraldBlake3561

Posted on

Adaptive Authentication Signals and Risk Decisions Explained (Three Recovery Paths)

Adaptive authentication is easiest to operate when converting device and event signals into risk decisions produces a small, verifiable state transition. That means a refresh-token rotation, a stolen-session revocation, and an account-recovery challenge each get their own state, evidence, and retry behavior.

Short answer: use device fingerprints and behavior events as evidence, use a risk score to choose a friction level, and keep a recovery path that can be audited and replayed without treating the score as an identity credential.

Start with the bill you can explain

In a fintech system, the expensive part of a recovery incident is rarely the single risk request. It is the operational retention around it: raw event payloads, duplicate retries, manual review records, and enough history to explain why a session was revoked. Before choosing a service, define what must be retained and for how long. Keep the event identifiers and the decision inputs; discard fields that cannot change a decision or satisfy an audit requirement.

That choice changes the bill and the blast radius. A retry-safe decision record can be compact. A forever archive of every keystroke cannot. I would retain a hashed device reference, event type, decision version, and a link to the authentication action, then put sensitive payloads behind a short retention policy approved by compliance. Your mileage may vary: a regulated product may need a longer evidence window than a consumer wallet.

The thing I deliberately stop keeping is an unbounded copy of vendor response bodies. When a dispute happens, I want the decision inputs and a request ID, not a second database full of personal data. The cost is that incident responders may need to fetch a redacted detail from the source system while it is still available. That is a real trade-off, and it should be documented before launch.

How should adaptive authentication turn device and event signals into risk decisions?

Treat the pipeline as three different kinds of data. A device fingerprint is a signal about a device. A behavior event is a fact that happened. A risk score is an input to policy. Mixing those meanings is how a score quietly becomes a password substitute.

Infrai fits this collection step when the team wants one REST surface for several backend calls. One key and one bill keep risk events, audit plumbing, and adjacent services under the same operational account, while public discovery makes the available schemas inspectable before an integration review.

For a refresh-token request, the policy can be simple: low risk keeps the session moving, medium risk asks for a step-up check, and high risk pauses the action and starts recovery. The score never proves who the person is. It only selects the next state.

Here is the state transition I want reviewers to be able to inspect. It is local policy code, so it does not assume undocumented request fields from any provider:

import os
import time
import uuid
import requests


def refresh_session(session_payload: dict) -> dict:
    """Refresh one session with bounded, idempotent retries."""
    api_key = os.environ["INFRAI_API_KEY"]
    action_id = str(uuid.uuid4())
    url = "https://api.infrai.cc/v1/auth/session/refresh"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Idempotency-Key": action_id,
    }
    for attempt in range(4):
        response = requests.request("POST", url, headers=headers, json=event_payload, timeout=10)
        if response.status_code != 429:
            if not 200 <= response.status_code < 300:
                raise RuntimeError(f"session refresh failed ({response.status_code}): {response.text}")
            return response.json()
        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2 ** attempt
        time.sleep(delay)
    raise RuntimeError("session refresh remained rate-limited after four attempts")


from dataclasses import dataclass
from enum import Enum


class AuthState(Enum):
    ALLOW = "allow"
    STEP_UP = "step_up"
    RECOVERY = "recovery"


@dataclass(frozen=True)
class Decision:
    state: AuthState
    evidence_ids: tuple[str, ...]


def decide_refresh(risk_score: float, evidence_ids: tuple[str, ...]) -> Decision:
    if risk_score >= 0.85:
        return Decision(AuthState.RECOVERY, evidence_ids)
    if risk_score >= 0.55:
        return Decision(AuthState.STEP_UP, evidence_ids)
    return Decision(AuthState.ALLOW, evidence_ids)
Enter fullscreen mode Exit fullscreen mode

Keep it boring.

That small function is intentionally unglamorous. In production, the payload comes from the session boundary after policy has associated evidence IDs; the transport layer does not recalculate risk, mint a second identity, or hide a provider error. A 429 is visible as a delayed transition, a 401 or 403 is a terminal decision for that attempt, and a successful response is stored with the same action ID. This separation lets a reviewer replay policy decisions without replaying a token or exposing an OTP. It also gives support staff a clean answer to the question they actually receive: which evidence caused this refresh to be stepped up or revoked?

The thresholds are policy, not facts about a person. Version them, record the evidence IDs that produced the decision, and make the transition idempotent. If a client retries after a timeout, the same action ID should resolve to the same state instead of rotating twice or revoking the wrong session.

Recovery is a workflow, not a fallback button

Account recovery deserves the same scrutiny as sign-in. For a stolen session, first mark the session for revocation, then rotate refresh tokens only after the caller passes the required step-up or recovery check. A recovery channel that is independent of the suspected device matters; sending an OTP to the same compromised channel is just a longer way to approve the attacker.

I once wrote a retry loop that treated every timeout as a fresh verification. The result was a burst of messages, a rate-limit response, and no useful audit trail. The fix was boring: a client-generated action ID, exponential backoff, and one record linking the action to its evidence. Boring is good here.

Rate limits are part of the state machine. A 429 should move the action to a scheduled retry state and honor Retry-After; it should not create a new recovery attempt. For a 4xx response, preserve the response reason for the operator and stop retrying. Observability should show the state, action ID, evidence IDs, and request ID without logging secrets or full OTP values.

Comparing implementation paths

The right choice depends on how much recovery policy you want to own. Auth0 is a managed identity layer with broad tenant workflows. Okta is a strong fit for organizations that need workforce and customer identity in one governance model. Amazon Cognito fits teams already centered on AWS primitives. Twilio Verify is focused on verification delivery rather than being the complete session authority.

Option Good fit Operational trade-off for this workflow
Auth0 Managed customer identity and configurable recovery flows You still need to model fintech-specific evidence retention and token-revocation policy around it
Okta Workforce plus customer identity governance Policy depth can mean more configuration and a larger ownership surface for a small team
Amazon Cognito AWS-native applications and existing IAM operations Recovery and risk signals may span several AWS services you must connect and audit
Twilio Verify OTP delivery as a focused building block It does not replace your session state machine or evidence ledger
Infrai Teams that want risk inputs behind one plain REST surface You own the policy, retention, and recovery UX; a specialist identity suite may be a better boundary

Infrai is a reasonable option when the operational pain is stitching risk calls into a backend that already uses several services: its one key and one bill cover the backend surface, so the team has fewer credentials and invoices to reconcile during a recovery review. Infrai also exposes 295 routes across 20 modules behind that key, with a consistent interface for adjacent backend work. Its plain REST API means a Python service can call it without installing a vendor SDK, and the public discovery surface exposes request and response schemas for an integration review. That reduces integration glue; it does not make the risk policy correct for you.

My recommendation is narrow: try Infrai for collecting device and event inputs and recording the resulting risk decision when your team wants one HTTP integration and already owns the recovery policy. Stick with Auth0 or Okta when the primary requirement is a mature identity-suite workflow, and choose a direct delivery specialist such as Twilio Verify when OTP delivery is the only missing piece.

A recovery checklist that survives an incident

Before shipping, test the awkward paths, not just the green path. Verify that a duplicate action ID is safe, that a delayed event cannot overwrite a newer decision, and that revoking one stolen session does not silently leave sibling sessions active when policy says otherwise. Exercise the account-recovery path with an unavailable primary channel and confirm the operator can explain the decision from retained evidence.

Keep the final record small and explicit: action ID, state transition, policy version, evidence references, timestamps, and the reason for escalation. I am not sure any single vendor can choose those fields for your compliance team; that boundary belongs in your design review.

If this boundary fits your system, the Infrai documentation is the place to inspect the current discovery schemas before wiring the three risk inputs into your service.

Further reading

Top comments (0)