Skip to content

feat: extend token expiration on each client-server interaction (+24h with 1h debounce) #84

@bnema

Description

@bnema

Context

CLI tokens are issued once with a fixed TTL (24h for password-auth sessions via POST /auth/password, up to 30 days or never-expiring for long-lived tokens) and never renewed automatically. There is an explicit comment in the code:

"Service tokens are not auto-refreshed. If the token expires during container runtime, the container will need to be recreated to get a new token."
internal/app/run.go:1159

A user relying on password-based login sessions will have their session expire after 24 hours of inactivity, requiring a manual gordon auth login again.

Urgency

  • Password sessions (24h TTL): actively painful for daily users — the session expires overnight.
  • Long-lived tokens (configurable, default 30d, can be never-expiring): less urgent; operators who use gordon auth token can generate tokens that don't expire.
  • Service tokens (30d TTL): after 30 days, internal image pulls silently fail until Gordon restarts.

Desired Behavior

Every successful authenticated request from the CLI to the admin API should slide the token's expiration forward by 24 hours, subject to a 1-hour debounce (i.e., if the token was already extended in the last hour, skip the extension to avoid hammering the token store on every command).

Current Architecture

  • Token creation: internal/usecase/auth/service.go:268GenerateToken() sets exp at creation time and persists the signed JWT to the token store.
  • Token validation: internal/usecase/auth/service.go:94ValidateToken() checks exp, JTI presence in store, and revocation. It does not slide the expiry.
  • Admin API middleware: internal/adapters/in/http/admin/middleware.go:79 — calls ValidateToken() on every request; the natural hook point for triggering an extension.
  • Token store: internal/adapters/out/tokenstore/ — tokens are stored as JSON files containing the full signed JWT string.
  • CLI request path: internal/adapters/in/cli/remote/client.go:139 — attaches the stored JWT to every request.

Implementation Constraints

Re-signing is mandatory. Expiry is embedded in the signed JWT claims and validated during JWT parse (internal/usecase/auth/service.go:157) as well as in an explicit check (internal/usecase/auth/service.go:229). Mutating only the store entry without re-signing would not extend the effective expiry — a new JWT must be issued.

JTI collision risk. When a token is re-issued, if a new JTI is assigned, the old token is immediately invalid. Any concurrent in-flight request using the old token will get a token not found error from the JTI store check (internal/usecase/auth/service.go:146). Implementation options:

  • Reuse the existing JTI (requires the store to support updating the JWT string for an existing JTI).
  • Issue a new JTI and accept a narrow race window (acceptable if the debounce is 1h).
  • Return the new token in a response header so the CLI can atomically swap it.

Implementation Notes

  1. Add a ExtendToken(ctx, tokenString) (newTokenString string, err error) method to auth.Service.
  2. Return the new token in an HTTP response header (e.g., X-Gordon-Token) so the CLI can persist it without an extra round-trip.
  3. Add a last_extended_at field to the stored token record to implement the 1h debounce.
  4. Exclude ephemeral access tokens (≤5 min, isEphemeralAccessToken at internal/usecase/auth/service.go:242) from this sliding logic — they are not stored and have their own short lifecycle.
  5. Exclude service tokens (sub: gordon-service) from this logic — they are managed by Gordon's boot sequence, not by CLI interactions.

Affected Files

  • internal/usecase/auth/service.go — add extension method
  • internal/adapters/out/tokenstore/ — store must support updating token records
  • internal/adapters/in/http/admin/middleware.go — trigger extension post-validation
  • internal/adapters/in/cli/remote/client.go — read response header and persist new token

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions