Fourth commit in the session-expiration-policy series. The gate that
ends "logged in forever" — refresh now rejects tokens whose original
login (auth_time) is older than abs_max seconds.
Algorithm (plan §4.5):
1. Decode JWT (dep already handles idle expiry).
2. Load user; reject inactive/missing as invalid_refresh_token.
3. Resolve effective auth_time/idle_max/abs_max, grandfathering
pre-PR tokens by snapshotting current account policy.
4. Atomically revoke the JTI regardless of outcome — this consumes
the token whether or not the absolute check passes, so an
absolute-expired token cannot be replayed forever.
5. If the atomic UPDATE matched zero rows -> invalid_refresh_token.
6. If now >= auth_time + abs_max -> commit the revoke explicitly
(so it survives the rollback hook in get_admin_db) and 401
session_expired_absolute.
7. Otherwise mint via _mint_with_claims, carrying claims forward.
Boundary check uses `>=`, not `>` — a deadline equal to now is
expired. _refresh_session_tokens (commit 3) replaced by two narrower
helpers: _resolve_refresh_claims (grandfather logic, no mint) and
_mint_with_claims (mint with explicit claims, no grandfather). Makes
the endpoint's algorithm read top-down without indirection.
Tests added in test_session_policy.py:
- #8: backdate auth_time by exactly abs_max -> session_expired_absolute
at the deadline boundary.
- #9: same token tried twice; first returns session_expired_absolute
AND consumes the row; second returns invalid_refresh_token.
- #12: legacy token without auth_time/idle_max/abs_max gets one
successful rotation; new JWT carries fresh policy snapshot from
the account (3d/14d defaults under Strict).
25/25 across test_session_policy + test_auth + test_oauth_callbacks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>