struct

KemalIdentity::Principal

Inherits Struct < Value < Object

Who is making this request, since when, and at what assurance.

The minimum security context and nothing more. It carries no roles, no permissions, no email, no profile — those are authorization or application concerns, they go stale the moment they are copied, and loading them would put a join on the hot path (docs/03-data-model.md). The application loads its own user object when it actually needs one.

#credential is not an exception to that rule. A role goes stale — somebody is removed from a team and the copy in a session says otherwise. "This request was proved by token tok_reporting" cannot go stale: it is a fact about this request, established at the same instant as assurance and authenticated_at, and it is false about no request it was attached to.

Constructors

new(subject : String, assurance : AssuranceLevel, authenticated_at : Time, credential : CredentialRef | Nil = nil, mfa_verified_at : Time | Nil = nil, tenant_id : String | Nil = nil, password_verified_at : Time | Nil = nil)
Source

Instance methods

assurance

How strongly this principal was authenticated.

Source
at_least?(level : AssuranceLevel) : Bool

Whether this principal reached at least level.

Source
authenticated_at

When the credential behind assurance was last verified.

Not a stored fresh_until: the freshness window belongs to the caller (require_fresh!(within: 5.minutes)), so storing a precomputed deadline would bake one caller's policy into every row.

Source
credential

The credential that proved this request, when one was presented.

This used to be session_id : String? and did the same job for exactly one credential kind, which is why #session_id below still answers. Generalising it is what lets an application tell one of its own personal access tokens from another — see blueprints/0021-credential-reference.md.

nil for a principal that no credential produced: the result of verifying a password at login, before Sessions::Service#start mints anything, and any principal an application constructs itself. nil reads as unattenuated, so it changes no decision on its own.

Source
fresh?(within : Time::Span, now : Time) : Bool

Whether the credential behind this principal was verified within within of now.

A principal below AssuranceLevel::Password is never fresh, however recent it is: a session restored from a remember-me cookie proves possession of a stored token, not the presence of the account holder. Step-up therefore always forces a real re-authentication out of Remembered.

Source
mfa_verified_at

When a second factor was last verified, if ever.

Source
password_verified?(within : Time::Span, now : Time) : Bool

Whether the password behind this principal was typed within within of now.

False when no password was ever typed for it, which is the whole point: "they were authenticated recently" and "they typed their password recently" are different questions, and only the second one may gate linking a new credential to the account.

Source
password_verified_at

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

authenticated_at cannot stand in for this. It is restamped by every assurance increase, so proving a second factor nine minutes after logging in satisfies a require_fresh! window that meant "type your password again". Nor can assurance: Password says one factor was proved, not which one, and a federated login sits at that level too.

nil is the answer for every principal no password produced — a remembered browser, a bearer token, a federated login, a session adopted from a legacy system — and for an account that has no password at all. #password_verified? therefore answers false for them rather than raising or guessing, which is the fail-closed direction: an application that never records a password gets a guard that always refuses, not one that always passes.

Source
session_id

The session this principal was resolved from, when there is one.

Derived from #credential rather than stored beside it: one idea, one home. Present so that "log out everywhere else" can spare the current session, and so that CSRF can anchor on the session that is actually signed in.

A bearer credential answers nil here even though it has an id of its own, which is the point — there is no session to spare or to anchor on.

Source
subject

The canonical account identifier, as a string.

A String rather than a generic parameter on purpose: RequestAuthenticator(T) would propagate T through every handler, service and repository in the type graph, and an application wanting a UUID in one place and an Int64 in another would have no way out. The cost is one subject.to_i64 at the application boundary — see docs/01-architecture.md.

Source
tenant_id

The tenant this principal is confined to, or nil for one that is not confined.

Read by Authz::RBAC#decide, which refuses a principal bound to one tenant asking about another before it consults membership at all — so this is an authorization input, not a label. Nil is unconstrained, which is the single-tenant deployment and also the person who belongs to several organisations.

For a session principal this is a copy of the account's tenant taken when the session was minted; Sessions::Record#tenant_id says what that means for a change made afterwards.

Source