Skip to content

Refresh rotation resets the absolute session lifetime, so there is no absolute cap #185

Description

@Bccorb

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2GovRAMP readiness trackenhancementNew feature or requestgovrampGovRAMP readiness track, see #155securitySecurity-sensitive: auth, tokens, OTP, sessions, crypto

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions