# Sessions, Consent & Logout

> How nauthera-server keeps users signed in: session cookies, lifetimes and history, when it asks for consent, and RP-initiated, front- and back-channel logout.

## Sessions

A successful sign-in creates a **login session** on the server and a cookie in the
browser. Single sign-on comes from that cookie: while it is valid, `/authorize` signs the
user in to the next application without asking for a password.

| Key | Default | Meaning |
|---|---|---|
| `auth.session.cookie_name` | `nauthera_session` | Base name of the session cookie |
| `auth.session.cookie_domain` | empty | Leave empty unless the cookie must reach subdomains |
| `auth.session.secure` | `auto` | `auto` sets `Secure` when the issuer is https, which it always is. `always` and `never` force it. |
| `auth.session.idle_ttl` | `12h` | Ends a session nobody used for this long. Each use extends it. |
| `auth.session.absolute_ttl` | `24h` | Ends every session this long after sign-in, used or not. Must be at least `idle_ttl`. |
| `auth.session.history_retention` | `2160h` | How long an ended session is kept in the session history, with where and how the user signed in and why it ended. At most `8760h`. |
| `auth.pending.cookie_name` | `nauthera_pending` | Short-lived cookie between the password and the MFA step. Must differ from the session name. |
| `auth.pending.ttl` | `10m` | How long that step may take |

### Cookie names

Sessions belong to one organization, so each organization has its own cookie. The main
organization uses the base name; any other organization appends its slug, so `acme`
uses `nauthera_session_acme`. A sign-in at one organization never signs the user in to
another.

The prefix follows the cookie's attributes:

| Attributes | Cookie name |
|---|---|
| `Secure`, no domain (the default) | `__Host-nauthera_session` |
| `Secure`, with `cookie_domain` | `__Secure-nauthera_session` |
| Not `Secure` (`secure: never`) | `nauthera_session` |

The session cookie is `HttpOnly` and `SameSite=Lax`.

### What ends a session

- The idle or absolute lifetime runs out. A sweep that runs every hour on each replica
  deletes the sessions that are past either one, and records it as
  `system.session.reaped`.
- The user signs out on the hosted UI, or an application sends them through
  RP-initiated logout (below).
