How a Login Works
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.
The moving parts
- 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:
- Your app redirects to
/authorizeexactly as it would for any external OIDC provider (code flow, PKCE,state,nonce). - 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.
- The user scans the code. The app fetches the session's parameters, including a server-issued one-time challenge the proof must commit to.
- The app runs the Octet location check on-device: sensors, motion, and the anti-spoof pipeline produce a signed proof whose payload is a region verdict, never coordinates.
- The phone submits the proof with a hardware attestation. LFA verifies the whole chain fail-closed (signature, device identity, Apple App Attest, freshness, and the policy) and pushes the result to the waiting browser.
- 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 (Okta, Microsoft Entra ID, Auth0) 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.
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 things happen inside the same flow. The user sees a slightly longer first login:
- 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_idyou 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. No re-enrollment, no extra prompts.
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 (a broker'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. No camera, no scan. The pushed 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, the device is not enrolled yet, or the push does not arrive. Your integration is the same code either way. Only the way the phone hears about the session 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, the authorization code 60 seconds and one redemption, the location assertion 120 seconds.
- Proof freshness is bounded by the policy's
max_proof_age_s.
A proof is a statement about now. Nothing in the flow doubles as a durable credential.
Where to go next
- Policies. What "pass" means: region, attestation floor, freshness, spoof-verdict floor.
- Device lifecycle. Enrollment, binding, new phones, lost phones.