# 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](/docs/getting-started/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.

:::note
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](/docs/integration/okta/), 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):

```bash
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.

```bash
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](/docs/concepts/device-lifecycle/).

The response carries the tokens and a convenience mirror of the location
assertion:

```json
{
  "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](/docs/reference/oidc/#endpoints), and check `iss`, `aud`,
`exp`, and your `nonce` echo. Then the gate is a single check:

```json
"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](/docs/troubleshooting/faq/):
expired QR, expired code, missing `octet_user_id`, a device bound to a different
user.

## Where to go next

- [Direct OIDC integration](/docs/integration/direct-oidc/). Policy selection per
  request, `login_hint`, and library notes.
- [OIDC Reference](/docs/reference/oidc/). Every parameter, claim, and error.
