struct

KemalIdentity::Sessions::Record

Inherits Struct < Value < Object

One row of auth_sessions: everything the shard knows about a live session.

The browser holds a high-entropy random secret and nothing else. This is the server-side half, and it stores only the digest of that secret — so a leaked database backup yields no usable session cookies (docs/02-security-model.md).

Constructors

new(id : String, account_id : String, token_digest : Bytes, auth_version : Int32, assurance : AssuranceLevel, created_at : Time, authenticated_at : Time, last_seen_at : Time, idle_expires_at : Time, absolute_expires_at : Time, tenant_id : String | Nil = nil, mfa_verified_at : Time | Nil = nil, password_verified_at : Time | Nil = nil, revoked_at : Time | Nil = nil)
Source

Instance methods

absolute_expires_at
Source
account_id
Source
assurance
Source
auth_version

The account's auth_version when this session was minted. A mismatch against the account's current value fails the session.

Source
authenticated_at

When the credential behind assurance was verified. Feeds Principal#fresh?.

Source
created_at
Source
idle_expires_at
Source
inspect(io : IO) : Nil

Never prints the digest. It is not a password, but it is the value that resolves a session, and docs/02-security-model.md forbids logging it.

Source
last_seen_at

Moved forward as the user stays active, but throttled: only written when now - last_seen_at exceeds the configured touch_interval (60 s by default). Without that throttle, every authenticated read becomes a write, which is the single biggest performance trap in this design. The cost is that idle expiry is accurate only to within one touch_interval, which is part of the contract rather than an implementation accident (docs/02-security-model.md).

Source
mfa_verified_at
Source
password_verified_at

When the password behind this session was last actually typed, if it ever was.

Separate from authenticated_at because that one is restamped by every assurance increase, and separate from assurance because a level says one factor was proved without saying which: a federated login sits at Password too. nil for a session no password produced — a remembered browser, a federated login, an adopted legacy session — and a guard reads nil as "no", never as "unknown, allow it".

Source
revoked?
Source
revoked_at
Source
tenant_id

The account's tenant as it was when this session was minted, and the only authorization input this shard copies into a session.

Service#resolve rebuilds the principal from this row, not from the account, so a change to the account's tenant is not felt by a session that already exists — for the rest of that session's life, which is a longer window than anything else in the authorization path has. Membership and roles are read on every decision; a grants Cache bounds itself to a minute; this is bounded only by the session's absolute deadline.

It is a copy on purpose: Lookup deliberately does not carry the account's tenant, and widening it would put another column of a join on every authenticated request for a value that changes once in an account's lifetime, if ever. The consequence is that an application that changes an account's tenant must revoke that account's sessions or bump its auth_versiondocs/02-security-model.md lists it with the other events that require this, and Accounts::Repository#bump_auth_version is the one-row way to do it.

Source
to_s(io : IO) : Nil

Same as #inspect(io).

Source
token_digest

SHA-256 of the raw token, as raw bytes.

Bytes rather than a hex string: BYTEA is half the storage of a hex CHAR(64) and there is no encoding for two adapters to disagree about (docs/03-data-model.md).

Source