Docs/Integration Guides/Okta Integration

Okta Integration

LFA plugs into Okta as a step-up location factor. Okta remains the identity authority and proves who the user is. LFA proves where they are. The mechanism is Okta's IdP Authenticator: an external OIDC IdP registered with IdP Usage = "Factor only", wrapped as an authenticator, and required by an app's authentication policy.

Broker setups are configured with you

Broker integrations need a signing key exchanged and your org's request objects pinned on the LFA side, so this one is not self-service. Work through it with us rather than in isolation. Email hello@octetproof.com and we will validate the end-to-end flow against your Okta org before you enable it for users.


How the pieces map

Okta concept LFA side
OIDC IdP, Factor only LFA's /authorize, /token, JWKS endpoints
Signed authorize request A broker client Octet pins to your org (see below)
IdP Authenticator The "Octet UK presence" factor users see
App authentication policy Where the factor becomes required
Username matching The preferred_username claim, which is Okta's default (see below)

Okta acts as a broker: it runs a fixed, standard token exchange and cannot attach custom parameters to it. Two consequences shape the whole setup.

First, Okta cannot send an octet_policy_id, so the policy is pinned per registered client. That means one client per policy, and therefore one IdP per policy. This guide uses a single Octet UK presence throughout as a running example. It is a name you choose, not a fixed value. To enforce a second region you repeat the whole setup (a new LFA client, a new Okta IdP, its own authenticator) named for that region (Octet US presence, and so on). Nothing below is shared between regions except the LFA endpoints.

Second, because Okta cannot add its own fields to the token exchange, LFA authenticates the front channel instead. Your /authorize request is cryptographically signed, and LFA trusts the signed request to say which user is logging in. That is the core of this integration, and it is what the Okta-side settings below configure.

What Octet sets up for you

A broker client is configured on the LFA side by the Octet team. You do not edit any server configuration yourself.

One client, one policy. Each client we register is pinned to a single region policy on our side (its default_policy_id), because a broker cannot choose a policy per login. Every region you want to enforce is its own client and its own Okta IdP, so UK presence and US presence means two clients and two IdPs rather than one shared setup. Name all the policies you need in step 1 and we register a client for each.

Setup takes two exchanges with us, and the order is forced: Okta generates the key that signs your requests only when you create the IdP, and you cannot create the IdP without the client ID we issue.

# Who What happens
1 You Send your Okta org URL and the policy you need, for example "UK presence"
2 Octet Issues your client_id
3 You Create the IdP in Okta with it (below) and click Finish
4 You On the IdP page click Download public key, send us that JWK
5 Octet Pins the key and confirms the client is live
6 You Finish the remaining Okta screens and test
Send the IdP's key, not your org's

Step 4 means the key on the Identity Provider page, generated for this IdP. It is not the org signing key at https://<your-org>.okta.com/oauth2/v1/keys. Those are different keys, and Okta signs request objects with the IdP's key. If we pin the org key, every login fails verification with an error visible only in our logs.

Your redirect URI is not something you choose. Okta fixes it per org, and it is shown on the IdP page:

https://<your-org>.okta.com/oauth2/v1/authorize/callback

Octet pins your issuer and that signing key to your broker client. From then on LFA accepts a signed /authorize from your org and rejects an unsigned or wrongly-signed one. That fail-closed behaviour is the point.

Notify Octet when you rotate signing keys

LFA verifies your requests against the key Octet has pinned. It does not fetch new keys automatically (automatic refresh is a planned follow-on). If you regenerate this IdP's keypair, or delete and recreate the IdP, send Octet the new public key first (again via Download public key). Otherwise signed requests start failing verification the moment the old key stops being used, and the error appears only in our logs, not yours.

See Prerequisites for what to gather before you reach out.

Okta-side configuration

1. Identity Provider

Security → Identity Providers → Add → OpenID Connect. One per policy, using the client credentials Octet issues.

Setting Value
IdP Usage Factor only
Client ID The value Octet issued you
Authentication type Public key / private key, letting Okta generate the keypair
Enable signed requests ON (required)
Algorithm RS256
Require PKCE ON
Application context Off
IdP username Leave at default (see step 2)
JIT Settings → Profile Source Off

Endpoints, to copy exactly. Okta does not read the discovery document, so all five are typed in by hand. They are the same for every customer:

Okta field Value
Issuer https://factor.octetproof.com
Authorization endpoint https://factor.octetproof.com/authorize
Token endpoint https://factor.octetproof.com/token
JWKS endpoint https://factor.octetproof.com/.well-known/jwks.json
Userinfo endpoint https://factor.octetproof.com/userinfo

