Docs/Concepts/Session-binding

Session-binding

In short

Pass a one-time sessionNonce to any predicate. The SDK commits a hash of it inside the signed proof, so your verifier can confirm the proof was made for one specific login rather than replayed from another.

The problem it solves

A proof of location says a device was inside a region at a time. On its own it does not say which request it was made for. An attacker who captures a valid proof from a device could forward it to authorize a different login from that device, or replay it later. For a login or a step-up check, you want the proof bound to the exact session you are authorizing.

How it works

  1. Your login backend issues a one-time nonce for this login attempt and sends it to the app.
  2. The app passes it as sessionNonce on a predicate call: isWithin, isOutside, or contains.
  3. The SDK hashes the nonce and commits the hash inside the signed proof. Only the hash is serialized. The raw nonce never rides in the proof.
  4. The app forwards verdict.proof to your backend.
  5. Your backend verifies the proof with octet-verify 1.2.0 or later, then compares the committed hash against the nonce it issued. A match ties the proof to that login.

The hash sits inside the signed proof, so editing it after signing breaks verification. A proof made for one nonce does not verify as a proof for another.

The nonce is opaque bytes, 1 to 512 of them, so it can be a raw login nonce, a hashed token, or any per-request challenge. An empty nonce, or one over 512 bytes, returns an INDETERMINATE verdict with reason INVALID_SESSION_NONCE and no proof. A session-bound call always generates a fresh proof rather than serving one from cache, so each call reflects the device's location at that moment.

let nonce = try await loginBackend.issueNonce()   // one-time login nonce
let verdict = await sdk.loc.isWithin(
    region: .country(isoCode: "US"),
    sessionNonce: nonce
)
if verdict.result == .yes {
    send(verdict.proof!, to: loginBackend)         // backend re-checks the nonce hash
}
val nonce: ByteArray = loginBackend.issueNonce()   // one-time login nonce
val verdict = sdk.loc.isWithin(
    region = OctetRegion.country("US"),
    sessionNonce = nonce,
)
if (verdict.result == OctetVerdict.Result.YES) {
    send(verdict.proof!!, loginBackend)            // backend re-checks the nonce hash
}

What the verifier does

octet-verify 1.2.0 or later confirms the proof commits a session-nonce hash and that the commitment is bound into the signed proof. A verifier below 1.2.0 does not recognize the stage and reports it as NOT-CHECKED, validating the rest of the proof. Comparing the committed hash against the nonce you issued is your backend's step, because only your backend knows which nonce it sent.

When to use it

  • Login and step-up checks. Bind the location proof to the specific login you are authorizing.
  • Wire-approval and other high-value actions. Bind the proof to the action's server-issued token.

Omitting sessionNonce produces the same proof as 1.1. An integration that does not pass it is unchanged.

Availability

Session-binding is available from SDK 1.2.0. Enforcing it needs octet-verify 1.2.0 or later. The proof wire format stays a strict superset of 1.1: a proof made without a nonce is byte-identical to a 1.1 proof.

Where to go next