DEV Community

JensenCole5829
JensenCole5829

Posted on

FastAPI Property Workforce Access: Immediate Offboarding Through Lease-Scoped Grants

For a property-management workforce access lifecycle, account creation is the easy part; the decisive test is what happens after a staff change. A property manager who leaves at 10:02 must lose access to tenant records at 10:02, even if a phone one-time-code session would otherwise remain valid all afternoon. That constraint changes the migration plan: move identity data behind a stable workforce subject, keep property grants in the application, and make every request consult revocable server-side session state.

Short answer: treat account creation, updates, and offboarding as ordered state transitions, not three unrelated provider calls; a FastAPI authorization layer should deny a disabled subject immediately while phone OTP remains only the login ceremony.

This is an authorization decision before it is an authentication integration. A managed identity provider can prove control of a phone number, but the property app still has to decide whether that person may open maintenance photos for Building 14, export a rent roll, or enter an occupied unit. During a provider migration, preserving that decision boundary matters more than reproducing every dashboard setting.

How should workforce access handle account updates and immediate offboarding?

Start with one immutable workforce_subject_id generated by the application or authoritative directory. Email address, phone number, display name, manager, and provider identifier are mutable attributes; none is a durable join key. A changed phone number should update an attribute. It should not silently create a second employee or inherit access from a recycled number.

Model the lifecycle as a small state machine: pending, active, suspended, and disabled. Creation records a subject in pending; an explicit activation step adds approved, lease-scoped grants. Updates replace attributes and grants using an event version. Offboarding writes disabled, increments a session epoch, revokes refresh material, and removes active grants in the same application transaction. The epoch gives request middleware a cheap revocation check: a session minted at epoch 7 cannot authorize anything after the subject moves to epoch 8.

Order is part of the contract. A delayed profile update with version 41 must not reactivate a subject already disabled by version 42. Store the latest source version, reject older events, and make repeated delivery idempotent. If the source cannot provide a monotonic version, serialize changes per subject and retain a source event ID for deduplication. I'm not sure every upstream HR system exposes enough ordering metadata; a migration spike should resolve that before cutover, because timestamp comparison alone is vulnerable to clock skew and coarse timestamp precision.

Revocation comes first.

In practice, “immediate” needs a testable service-level definition. It should mean that once the application commits the disable transition, the next protected request returns 401 for an invalidated session or 403 for a still-authenticated subject without the requested grant. It cannot honestly mean “the instant HR learns something” unless the HR-to-directory and directory-to-app delivery paths also have bounded latency. Measure those intervals separately instead of hiding them inside one offboarding metric.

Keep phone OTP separate from employment status

Phone OTP is a possession check, not proof of current employment. It is also a poor place to store authorization semantics. NIST SP 800-63B treats use of the public switched telephone network as a restricted authenticator and tells verifiers to consider signals such as SIM changes and number porting. For a workforce tool, that means a successful code can establish an authentication session only after the server maps the provider identity to an active workforce subject.

The lookup should be (issuer, provider_subject) -> workforce_subject_id, never phone -> employee. Normalize and verify a new phone number through a controlled update flow, require reauthentication for sensitive account changes, and notify through an independent channel. OWASP likewise recommends reauthentication after risk events and credential changes. Don't let an administrator edit a phone field and thereby bypass verification.

This separation also makes migration reversible. During a controlled overlap, old and new authentication issuers may map to the same workforce subject, while policy and grants remain unchanged. Once sessions from the old issuer have expired or been revoked, remove that mapping. The catch is that dual-issuer operation expands the account-linking surface, so it is not suitable when the team cannot review collisions and audit every link. In that case, use a maintenance window and require fresh login through the new issuer rather than attempting automatic matching.

A focused FastAPI transition model

The useful unit of testing is a lifecycle command plus its authorization effect. The example below deliberately omits SMS delivery and database plumbing. It shows the part that should survive a managed-provider migration: ordered transitions, a monotonic session epoch, and lease-scoped grants.

from dataclasses import dataclass, field
from enum import StrEnum


class Status(StrEnum):
    PENDING = "pending"
    ACTIVE = "active"
    SUSPENDED = "suspended"
    DISABLED = "disabled"


@dataclass
class WorkforceSubject:
    subject_id: str
    status: Status = Status.PENDING
    source_version: int = 0
    session_epoch: int = 0
    grants: set[str] = field(default_factory=set)


def apply_lifecycle_change(
    subject: WorkforceSubject,
    *,
    source_version: int,
    status: Status,
    grants: set[str],
) -> bool:
    """Apply a newer desired state; return False for stale or duplicate input."""
    if source_version <= subject.source_version:
        return False

    was_enabled = subject.status in {Status.PENDING, Status.ACTIVE, Status.SUSPENDED}
    subject.source_version = source_version
    subject.status = status
    subject.grants = grants if status is Status.ACTIVE else set()

    if status is Status.DISABLED and was_enabled:
        subject.session_epoch += 1
    return True