No trailing slashes, and the issuer has no path. You can check these against the live discovery document at any time:

curl -s https://factor.octetproof.com/.well-known/openid-configuration | jq

Four of those settings need explanation.

Userinfo is not optional. Okta builds the user profile from the UserInfo response, not the id_token. Leaving it blank fails the login.

Signed requests also change client authentication. Turning them on switches Okta to private_key_jwt at the token endpoint, signed with the keypair Okta generates and verified by LFA against your pinned key. There is no client secret to enter. Okta hides the field in this mode, and Octet does not issue you one for a broker client.

RS256 is required. Okta verifies only RS256 and rejects LFA's default ES256.

Leave JIT off. JIT provisioning lets an external IdP create or update Okta user profiles. LFA is a factor, not an identity source, and Okta remains the authority on who the user is. It matters practically too: LFA emits email with email_verified: false, because that value is the hint your org signed rather than an address we verified. With Profile Source enabled, that unverified value could overwrite the real email on the Okta user record. Left off, the claim is only ever a matching input.

2. Username matching

Leave IdP Username at its default.

A Factor-only IdP always matches on idpuser.preferredUsername and ignores whatever you type in that field. LFA emits preferred_username for broker clients precisely so the default works.

You do not need a Profile Editor attribute, and a custom expression has no effect. If you set one, Okta still evaluates preferredUsername, and the login fails with a message that does not mention usernames at all.

3. The authenticator

The IdP by itself is inert. Okta offers it to users only once it is registered as an authenticator.

Security → Authenticators → Setup tab → Add authenticator → Identity Provider. Select the IdP you just made, and give it the name users will see at sign-in, for example Octet UK presence. Save.

4. Enrollment policy

Security → Authenticators → Enrollment tab → edit the policy that covers your users (the Default Policy, unless you have added others) → set the new authenticator to Optional. Save.

Without this the authenticator exists but no one can use it, and it does not appear in the policy picker in the next step.

5. The app policy

This is where the factor gets exercised. You need an app to sign in to and a policy that forces the factor for that app.

a. An app. If you are only testing, make a throwaway one: Applications → Create App Integration → OIDC → Web Application. Give it any redirect URI. A dead http://localhost:8080/authorization-code/callback is fine, and you never need a server there. Note two things from its General tab: the Client ID, and the exact Sign-in redirect URI Okta saved. It may differ from what you typed, so copy it verbatim. You need it to launch the test.

With Federation Broker Mode on (the default for a new OIDC app), the app is implicitly assigned to all users and access is governed entirely by its sign-on policy, so there is no separate assignment step. If you turned that off, assign yourself under the app's Assignments tab. Otherwise the policy below never runs and the login silently skips the factor.

b. A policy that forces Octet. Security → Authentication Policies → Add a policy, pick the app sign-in policy type (labelled "OIDC" or "Okta Sign-On", not account-management). A new policy comes with one Catch-all Rule. Edit that rule rather than adding another:

  • User must authenticate with: Password + Another factor
  • Possession factor constraints are: uncheck every box. A new rule ships with Device Bound required, and an external-IdP factor is not device bound, so leaving it ticked silently removes Octet from the options.
  • Authentication methods: Allow specific authentication methods, and add exactly Password and Octet UK presence, nothing else.

That last setting is what forces the factor, and it is easy to get wrong:

Do not leave the rule as "any 2 factors"

If the rule allows any second factor (the default), Okta satisfies it with whatever the user already has enrolled, usually Okta Verify, and never prompts for Octet at all. The login looks like 2FA while the location factor stays optional. Restricting Authentication methods to Password and Octet is what makes presence mandatory.

c. Attach the app to the policy. Easy to miss, and nothing works without it: on the policy's Applications tab, Add app and pick the app from (a). A policy governs nothing until an app points at it.

Setup checklist

  • The authenticator is Active on Authenticators → Setup.
  • Your test app is listed on the policy's Applications tab.
  • The rule's Authentication methods list is exactly Password and Octet UK presence, and every possession-factor constraint is unchecked.

Testing the login

Launch the app's sign-in directly at Okta. No running app is needed behind it. Build this URL, substituting your app's Client ID and the exact redirect URI from step 5a:

https://<your-org>.okta.com/oauth2/v1/authorize?client_id=<CLIENT_ID>&response_type=code&scope=openid+profile+email&redirect_uri=<REDIRECT_URI>&state=test&nonce=test

