Docs/LFA/Reference

OIDC Reference

The complete wire contract: endpoints, protocol requirements, parameters, claims, and errors. Discovery is live at https://factor.octetproof.com/.well-known/openid-configuration.


Endpoints

Endpoint URL
Issuer https://factor.octetproof.com
Authorization https://factor.octetproof.com/authorize
Token https://factor.octetproof.com/token
UserInfo https://factor.octetproof.com/userinfo
JWKS https://factor.octetproof.com/.well-known/jwks.json
Device revocation (LFA extension) https://factor.octetproof.com/v1/device/revoke

Protocol requirements

LFA implements only the authorization-code flow, with these requirements:

Requirement Value
Response type code only
Grant authorization_code only, with no refresh tokens
PKCE S256 required. An /authorize request without a code challenge is rejected
state and nonce Both mandatory (stricter than the OIDC spec, which only recommends state)
Client authentication At /token: client_secret_post, client_secret_basic, or private_key_jwt (RFC 7523 §2.2, broker clients, verified against the same pinned keys as the request object). /v1/device/revoke accepts a client secret only
Authorization code Single-use (enforced atomically), 60-second redemption window
id_token signing ES256 by default, RS256 available per client. Both keys are published at the JWKS endpoint with distinct kids, and the id_token header names the one that signed it. Ask us to switch your client to RS256 if your stack cannot verify EC signatures. Okta, for example, verifies only RS256
Scopes openid and octet.location, the only scopes advertised. Extras a library adds (profile email) are ignored
UserInfo Accepts the access token only, which is valid for 120 seconds. An id_token or location assertion presented as a bearer token is rejected (401)

/authorize parameters

Beyond the standard authorization-code parameters:

Parameter Purpose
octet_policy_id Selects which registered policy this login must satisfy. Omit for the client's pinned default. An unknown id is rejected
login_hint Optional and unverified, echoed back as the octet_login_hint claim
request A signed request object (RFC 9101 JAR). Required for broker clients, ignored for direct clients. See below

Signed request objects (brokers)

Identity brokers, such as Okta's IdP-Authenticator "Factor only", run a fixed, standard token exchange and cannot add the octet_user_id that a direct client sends at /token. A broker instead authenticates the front channel: it signs its /authorize request as a JWS and passes it as request=<JWS>, and the user it binds is taken from inside that signed object.

A client is a broker client when Octet has registered your broker's issuer and request-signing public keys for it. This is fail-closed: once a client is a broker, an /authorize without a valid signed object is rejected. For a direct client a stray request= is ignored.

Check Rule
Verification Verified against the broker's pinned keys only, with no network key fetch. RS256 and ES256 only. none and HS* are rejected
iss Must equal the configured broker issuer
aud Must equal LFA's own issuer (https://factor.octetproof.com)
exp / nbf Must be valid, in a strict 3-segment JWS
client_id The object's client_id must equal the query client_id (RFC 9101 §5)
login_hint Required inside the object. Becomes the broker-authenticated binding identifier. An object with no login_hint is rejected

Parameters inside the signed object take precedence over the query string: response_type, redirect_uri, scope, state, nonce, the PKCE challenge and method, acr_values, and octet_policy_id.

Note

The device binds only to the login_hint from the signed object, never the raw front-channel query login_hint. A browser that edits the query login_hint to a victim's id breaks the signature, so the request is rejected. See Okta integration for the broker setup.

/token parameters

Beyond the standard code-exchange parameters (grant_type, code, redirect_uri, client credentials, code_verifier):

Parameter Purpose
octet_user_id Required for direct clients. Your stable, opaque identifier for the user signing in. Drives device-to-user binding
octet_device_enroll Optional. true replaces the user's bound device with the one that just proved presence, revoking the old device. Send only after your own step-up verification

There are two binding sources, depending on the client:

  • Direct clients send octet_user_id on the RP-authenticated /token, as documented above, and use octet_device_enroll for device replacement.
  • Broker clients bind on the login_hint from the signed request object at /authorize (see Signed request objects) and cannot send octet_user_id or octet_device_enroll. Enrollment is forced off, so a new or different device for an already-bound user is rejected (invalid_grant). To recover, ask Octet to revoke the binding with POST /v1/device/revoke. The next login binds the new phone automatically. A broker session that reaches /token with no authenticated hint is rejected regardless of attestation.

The token response:

{
  "token_type": "Bearer",
  "access_token": "…",
  "expires_in": 120,
  "id_token": "…",
  "location_assertion": "…"
}

location_assertion is a top-level mirror of the octet_location_assertion claim, for stacks that do not want to open a nested JWT.

id_token claims

