import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Session-binding

:::note[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](/docs/concepts/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.

<Tabs groupId="platform">
  <TabItem value="swift" label="Swift (iOS)">

```swift
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
}
```

  </TabItem>
  <TabItem value="kotlin" label="Kotlin (Android)">

```kotlin
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
}
```

  </TabItem>
</Tabs>

## 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

- [Predicates](/docs/api-reference/predicates/). The `sessionNonce` parameter on each predicate.
- [Verifying Proofs](/docs/concepts/verifying-proofs/). The full list of checks, including session binding.
- [Serialization](/docs/api-reference/serialization/). What the proof commits and serializes.
