Docs/LFA/Concepts

How a Login Works

In one sentence

LFA is an OIDC identity provider that issues tokens only after the user's phone produces a hardware-attested proof of presence in the region your policy names.

Components

  • Your application (or your broker) starts the login and consumes the tokens.
  • The browser shows a QR code and waits.
  • The Octet Location app runs the location check on the phone and produces the proof. It embeds the Octet SDK, which is what measures and signs proof of location.
  • LFA (factor.octetproof.com) checks the proof (how verification works) against your policy, then answers over standard OIDC.

The flow

flowchart TD
    A[Your app redirects to /authorize] --> B[Browser shows QR code / app link]
    B --> C[User scans with the Octet Location app]
    C --> D[App runs the location check<br/>signed proof, region verdict only]
    D --> E[Phone submits proof to LFA<br/>with a hardware attestation]
    E --> F{LFA verifies:<br/>attestation, freshness, policy}
    F -->|pass| G[Browser returns to your app with a code]
    F -->|fail| H[access_denied]
    G --> I[Your app exchanges the code + octet_user_id]
    I --> J[id_token with acr urn:octet:loc:strong<br/>+ signed location assertion]

Step by step:

  1. Your app redirects to /authorize exactly as it would for any external OIDC provider (code flow, PKCE, state, nonce).
  2. The browser lands on the LFA login page (a QR code, or a tap-to-open app link on the phone itself) and waits on a live status stream.
  3. The user scans the code. The app fetches the session's parameters, including a server-issued one-time challenge the proof must commit to.
  4. The app runs the Octet location check on the phone and produces a signed proof whose payload is a region verdict, never coordinates.
  5. The phone submits the proof with a hardware attestation. LFA verifies the whole chain fail-closed (signature, device identity, platform attestation, freshness, and the policy) and pushes the result to the waiting browser.
  6. The browser returns to your app with an authorization code. You exchange it (with octet_user_id) for the tokens.

Direct integrations and brokers

The flow above shows a direct integration: your app authenticates the /token exchange with its client secret and sends octet_user_id there. That is the identifier LFA binds the attested device to (see Device lifecycle).

Identity brokers such as Okta run a fixed, standard token exchange and cannot add octet_user_id, so they bind over the front channel instead. A broker authenticates the /authorize request with a signed request object (RFC 9101, a JAR passed as request=<JWS>). LFA verifies that object against the broker's pinned signing keys and binds the device to the login_hint carried inside the verified object.

In both cases the user id LFA binds to arrives over an authenticated channel: a client-secret-authenticated /token exchange, or a signature-verified /authorize object. It is never read from the raw browser redirect. Front-channel tampering (a browser rewriting login_hint to a victim's id) breaks the signature, and the request is rejected.

Note

Direct clients are unaffected by broker handling. A stray request= parameter on a non-broker client is ignored, and direct clients keep using octet_user_id and octet_device_enroll on the RP-authenticated /token.

See Okta for the broker setup, and Device lifecycle for how binding and device replacement work in each mode.

First login versus later logins

The first time a phone is used, two one-time steps happen inside the same flow, so the first login takes slightly longer:

  • The app creates a signing key in the phone's secure hardware and has the platform certify it to LFA: Apple App Attest on iOS, hardware key attestation chained to a Google root on Android. This is device enrollment.
  • Your token exchange binds that device to the octet_user_id you sent (see Device lifecycle).

Every later login reuses the enrolled key. The phone produces a fresh hardware signature over this session's challenge, and LFA checks it against the key and binding on file. There is no re-enrollment and no extra prompt.

Push for enrolled devices

Once a device is enrolled and bound to a user, LFA can reach that phone directly. If your /authorize request identifies the user in a verified way (a broker client's signed request object carries them in login_hint), LFA sends a push notification to their bound phone instead of expecting a scan:

Location check requested

Acme is requesting a location check. Tap to review.

The user taps the notification, sees the same consent screen, and approves, without scanning a QR code. A broker login session lasts 180 seconds, and a short per-user cooldown stops repeated requests piling up on someone's lock screen.

The QR code remains the fallback: when the request does not identify the user (including every direct-client login, whose login_hint is unverified), the device is not enrolled yet, or the push does not arrive. Your integration code is the same either way. Only the delivery of the session to the phone differs.

Why a captured proof cannot be reused

Everything in the flow is single-use and short-lived:

  • The proof commits to a per-session server challenge, signed inside the proof itself, so it cannot be replayed into any other login. The guarantee holds wherever the proof travels.
  • The login session lives 90 seconds, or 180 seconds for a broker login that can be pushed to a bound phone. The authorization code lives 60 seconds and can be redeemed once. The location assertion lives 120 seconds.
  • The proof must be fresh when LFA checks it (see Policies).

Nothing in the flow can serve as a long-lived credential.

Where to go next

  • Policies. What "pass" means: country, attestation, freshness, and anti-spoof tier.
  • Device lifecycle. Enrollment, binding, new phones, lost phones.
TermsPrivacy© 2026 Understone, Inc. d/b/a Octet