# FAQ & Troubleshooting

Indexed by what you see. Errors at the token endpoint follow standard OAuth
shapes (`error` plus `error_description`).

---

## At the login page

| Symptom | Cause and fix |
|---|---|
| QR page expires before the user finishes | The login session lives 90 seconds. Restart the login, and have the phone ready |
| Phone shows "Verification failed" | The proof did not satisfy the [policy](/docs/concepts/policies/): the user is outside the region, the proof is stale, or the spoof verdict is below the floor. The browser receives `access_denied` |
| Browser returns `error=access_denied` | Any failed verification or policy check, including a **revoked device**. This is fail-closed behaviour, not an outage |

## At `/authorize`

| Symptom | Cause and fix |
|---|---|
| `invalid_request: PKCE S256 code_challenge is required` | Your library is not sending PKCE, or sends `plain`. Enable S256 |
| `missing field state` or `missing field nonce` | Both are **mandatory**, stricter than the OIDC spec. Configure your library to send both |
| Unknown `octet_policy_id` rejected | Deliberate, because a typo must not fall back to a different policy. Check the id against your registered policies |

## At `/token`

| Symptom | Cause and fix |
|---|---|
| `invalid_request: octet_user_id is required` | Every production exchange must name the signing-in user. See the [Quick Start](/docs/getting-started/quickstart/#3-exchange-the-code-with-octet_user_id) |
| `invalid_grant: this device is bound to a different user` | The phone that proved presence is enrolled to another account. The user must use their own phone |
| `invalid_grant: a new device requires enrollment` | The user already has a bound device and just used a different one. Re-verify the user, then retry with `octet_device_enroll=true`. See [Device lifecycle](/docs/concepts/device-lifecycle/) |
| `invalid_grant: code expired / already redeemed` | Codes are single-use with a 60-second window. Exchange immediately in the callback, and check for duplicate callback invocations |

## Common questions

**Does the user have to enroll before the first login?**
No. The first successful login enrolls the phone and binds it to the
`octet_user_id` you send. It is one flow, slightly longer the first time.

**How does a user switch phones?**
Two paths. If the user still controls their account, use your own step-up
verification plus `octet_device_enroll=true`. If the phone is lost or stolen, call
[`/v1/device/revoke`](/docs/reference/oidc/#post-v1devicerevoke) and the next phone
binds automatically. See [Device lifecycle](/docs/concepts/device-lifecycle/).

**Can I get the user's coordinates?**
No. They never leave the phone. LFA can only tell you the region verdict. See
[Trust & Privacy](/docs/concepts/trust-and-privacy/).

**Why are there no refresh tokens?**
A location proof is a statement about the present moment. If you need fresh
presence, run a fresh login. The assertion's 120-second validity is the design,
not a limitation.

**Why did my OIDC library's `profile email` scopes return nothing?**
LFA advertises `openid octet.location` only and asserts no identity claims for
direct clients. The extras are ignored rather than rejected.

**Is Android supported?**
Yes. The Octet Location app runs on iOS and Android, and both reach the same
`urn:octet:loc:strong` assurance: iOS via Apple App Attest, Android via hardware
key attestation chained to a Google root. Your integration does not change. The
platform difference is invisible on the wire, and the `acr` you receive is
identical. iOS is on the App Store, and Android is in closed testing.

**We use Okta, Entra, or Auth0. Can we broker LFA?**
Yes. A broker cannot add `octet_user_id` on the fixed token exchange, so LFA
authenticates the **front channel** instead: the broker signs its `/authorize`
request as a signed request object (RFC 9101, "JAR"), and LFA binds the device on
the `login_hint` inside that verified object. Octet marks your client as a broker
server-side and pins your org's request-signing key, which you supply along with
your issuer URL. Device enrollment is forced off for brokers, so a broker user's
new phone is recovered by an admin
[`/v1/device/revoke`](/docs/reference/oidc/#post-v1devicerevoke) rather than
`octet_device_enroll`. See the [Okta guide](/docs/integration/okta/).

:::note
Broker setups are configured with us rather than self-served. A signing key is
exchanged and your org's request objects are pinned on the LFA side. Contact
[hello@octetproof.com](mailto:hello@octetproof.com) and we will validate the flow
against your org before you enable it for users.
:::

## Brokered clients (Okta, Entra, Auth0)

| Symptom | Cause and fix |
|---|---|
| `invalid_request: signed request object required` | The client is configured as a broker, so a plain `/authorize` is rejected fail-closed. Send the request as a signed object via `request=<JWS>` |
| `invalid_request` with no `login_hint` in the request object | The signed object is the only source of the bind user. Include `login_hint` inside the signed request object, not just the query string |
| Broker `/authorize` rejected even though you are sending `request=` | Okta's **Enable signed requests** toggle is off, so the object is not signed with your org key. Turn it on. It is required |
| Login fails at username matching | Leave *IdP Username* at its default. A Factor-only IdP always matches on `preferred_username`, which LFA emits for broker clients, and it ignores any custom expression you set. If it still fails, read `debugContext.debugData.errorMessage` in the System Log. The visible error text names something other than the actual cause |
| `invalid_grant` with a new device rejected for a broker user | Brokers cannot enroll a replacement device. Have an admin call [`/v1/device/revoke`](/docs/reference/oidc/#post-v1devicerevoke), and the next login binds the new phone |

:::note
Signature verification is against **pinned** keys only. LFA does not fetch your
JWKS over the network. Pin your org's current signing key and refresh it manually
when you rotate, or valid logins will start failing. Only `RS256` and `ES256` are
accepted.
:::
