Part of #155. Session 7. Found while investigating #157.
Problem
There is no absolute cap on how long a session can live. Refresh rotation does
not extend the existing session, it creates a new one with fresh times
(src/controllers/authentication.ts:364):
const { expiresAt, idleExpiresAt } = computeSessionTimes(now);
...
const newSession = await Session.create({
...
expiresAt,
idleExpiresAt,
});
session.replacedBySessionId = newSession.id;
The old row is chained through replacedBySessionId, but nothing carries the
original session start forward. Each rotation resets the clock, so a client that
refreshes before expiry can extend a session indefinitely. The "absolute
lifetime" only ever bounds a session that stops refreshing, which is what the
idle bound is for.
Why it matters
NIST 800-53 AC-12. An absolute cap exists so that a session ends and
re-authentication happens on a fixed schedule regardless of activity, which is
what limits the useful lifetime of a stolen refresh token. A stolen token that
is rotated on schedule currently never ages out on its own.
Refresh token reuse detection still applies and will revoke the chain if the
stolen token is replayed after rotation, so this is a defence-in-depth gap
rather than an open door. It is also exactly the kind of thing a GovRAMP
reviewer asks about directly.
What to do
- Carry the origin of the rotation chain forward, for example a
chainStartedAt
copied from the previous session on every rotation
- Refuse to rotate once
chainStartedAt + absolute lifetime has passed, and
force a fresh authentication
- Keep the idle bound behaving as it does now, sliding on each refresh
- Emit an auth event when a rotation is refused for this reason, so the forced
re-authentication is explainable
Acceptance
- A session that refreshes continuously still terminates at the absolute bound
- The idle bound continues to slide on refresh
- The refusal is distinguishable in the audit trail from an expired or revoked
session
Depends on
#157, which makes the two bounds configurable and distinct. This issue is only
meaningful once they are.
Freeze status
Anticipated GovRAMP finding (exception 2). Sequenced with the rest of Session 7,
after the Snapshot.
Part of #155. Session 7. Found while investigating #157.
Problem
There is no absolute cap on how long a session can live. Refresh rotation does
not extend the existing session, it creates a new one with fresh times
(src/controllers/authentication.ts:364):
The old row is chained through
replacedBySessionId, but nothing carries theoriginal session start forward. Each rotation resets the clock, so a client that
refreshes before expiry can extend a session indefinitely. The "absolute
lifetime" only ever bounds a session that stops refreshing, which is what the
idle bound is for.
Why it matters
NIST 800-53 AC-12. An absolute cap exists so that a session ends and
re-authentication happens on a fixed schedule regardless of activity, which is
what limits the useful lifetime of a stolen refresh token. A stolen token that
is rotated on schedule currently never ages out on its own.
Refresh token reuse detection still applies and will revoke the chain if the
stolen token is replayed after rotation, so this is a defence-in-depth gap
rather than an open door. It is also exactly the kind of thing a GovRAMP
reviewer asks about directly.
What to do
chainStartedAtcopied from the previous session on every rotation
chainStartedAt + absolute lifetimehas passed, andforce a fresh authentication
re-authentication is explainable
Acceptance
session
Depends on
#157, which makes the two bounds configurable and distinct. This issue is only
meaningful once they are.
Freeze status
Anticipated GovRAMP finding (exception 2). Sequenced with the rest of Session 7,
after the Snapshot.