class

KemalIdentity::Sessions::Service

Inherits Reference < Object

Creating, resolving, rotating and revoking sessions.

This is where the session lifecycle rules live. The repository reports facts; this decides what they mean.

Constructors

new(sessions : Repository, clock : Clock, random : RandomSource, config : Config = Config.new)
Source

Instance methods

delete_expired

Deletes rows past their absolute deadline. Disk reclamation only — correctness never depends on this having run.

Source
resolve(raw : String | Nil) : Outcome

Resolves a raw cookie value.

Returns Anonymous when there is no credential to check, and Failed when there was one and it did not hold — the caller needs that distinction to know whether to clear the cookie (docs/02-security-model.md).

The order of the checks below is the order in docs/02-security-model.md and is not arbitrary: shape before any I/O, then revocation, then expiry, then account status. Expiry is evaluated here, on every read, never deferred to the sweeper. That is the direct lesson of kemal-session issue #116, where a timeout only marked a session for deletion at the next GC pass, and a read could refresh its access time before any expiry check — reviving it.

Source
revoke(session_id : String) : Bool

Ends one session. Returns false if it did not exist or was already revoked.

Source
revoke_after_credential_change(account_id : String, current_session_id : String | Nil = nil) : Int32

Ends the sessions that a password change or MFA recovery must invalidate.

Every other session dies unconditionally — evicting whoever knew the old credential is the entire point. Whether the session performing the change dies too is Config#revoke_current_on_credential_change, and defaults to false so that changing your own password does not log you out of the tab you changed it in (docs/02-security-model.md).

This is the revocation half. The caller pairs it with Accounts::Repository#bump_auth_version, which invalidates sessions without enumerating rows — belt as well as braces, since a session created concurrently with this call would otherwise survive it.

Source
revoke_all(account_id : String, except_id : String | Nil = nil) : Int32

Ends every session for an account, returning how many it ended.

Source
rotate(record : Record, account : Accounts::Account, assurance : AssuranceLevel | Nil = nil, mfa_verified_at : Time | Nil = nil, password_verified_at : Time | Nil = nil) : Issued

Issues a new secret and a new row, revoking the old one.

Called on successful login — this is the session fixation defence, and it is why the identifier a client held before authenticating is worthless afterwards — and on an assurance increase or a credential change.

Both windows restart. Re-authentication legitimately begins a new session lifetime, which is a different thing from activity: activity moves idle_expires_at only, and can never postpone the absolute deadline.

Source
start(account : Accounts::Account, assurance : AssuranceLevel, mfa_verified_at : Time | Nil = nil, password_verified_at : Time | Nil = nil) : Issued

Starts a new session for an account.

Raises ArgumentError for a disabled account. That is a caller bug rather than an authentication failure: whoever calls this has already verified a credential, and the disabled check belongs in front of that. Failing loudly here stops a caller from accidentally minting a session the very next request would reject.

Source