DEV Community

NevilleChristensen2637
NevilleChristensen2637

Posted on

Property Account Identity: Resolve First, Inspect Before Safe Attach Decisions

Short answer: treat identity linking as a state machine with a short-lived claim, inspect ownership before any write, and attach only after the second factor proves control. In a property-management system, that order keeps a stolen device fingerprint from silently becoming the key to a tenant's rent history.

The expensive thing is what you keep

The bill for this workflow is rarely the lookup itself. It is retention: raw device signals, every tentative match, screenshots from support, and audit records that nobody expires. A building portfolio with 18,000 active accounts can accumulate millions of fingerprint observations in a year if each login writes a full JSON blob. The dominant term is bytes retained multiplied by replication and index overhead, not the handful of authentication calls.

I keep a compact, salted fingerprint reference for the risk decision, a decision code, and an immutable event ID. The raw signal goes to a short retention tier, then disappears. That creates a real trade-off: an investigator loses the ability to replay an old browser's exact shape, but the breach radius and privacy burden shrink. Your mileage may vary when a regulator requires a longer evidentiary window; document that exception instead of retaining everything by default.

Here is the retention decision I want in a design review:

Data Default retention Why Cost of deleting sooner
Derived risk score and reason code 90 days Explain a recent challenge Older support cases need a separate case ID
Salted device reference 30 days Detect a repeat device Long-running fraud rings look less connected
Raw fingerprint attributes 24 hours Re-run a disputed score Forensics must use upstream logs
Link and unlink events Account lifetime + policy window Prove who changed ownership Storage and access controls stay permanent

The uncomfortable part is intentional: retention is a security control, not just a storage setting.

Keep less.

What should resolve, inspect, and attach in a safe identity workflow?

Resolve means finding candidates without changing account state. A resolver can use a verified email, lease ID, or an existing session-bound identifier, but it should return a claim object with an expiry and a confidence reason. It must not return “the account” as if a fuzzy match were proof.

Inspect is a read-only ownership check. Compare the claim with a fresh authenticator, recent session context, and account status. For a tenant portal, that might be a passkey assertion or a one-time code delivered through a channel already on file. Device similarity is a signal; it is not possession.

Attach is the only mutating step. Make it idempotent with a unique pair such as (account_id, identity_provider, subject), and record who authorized it, when, and which policy version made the decision. If the pair already exists, return the existing link rather than creating a second row. If it belongs to another account, stop and ask for recovery; do not merge records in the login request.

I once treated a high fingerprint score as a green light in a test harness. The fixture reused a tablet image across two residents, and the score was 0.97; the resulting link crossed the household boundary. The fix was not a cleverer threshold. It was moving the ownership proof between resolve and attach, then making the database constraint enforce the same rule. That change also forced us to separate the support view from the authentication record, redact the raw attributes before they entered analytics, and replay expired claims in tests so a late mobile response could never attach an identity after its authorization window had closed.

That was the turning point.

A small state machine beats a clever callback

Keep transitions explicit: unresolved -> resolved -> inspected -> attachable -> attached, with expired and rejected as terminal states. Store the claim token server-side or encrypt it with an expiry; never trust an account ID echoed by a browser. The mutating handler should verify the token, re-check account status, and perform the insert in one transaction.

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

@dataclass(frozen=True)
class LinkClaim:
    account_id: str
    subject: str
    expires_at: datetime
    policy_version: str

def can_attach(claim: LinkClaim, now: datetime, ownership_ok: bool) -> bool:
    if now >= claim.expires_at:
        return False
    return ownership_ok

def create_claim(account_id: str, subject: str, policy_version: str) -> LinkClaim:
    return LinkClaim(
        account_id=account_id,
        subject=subject,
        expires_at=datetime.now(timezone.utc) + timedelta(minutes=5),
        policy_version=policy_version,
    )
Enter fullscreen mode Exit fullscreen mode

The five-minute value is a policy example, not a universal constant. Measure completion time and challenge abandonment, then choose a window that limits replay without trapping a resident in a slow mobile flow. Rate-limit resolution and attachment separately so an attacker cannot turn cheap lookups into an account-enumeration oracle.

Failure modes that hide in production

The common failure is a check-then-write race: two requests inspect the same claim, both see no existing link, and both insert. A unique constraint plus a transaction is the boring, correct answer. Another is cache confusion, where a risk result for one device is served to another because the cache key omits the tenant or authenticator ID.

Watch for these signals in telemetry: resolve-to-inspect latency, inspect rejection rate, attach conflicts, expired claims, and unlink events by support operator. Log stable event IDs and reason codes, not raw fingerprints or one-time secrets. Alert on a sudden rise in cross-account conflicts; it often indicates an enumeration attempt or a broken resolver rule.

Do not silently retry an attach after an authorization failure. Retry transport failures only, with an idempotency key, and surface a new inspection requirement when the claim has expired. That distinction keeps an availability problem from becoming an authorization bypass.

No magic threshold.

Choosing friction deliberately

Low friction is appropriate for a known device with a recent strong authenticator and a low-value action such as viewing a maintenance notice. Require a fresh proof for payout changes, lease transfers, or a link that would join identities from different residents. The catch is that strict challenges annoy legitimate users on shared building kiosks; in that case, keep the kiosk session scoped and use a separate recovery path rather than weakening the attach rule.

Situation Safer action User cost
New device, matching email Inspect with passkey or code One extra challenge
Shared kiosk fingerprint Do not trust device continuity Re-authentication per session
Identity already linked elsewhere Reject and open recovery Support involvement
High-risk account mutation Fresh authenticator plus step-up Noticeable friction

Standards help define the floor, not the whole policy. The OWASP Authentication Cheat Sheet covers reauthentication, credential recovery, and session handling; your threat model still decides which account actions deserve a step-up. I am not sure a single numeric risk threshold can survive every property portfolio, because resident behavior, kiosk use, and fraud pressure differ; test the policy against those distributions before shipping.

Further reading

Top comments (0)