def authorize(
    subject: WorkforceSubject,
    *,
    session_epoch: int,
    required_grant: str,
) -> None:
    if subject.status is not Status.ACTIVE or session_epoch != subject.session_epoch:
        raise PermissionError("authentication_required")
    if required_grant not in subject.grants:
        raise PermissionError("grant_required")
Enter fullscreen mode Exit fullscreen mode

In a real FastAPI service, load the subject in authentication middleware or a dependency, compare the session's epoch, and translate the two denial reasons consistently. More important, wrap the status, epoch, grant, and audit writes in one database transaction. Otherwise, a crash between “disabled” and “grants removed” creates a state the model says cannot exist.

The experiment should have an eval harness, not a click-through checklist. Generate transitions for two employees with adjacent identifiers and two buildings, then assert that a version-12 disable blocks the old session, a replay of version 11 changes nothing, a phone update does not change grants, and a newly active replacement cannot inherit the former employee's sessions. Add a concurrency case in which update 21 and disable 22 arrive together. The expected final state is deterministic: disabled, no grants, epoch incremented exactly once. Run these tests against both provider adapters during the overlap period. This is the auth equivalent of keeping a model eval fixed while swapping an inference backend — the adapter changes, but the assertions don't.

Where directory products stop and application policy begins

SCIM 2.0 defines standard operations and resource schemas for provisioning users and groups. It is useful for the create/update/disable transport boundary, yet RFC 7644 does not define how an application revokes its own cookies, refresh tokens, cached policy decisions, database connections, or queued jobs. A clean SCIM response therefore proves synchronization, not complete offboarding.

Microsoft Entra ID, Okta, and JumpCloud illustrate three different control-plane shapes without changing that boundary. Entra documents provisioning applications through SCIM and ties workforce controls into a Microsoft tenant. Okta documents lifecycle management around its directory and provisioning integrations. JumpCloud documents SCIM provisioning from its cloud directory alongside device-oriented administration. Those are relevant operational differences for an internal-tool estate, but none removes the application's responsibility to invalidate local sessions and enforce property-level grants. Compare them on source-of-truth fit, event ordering, group semantics, audit export, authenticator policy, and failure visibility; don't score the migration on the phone-code screen alone.

A standards-based adapter still has costs. SCIM schemas need mapping, group changes can fan out, and an asynchronous provisioning path cannot provide synchronous revocation by itself. Stick with a managed lifecycle product when the team lacks on-call ownership for identity synchronization or when the product's conditional-access and device controls are required by policy. A self-managed policy boundary is not suitable when the app cannot maintain transactional state and security telemetry. Conversely, even with a managed directory, keep sensitive resource authorization local when only the application understands which property assignments are valid.

Measure revocation, drift, and recovery before cutover

The main metric is disable-to-deny latency, split into source detection, event delivery, application commit, and cache invalidation. Track stale-event rejections, duplicate deliveries, subjects with no provider mapping, active sessions for disabled subjects, and grants that differ from the authoritative assignment feed. Alert on the invariant violation, not merely on webhook failures.

Keep the logs specific: subject ID, source event ID, source version, old and new status, actor, correlation ID, and committed session epoch. Exclude OTP values and avoid raw phone numbers; those do not help an authorization audit. Retention and access to this log should follow the organization's legal and security policy.

Before copying this design, run a cutover drill with synthetic staff and property assignments. The release gate should prove five things: stale updates cannot resurrect access; disabling a subject blocks an already-open session on its next protected request; changing a phone number does not alter the workforce identity; a cache cannot extend authorization beyond the declared revocation target; and rollback does not create two active subjects for one employee. Also measure the operational cost — adapter maintenance, audit storage, SMS usage, and incident response time — because a design that passes correctness tests but exceeds the team's support capacity is the wrong design.

Small detail, big consequence.

The final decision rule is straightforward: migrate the authentication provider only after workforce identity and authorization can be evaluated independently of it. Account creation should establish a durable subject, updates should be ordered desired-state changes, and offboarding should synchronously invalidate application sessions. The phone OTP flow can then change without redefining who the employee is or what tenant data they may reach.

References

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

The approach of treating account updates and offboarding as state transitions rather than isolated events is a crucial insight. This not only strengthens security by ensuring timely revocation but also simplifies the complexity of managing user identities across various systems. I appreciate how you've emphasized the importance of maintaining a clear decision boundary during provider migrations; it’s a vital consideration in preventing unintended access. If you're looking for help with implementing the state machine logic or any related development tasks, I'd be glad to explore a paid collaboration. What are your thoughts on the potential challenges of integrating with existing HR systems that may not provide sufficient ordering metadata?