pub struct Session {Show 14 fields
pub session_id: String,
pub did: String,
pub challenge: String,
pub state: SessionState,
pub created_at: u64,
pub last_seen: u64,
pub refresh_token: Option<String>,
pub refresh_expires_at: Option<u64>,
pub tee_attested: bool,
pub amr: Vec<String>,
pub acr: String,
pub acr_expires_at: Option<u64>,
pub token_id: Option<String>,
pub session_pubkey_b58btc: Option<String>,
}Expand description
A session record stored in fjall under session:{session_id}.
Debug is hand-written below to redact the refresh_token. The raw
derive would render it inline — any tracing::debug!("{session:?}"),
panic backtrace, or dbg!() call holding a Session would otherwise
exfiltrate a bearer-equivalent secret to logs.
Fields§
§session_id: String§did: String§challenge: String§state: SessionState§created_at: u64§last_seen: u64Wall-clock epoch seconds of the most recent authenticated request on
this session. Intrinsic-sender (DIDComm/TSP) sessions carry no refresh
token, so this drives their idle-TTL expiry in
cleanup_expired_sessions. REST sessions set it too but are bounded
by refresh_expires_at. #[serde(default)] so rows written before this
field existed deserialise with 0; the sweeper falls back to
created_at in that case.
refresh_token: Option<String>§refresh_expires_at: Option<u64>§tee_attested: boolWhether the challenge issued for this session was accompanied
by a successful TEE attestation. Distinct from “this VTA was built
with the TEE feature”: a TEE binary running in TeeMode::Optional
can serve unattested challenges when the provider errors out, and
the resulting JWT must reflect that.
#[serde(default)] so older session records (written before this
field existed) deserialize as false — the conservative default.
amr: Vec<String>AAL claims persisted across token rotation. Mirrors the JWT’s
amr / acr so [/auth/refresh] mints a new access token at
the same authentication-method-references and assurance level
the session was last issued at. Without this, a session that
was step-upped to aal2 would be silently dropped back to
aal1 on every 15-minute refresh.
#[serde(default)] on both: a session row written before this
field landed deserialises with empty vectors / empty string,
which the refresh handler treats as “unknown AAL — fall back
to aal1”. Same behaviour as pre-migration; the holder can
re-step-up if needed.
acr: String§acr_expires_at: Option<u64>Epoch-seconds deadline after which a step-up elevation lapses. Set when
a step-up ceremony elevates the session; None for a session that was
never stepped up.
This — not acr — is what “a second factor was confirmed just now”
means. acr records the level the session reached and stays there for
its whole life (a passkey sign-in is aal2 from its first request, and
refresh preserves it), so it cannot express freshness on its own.
Read by both transports, in the shape each needs:
- Intrinsic-sender (DIDComm/TSP) —
resolve_did_sessionreadsacroff this row on every message, so it rewrites the row viaSession::downgrade_lapsed_elevationonce the window closes. - REST —
StepUpAuthreads the deadline directly viaSession::elevation_active, so a staleacron the row can never satisfy the gate and there is nothing to rewrite.acris left alone, which keeps a passkey login honestly reported asaal2rather than being downgraded below the level it logged in at.
#[serde(default)] for back-compat with pre-existing rows — which
deserialise as “never stepped up”, the fail-closed reading.
token_id: Option<String>JWT jti rotation pin. Set per-token-issue so old JWTs are
immediately invalidated when a new token is minted for the
same session — the AuthClaims extractor compares the JWT’s
jti against this field and rejects mismatches.
Optional because not every consumer uses per-token-issue
rotation; the canonical extractor checks this only when
Some(_). #[skip_serializing_if = "Option::is_none"]
keeps the field out of the serialised form when unused so
existing storage rows do not gain a token_id: null column.
session_pubkey_b58btc: Option<String>Ephemeral session pubkey for Data Integrity proof binding
(eddsa-jcs-2022). Ed25519 multikey, base58btc with the
z prefix (e.g. z6MkfBwQrx…). The corresponding
did:key:<this> is the verificationMethod the holder uses
when signing trust-task envelopes for this session.
None for clients that did not register a session pubkey;
REQUIRED-spec dispatch then rejects proofless envelopes per
the trust-task framework’s IS_PROOF_REQUIRED gate.
Implementations§
Source§impl Session
impl Session
Sourcepub fn elevation_active(&self, now: u64) -> bool
pub fn elevation_active(&self, now: u64) -> bool
Whether a step-up elevation is currently live on this session.
True only when acr_expires_at names a deadline
that has not yet passed. An absent deadline is not a live elevation —
it means this session was never stepped up (a passkey login, for
instance, is aal2 from its first request and carries no window). Gates
that need “a second factor was confirmed for this operation, just
now” must consult this rather than acr, which stays elevated for the
whole session and therefore cannot express freshness.
Fails closed: an unknown elevation time never reads as a recent one.
Sourcepub fn downgrade_lapsed_elevation(&mut self, now: u64) -> bool
pub fn downgrade_lapsed_elevation(&mut self, now: u64) -> bool
Drop a lapsed step-up elevation back to the un-elevated baseline, reporting whether anything changed.
Used by the intrinsic-sender resolver (resolve_did_session), where
acr is read straight off this row on every message and so must be
rewritten once the window closes. REST callers do not need this: their
gate (StepUpAuth) reads the
deadline itself, so a stale acr on the row can never satisfy it.
The baseline is the single did factor at aal1 — the level every
intrinsic-sender session starts at in resolve_did_session.