Claim Meaning
iss https://factor.octetproof.com
sub The bound user id, returned verbatim: the octet_user_id you supplied at /token for a direct client, or the signed-request login_hint for a broker client
aud Your client_id
exp, iat Standard timestamps
auth_time When the location proof was accepted, which is the moment the user authenticated
nonce Echo of your nonce. Verify it
acr urn:octet:loc:strong, meaning the proof was hardware-attested (Apple App Attest on iOS, hardware key attestation on Android) and the policy passed
amr ["octet-loc"]
token_use id (the access token carries access)
octet_login_hint Echo of the login_hint, if any. Unverified for a direct client (front-channel passthrough). For a broker client it is the signature-verified hint from the request object, the same value bound as sub
preferred_username Broker clients only. The signed hint, echoed back so account-matching works with default configuration. Not emitted for direct clients
email, email_verified Broker clients only, and only when the signed hint is an email address. email_verified is always false, because LFA verifies location rather than mailboxes. Do not treat it as a verified address
octet_location_assertion The signed location assertion (nested JWS) stating the region verdict

Why brokers get identity claims and direct clients do not

A direct client's login_hint is an ordinary query parameter. Anyone can put any value there, so reflecting it into email or preferred_username would let a caller impersonate someone to their own RP. A broker's hint arrives inside a signature-verified request object, signed by a key LFA has pinned, and LFA returns it only to the party that signed it. LFA does not vouch for the identity. It returns the broker's own assertion, which is why email_verified stays false.

GET /userinfo

Returns the same identity claims as the id_token: sub, octet_login_hint, and (for brokers) preferred_username, email, email_verified. Some products build their user profile from UserInfo rather than the id_token, so the two return the same identity claims. Present the access token as a bearer token. An id_token or location assertion is rejected with 401.

The location assertion

A compact ES256 JWS, verifiable against the same JWKS, asserting the region verdict for this login. It expires 120 seconds after issue and is bound to the login's nonce. It is a statement of presence at that moment, for audit use, not a credential. The subject is device-rooted (the enrolled device, not your user id), so the assertion can be verified downstream without carrying your user identifiers.

POST /v1/device/revoke

Revokes the device bound to one of your users. Use it when a phone is lost or stolen, and to move a broker user to a new phone. Direct clients call it themselves. Broker clients hold no client secret, so Octet calls it for them. See Device lifecycle.

Authentication Client secret only (client_secret_post or client_secret_basic). private_key_jwt is not accepted here
Body octet_user_id=<the user whose device to revoke>. For a direct client this is the octet_user_id you send at /token. For a broker client it is the login_hint value from the signed request object
Effect The user's bound device is denied from the next proof submission onward. Their next successful login on a new phone binds it automatically
Response 200 {"revoked": true, "device_id": "<id>"}
Errors 404 no_device when no active device is bound to that user. 401 invalid_client for bad credentials. 400 invalid_request when octet_user_id is missing
Scope Per client. You can only revoke bindings under your own client_id

Errors

Error Where Meaning
Error page on LFA: unknown client_id /authorize The client_id is not registered. LFA shows its own page (HTTP 400) and does not redirect
Error page on LFA: redirect_uri not registered for this client or missing required parameter: redirect_uri /authorize The redirect_uri must match a registered one exactly. Email hello@octetproof.com to add one
Error page on LFA: this client must send a signed request object /authorize A broker client sent no request= object
Error page on LFA: signed request object failed verification /authorize The object failed signature, iss, aud, exp, or nbf verification, or used an unsupported algorithm
Error page on LFA: request object client_id mismatch /authorize The client_id inside the signed object does not match the query client_id (RFC 9101 §5)
Error page on LFA: request object must carry login_hint /authorize A broker's signed object carried no login_hint, so there is no user to bind
invalid_request (PKCE, state, or nonce missing) /authorize The request does not meet the protocol requirements
invalid_scope (scope must include openid) /authorize Add openid to the requested scopes
unsupported_response_type /authorize Only response_type=code is supported
invalid_request (unknown octet_policy_id) /authorize The policy id is not registered for your client. LFA does not fall back to another policy
HTTP 429 (try again shortly) /authorize LFA could not start a login session. Retry after a short wait
access_denied callback redirect The phone's proof failed verification or policy: wrong region, stale proof, spoof verdict below the floor, attestation failure, or revoked device
invalid_grant (code expired or already redeemed) /token Codes are single-use and live 60 seconds
invalid_client (HTTP 401) /token The client credentials are missing or wrong
invalid_grant (PKCE code_verifier does not match challenge) /token The code_verifier is not the one whose S256 hash you sent as code_challenge
invalid_grant (redirect_uri mismatch) /token The redirect_uri differs from the one sent at /authorize
invalid_grant (client_id does not own this code) /token The code was issued to a different client
invalid_request (octet_user_id is required) /token Exchanges from a direct client must carry the user identifier
invalid_grant (this device is bound to a different user) /token The proving phone belongs to another account
invalid_grant (a new device requires enrollment) /token The user already has a bound device. See Device lifecycle
401 /userinfo The bearer token is not an access token (token_use mismatch)
TermsPrivacy© 2026 Understone, Inc. d/b/a Octet