Open it in a fresh private or incognito window. Expected sequence:

  1. Okta login: username and password.
  2. "Verify with Octet UK presence". On the first run Okta enrols it in the same step. If you instead see only Okta Verify and Password, the rule is not forcing the factor, so recheck step 5b.
  3. The app redirects to your phone, and the phone proves presence.
  4. The browser lands on your dead redirect URI showing …/callback?code=…&state=test.

That final ?code=… on a dead page is success. It means Okta completed the whole exchange and issued an authorization code. The connection error is expected, because you never ran a server there.

After any policy change, start a brand-new sign-in

Okta snapshots the policy at the moment a sign-in begins and holds it for the whole flow. A tab you started before a change keeps the old rules no matter how many times you save. Always retest in a fresh private window, never the error screen's Verify button, which resumes the old transaction.

What travels where

  1. Okta → LFA /authorize. Okta signs the request as a JWS request object (request=<JWS>). LFA verifies it against the key pinned to your broker client. The issuer, audience, and expiry must check out, and the parameters inside the signed object take precedence over anything in the URL. The login_hint inside that signed object is the user LFA binds the device to. A signed request that carries no login_hint is rejected.

  2. Phone → LFA. A signed location proof plus Apple App Attest, carrying a region verdict only, no raw coordinates. See Trust & Privacy.

  3. LFA → Okta. An RS256 id_token (verifiable against LFA's JWKS) with:

    • acr: urn:octet:loc:strong, meaning hardware-attested presence with the policy passed.
    • octet_location_assertion, the signed region-verdict statement.
    • preferred_username, the hint from the signed request. This is what Okta matches the user on.
    • octet_login_hint, plus email with email_verified: false when the hint is an address. The same claims are returned from /userinfo, which is where Okta reads them.

The device binds to the login_hint from the signed request, never a raw value from the browser's URL. A tampered front-channel request therefore cannot bind a victim's account: editing the hint breaks the signature, and the request is rejected.

Device replacement and recovery

A broker login binds the user's phone on first success, the same as any LFA login (see Device Lifecycle). Because Okta cannot send device-enrollment fields on the token exchange, a broker user's new or different phone is rejected (invalid_grant) rather than silently rebinding.

To move a user to a new phone, ask Octet to revoke the binding (POST /v1/device/revoke). The user's next login then binds the replacement phone automatically.

Troubleshooting

Symptom Cause Fix
/authorize rejected as an unsigned or invalid request Signed requests are off, or the key Octet pinned is not the one Okta is signing with Turn Enable signed requests on. Otherwise re-send the key from Download public key on the IdP page, not /oauth2/v1/keys, which is a different key and a common mix-up
"Octet UK presence" never appears at the verify screen, only Okta Verify and Password The rule is not forcing it: it allows any 2 factors, a possession-factor constraint (Device Bound) is filtering the external-IdP factor out, or (with Federation Broker Mode off) you are not assigned to the app Step 5b: set Authentication methods to Allow specific → Password + Octet, uncheck all possession constraints, and confirm the app is assigned
Authorization error: signed request object failed verification on LFA's page The public key on file does not match the one Okta is signing with Re-send the key from Download public key on the IdP page so Octet can re-pin it. This mismatch does not show up in Okta's own logs
Login rejected with no bind user The signed request reached LFA without a login_hint Ensure Okta is sending login_hint on the authorize. A broker request without it is rejected
"Unable to verify authenticator", or "The UserInfo response is invalid" Okta reports several unrelated failures with these two strings, and neither describes the real cause Open Reports → System Log, expand the Authenticate user via IDP failure, and read debugContext.debugData.errorMessage. It names the exact check that failed. The summary text does not
Changing IdP Username makes no difference Expected. A Factor-only IdP ignores that field Leave it at the default. Matching is always on preferred_username
Login fails immediately after a config change, but the settings look right An in-flight sign-in keeps the configuration it started with Retest from a fresh private window, not the error screen's Verify button
User's new phone rejected (invalid_grant) The user is already bound to a previous device Ask Octet to revoke the binding. The next login rebinds the new phone
QR code or login expires before completion The login session (90 s) or the authorization code (60 s) lapsed Restart the login. Presence proofs are deliberately short-lived

Where to go next

  • Policies. What the pinned policy enforces.
  • Device Lifecycle. Binding, revocation, and new phones on the broker path.
  • Direct OIDC integration. The same endpoints without a broker, where your token exchange sends octet_user_id directly.