- An administrator ends the session, or every session of the user, through the admin
  API or the [admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard). See
  [Admin API & Users](https://www.nauthera.io/docs/v1/user-management#sessions).
- The password is reset, by the user through an emailed link or by an administrator, or
  the user is deleted. See
  [Admin API & Users](https://www.nauthera.io/docs/v1/user-management#ending-a-users-access).
- An administrator suspends or blocks the account. Every session ends, and stays ended
  when the account is reactivated or a suspension lifts by itself at its set time. See
  [Admin API & Users](https://www.nauthera.io/docs/v1/user-management#account-status).
- The user changes a second factor in one of the ways
  [Multi-Factor Authentication](https://www.nauthera.io/docs/v1/mfa#changing-a-factor) lists. Every session of
  the user ends except the one the change was made in.

An ended session is kept in the user's session history for
`auth.session.history_retention`, with why it ended: signed out, ended by an
administrator, password reset, account suspended or blocked, second factor changed,
idle timeout or lifetime reached. Administrators read it through the admin API and the
dashboard.

Ending a session also stops the refresh tokens issued in it: the next refresh answers
`invalid_grant`. A password reset, an administrator ending every session of the user or
suspending or blocking the account, and a change of second factor go further: they
retire every refresh token the account was given until then, from any session, client or
flow, so device-code grants and grants that outlived their session stop too. A grant
from a later sign-in keeps working. See
[Tokens & Sender Constraint](https://www.nauthera.io/docs/v1/tokens#revocation-and-introspection).

A session that simply expires retires nothing. Its refresh tokens keep working until
their own lifetime runs out; an absolute lifetime for a refresh-token family is tracked
in #310.

Signing in again does not yet replace an earlier session in the same browser (#311).

## New-sign-in alerts

When a user signs in from a browser or a network they have not signed in from before,
the server emails them an alert, in their browser's language where it has one (English,
German or French), with when it happened, the browser and system, the IP address and,
with a geolocation database, the city and country. The alert links to the sign-in app's
password reset, for a user who did not sign in. It is checked when a sign-in completes
with the password, a passkey alone, an external identity provider or a second factor, and
when a registration signs the new account in.

- **A browser** is recognized by a random marker in a long-lived `HttpOnly` cookie,
  `nauthera_browser`, scoped and prefixed per organization like the session cookie.
- **A network** is the address's `/24` (IPv4) or `/48` (IPv6), so a new address in the
  same range is not new.
- Each is stored only as a digest of the user's id and the value. A user keeps up to 20
  browsers and 20 networks, and one not seen for 180 days is forgotten.
- A user with nothing remembered, such as on their first sign-in, gets no alert. One
  browser gets at most one alert a day.
- The alert needs the organization to have an
  [email relay](https://www.nauthera.io/docs/v1/organizations#email). Without one, nothing is sent.

Set `geoip.database` to a GeoLite2-City or GeoIP2-City file to name the place. The server
re-reads the file when it changes; replace it by renaming the new file into place, as
`geoipupdate` does. In the Helm chart, set `geoip.volume` to the volume that holds it.

Administrators see a user's known browsers and networks in the admin API and the
dashboard; see [Admin API & Users](https://www.nauthera.io/docs/v1/user-management#known-devices).

## Consent

Whether a user is asked to approve a client depends on how the client was registered:

| Client | Asked? |
|---|---|
| Registered through the gRPC AdminService (for example with `nauthera-cli`) | No. An administrator registered it for a fixed set of scopes, which counts as administrative consent (OIDC Core §3.1.2.4). |
| Registered through dynamic registration | Yes, the first time, and again whenever it asks for a scope the user has not approved. |
| A public client whose redirect URI is not `https` (a loopback redirect) | Yes, every time, whoever registered it. Another app on the device could claim the same redirect (RFC 8252 §8.6). |
| Any client sending `prompt=consent` | Yes |

An approval is stored per organization, user, client and resource indicator. It covers
any later request for the same or fewer scopes, and approving more scopes widens it.

Two things are missing today:

- **Approvals never expire.**
- **They cannot be withdrawn.** There is no API or screen to revoke an approval.

Both are tracked in #321.

## Logout

### RP-initiated logout

An application signs the user out by sending the browser to `/end_session` (GET or form
POST) with `id_token_hint`, `client_id`, `post_logout_redirect_uri` and `state`. The
parameters are listed in [Connect an Application](https://www.nauthera.io/docs/v1/integration#10-sign-the-user-out).

What happens next depends on who is signed in:

| Situation | Result |
|---|---|
| `id_token_hint` verifies and names the user signed in here | The session ends at once and the browser goes to `post_logout_redirect_uri`. |
| Nobody is signed in | Nothing to end. The browser goes straight to `post_logout_redirect_uri`. |
| No hint, an expired or invalid hint, or a hint for someone else | The hosted `/logout` screen asks the user to confirm. The session ends only after they do. |

The confirmation also stops a cross-site page from signing a user out with a plain link
or image. When `client_id` is sent with a valid `id_token_hint`, the client must be in
the token's audience, or the request is refused. An expired `id_token_hint` counts as no
hint, so a signed-in user is asked to confirm. Accepting it, as the spec allows, is
planned (#312). ID tokens last 10 minutes by default, so by the time a user signs out,
the ID token from sign-in has often expired.

`post_logout_redirect_uri` must exactly match a URI registered for the client named by
`client_id`, or, if no `client_id` is sent, by the audience of a valid `id_token_hint`.
After the confirmation screen only `client_id` counts. If no URI matches, the browser
goes to the server's own `/logout` page instead of the application. Send `client_id` on
every logout. Without it, `post_logout_redirect_uri` is honoured only when a valid
`id_token_hint` is sent and the user is not asked to confirm.

### Telling the applications

When a session ends through logout, the applications the user signed in to during that
session are told through the logout URIs they registered:

- **Front-channel.** Only when `/end_session` ends the session at once (the
  `id_token_hint` verifies and names the user signed in here), clients that registered a
  `frontchannel_logout_uri` get it loaded in a hidden iframe on a page the server renders
  before the final redirect. A logout the user confirms on the `/logout` screen and a
  sign-out on the hosted UI load no front-channel URIs. Even where it runs, front-channel
  is best effort: browsers that block third-party cookies can keep the application from
  seeing its own session.
- **Back-channel.** On every logout path, clients that registered a
  `backchannel_logout_uri` receive a signed logout token in a direct `POST` from the
  server. Delivery to each client is tried up to three times, five seconds per attempt,
  with backoff from one second. All deliveries for one logout stop after 30 seconds.
  Back-channel URIs can be registered only through dynamic registration.

Sessions an administrator ends, by ending them, setting a password, suspending or
blocking the account or deleting a user, are announced over back-channel logout too.

### Session management

OpenID Connect Session Management lets an application poll an iframe to notice that the
user signed out elsewhere. It is off by default (`auth.session_management.enabled`).
When it is on, the server sets an extra browser-state cookie, `nauthera_op_bs`, which is
readable by script and `SameSite=None`, and returns `session_state` on the authorization
response.

The advertised `check_session_iframe` URL answers only with a `client_id` query
parameter appended (#313), and how the frame learns which origins may talk to it is
still open (#367). Prefer back-channel logout.
