Quick Start
A complete LFA login, end to end: redirect the browser out, let the phone prove presence, exchange the code, check one claim. Nothing here requires a specific framework. It is the plain authorization-code flow plus one extra token field.
Have the prerequisites ready: a client_id, a
client_secret, a registered redirect URI, and a phone with the Octet Location
app inside your policy's region.
This is the direct integration walkthrough: your backend does the code
exchange itself and sends octet_user_id on /token. Identity brokers
(Okta, Microsoft Entra ID, Auth0) do not follow these steps. They cannot add
octet_user_id to their fixed token exchange, so they authenticate the
/authorize request with a signed request object instead. If you are wiring up
a broker, follow Okta integration, not this page.
1. Redirect to /authorize
Send the browser to LFA with standard authorization-code and PKCE parameters.
state and nonce are both required (stricter than the OIDC spec):
https://factor.octetproof.com/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.example/callback
&scope=openid%20octet.location
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
The browser lands on the LFA login page: a QR code, plus an app link if the user is already on their phone. The login session lives about 90 seconds.
2. The user proves presence
The user scans the QR code with the Octet Location app. The app runs the
location check, produces a hardware-attested proof, and submits it directly to
LFA while the browser waits. On success the browser returns to your
redirect_uri with ?code=…&state=…. On failure (wrong region, spoofing
detected, attestation failure) it returns error=access_denied.
3. Exchange the code with octet_user_id
Verify state, then exchange the code within 60 seconds. The exchange is
standard except for one field: octet_user_id, your identifier for the user who
is signing in.
curl -s https://factor.octetproof.com/token \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=https://yourapp.example/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=THE_PKCE_VERIFIER \
-d octet_user_id=usr_1234
On the user's first login this binds their phone to usr_1234. On every
later login it checks that the phone is still that user's bound device. Omitting
the field fails with invalid_request. A mismatched device fails with
invalid_grant. See Device lifecycle.
The response carries the tokens and a convenience mirror of the location assertion:
{
"token_type": "Bearer",
"access_token": "…",
"id_token": "…",
"location_assertion": "…"
}
4. Validate and gate
Validate the id_token as you would from any OIDC IdP: check the ES256
signature against the JWKS, and check iss, aud,
exp, and your nonce echo. Then the gate is a single check:
"acr": "urn:octet:loc:strong"
acr: urn:octet:loc:strong means the proof was hardware-verified end to end and
the policy passed. The sub claim is the octet_user_id you sent. The signed
region statement itself travels as the octet_location_assertion claim (and the
top-level location_assertion mirror). Most integrations never open it. It is
there for audit trails and downstream verification.
Errors
Every error in this flow is indexed in Troubleshooting:
expired QR, expired code, missing octet_user_id, a device bound to a different
user.
Where to go next
- Direct OIDC integration. Policy selection per
request,
login_hint, and library notes. - OIDC Reference. Every parameter, claim, and error.