# `Attesto.SessionState`
[🔗](https://github.com/XukuLLC/attesto/blob/v1.3.0/lib/attesto/session_state.ex#L1)

OpenID Connect Session Management 1.0 — the `session_state` value.

Session Management lets a Relying Party poll, from JavaScript, whether the
End-User's login state at the OP has changed without a network round trip:
the RP posts `client_id + " " + session_state` to the OP's
`check_session_iframe`, which recomputes the value from the *current* OP
browser state and answers `unchanged` or `changed` (§3.1/§3.2).

`session_state` is returned to the client as an additional authorization
response parameter (§2) and is computed as (§3.2):

    hex(SHA256(client_id <> " " <> origin <> " " <> op_browser_state <> " " <> salt)) <> "." <> salt

  * `client_id` — the RP's client identifier.
  * `origin` — the origin of the Authentication Response's `redirect_uri`
    (see `origin/1`), which is what the browser reports as
    `MessageEvent.origin` when the RP's iframe posts to the OP iframe.
  * `op_browser_state` — the OP User Agent state: an opaque value stored in
    a JavaScript-readable cookie at the OP origin that changes on
    login/logout, so a recomputation with a stale value yields `changed`.
  * `salt` — a random per-response salt carried in cleartext after the `.`
    so the OP iframe can recompute the hash.

The hash is lowercase hex, matching the §3.2 example's `CryptoJS.SHA256`
string form, so the OP iframe's JavaScript recomputation compares equal. The
value contains no space character (§2).

Like the rest of attesto core this module is pure: the host owns the browser
state cookie and the `check_session_iframe` page; this module owns the
computation both sides must agree on.

## OP-owned, login-bound browser state

The `session_state` recipe treats `op_browser_state` as an opaque string, so
the OP is free to give that string internal structure. Session Management
requires the OP browser state to be **OP-owned** (an RP-visible / injected
value must not be able to forge `unchanged`) and to **change when the
End-User's login state changes** (§3.2). `mint_browser_state/2` and
`browser_state_valid?/3` give the value both properties without changing what
the iframe hashes:

    random . login_tag . mac

  * `random` — fresh 128-bit entropy, unguessable cross-origin;
  * `login_tag` — `HMAC(secret, login_binding)`, a stable fingerprint of the
    current End-User login state (subject / auth_time / sid); a re-auth or
    account switch changes it, so a stale cookie fails `browser_state_valid?/3`
    and the OP rotates;
  * `mac` — `HMAC(secret, random . login_tag)`, so a value the OP did not mint
    (a cookie injected by a sibling/parent-domain origin) cannot verify.

Only the OP knows `secret`. The whole `random . login_tag . mac` string is
still what `compute/4` hashes as `op_browser_state`, so the iframe recipe is
unchanged.

# `browser_state_valid?`

```elixir
@spec browser_state_valid?(binary(), binary(), binary()) :: boolean()
```

Whether `value` is an OP browser-state string this OP minted (under `secret`)
for the current `login_binding`.

Returns `false` — so the caller mints a fresh value — when either:

  * the value is malformed or its MAC does not verify: a value the OP never
    minted (a forged / cross-origin-injected cookie), which must not be
    trusted as authoritative browser state; or
  * the MAC verifies but the embedded `login_tag` is for a different login
    state: the End-User re-authenticated or switched accounts, so the OP
    browser state MUST rotate (Session Management 1.0 §3.2) and any earlier
    RP `session_state` becomes `changed`.

Both comparisons are constant-time.

# `compute`

```elixir
@spec compute(String.t(), String.t(), String.t(), String.t()) :: String.t()
```

Compute the `session_state` for an authorization response (§3.2).

`origin` must be a browser-form origin (`scheme://host[:port]`, default port
omitted) — derive it from the response's `redirect_uri` with `origin/1`.
`salt` defaults to a fresh `generate_salt/0`.

# `generate_browser_state`

```elixir
@spec generate_browser_state() :: String.t()
```

A fresh OP browser state value.

The host stores it in a JavaScript-readable cookie at the OP origin (§3.2 —
the `check_session_iframe` script must read it, so `HttpOnly` cannot be set)
and changes it when the End-User's login state changes (login/logout).

# `generate_salt`

```elixir
@spec generate_salt() :: String.t()
```

A fresh random salt for `compute/4` (unpadded URL-safe Base64, no spaces or dots).

# `mint_browser_state`

```elixir
@spec mint_browser_state(binary(), binary()) :: String.t()
```

Mint an OP browser-state value that is OP-owned and bound to the End-User's
current login state (see the module doc).

`secret` is an OP-only HMAC key; `login_binding` is a caller-chosen string
that captures the current login state (e.g. `subject`, `auth_time`, and `sid`
joined together). The returned `random . login_tag . mac` string contains no
space or `.`-ambiguity in its parts (each part is base64url-no-pad), so it is
a valid `op_browser_state` for `compute/4` and splits back cleanly in
`browser_state_valid?/3`.

# `origin`

```elixir
@spec origin(String.t()) :: {:ok, String.t()} | {:error, :invalid_uri}
```

The browser-form origin of `uri` (RFC 6454): `scheme://host`, with the port
appended only when it is not the scheme's default — exactly the string the
browser reports as `MessageEvent.origin` for a page loaded from `uri`.

Returns `{:ok, origin}` or `{:error, :invalid_uri}` for a URI with no
scheme/host (a `session_state` computed over a malformed origin could never
compare equal in the browser, so fail closed instead).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
