Skip to content

feat: add session max lifetime and fix refresh logic#559

Merged
steveiliop56 merged 6 commits into
tinyauthapp:mainfrom
pushpinderbal:main
Jan 7, 2026
Merged

feat: add session max lifetime and fix refresh logic#559
steveiliop56 merged 6 commits into
tinyauthapp:mainfrom
pushpinderbal:main

Conversation

@pushpinderbal
Copy link
Copy Markdown
Contributor

@pushpinderbal pushpinderbal commented Jan 3, 2026

Session Refresh Logic Improvements

=> Fixed refresh threshold calculation

  • When SessionExpiry < 1 hour: uses SessionExpiry / 2 as the refresh threshold
  • When SessionExpiry >= 1 hour: uses 1 hour as the refresh threshold

This prevents the refresh threshold from exceeding the session expiry duration

=> Fixed refresh timing to prevent excessive database writes

Changed from currentTime + refreshThreshold to session.Expiry + refreshThreshold.
Using currentTime + refreshThreshold caused continuous refreshes once the threshold was reached (e.g., with 1hr threshold: session drops to 59min → refresh adds 1hr → next request at 59min triggers another refresh). Adding to the existing expiry (session.Expiry + refreshThreshold) ensures refresh only happens once when crossing the threshold, while still extending sessions incrementally for active users.

=> Added hard session lifetime limit

  • New config option: SessionMaxLifetime (defaults to 0 for unlimited)
  • Prevents indefinitely extending sessions through refresh
  • Forces re-authentication after the absolute maximum lifetime is reached, regardless of activity
  • Added created_at field to sessions table to track session age

Summary by CodeRabbit

  • New Features

    • Added configurable maximum session lifetime to enforce absolute session expiration.
  • Configuration Changes

    • Introduced a new sessionMaxLifetime setting (env/config examples updated).
  • Improvements

    • Sessions now record creation timestamps for accurate lifetime enforcement.
    • Session refresh logic and cookie expiry handling improved.
    • Database initialization tuned and migrations added for the schema change.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 3, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Session maximum lifetime tracking and enforcement are added: sessions now record created_at, a new SessionMaxLifetime config controls optional max lifetime, refresh logic uses a dynamic threshold, DB schema/migrations add created_at, and SQLite connections are limited to one during bootstrap.

Changes

Cohort / File(s) Summary
Configuration & Environment
\.env.example`, `config.example.yaml`, `cmd/tinyauth/tinyauth.go``
Add TINYAUTH_AUTH_SESSIONMAXLIFETIME env var and auth.sessionMaxLifetime config option; populate SessionMaxLifetime in app init.
Config Struct
\internal/config/config.go``
Replace SessionExpiry field usage with SessionMaxLifetime on AuthConfig (field added; prior placement adjusted).
Bootstrap & Initialization
\internal/bootstrap/app_bootstrap.go`, `internal/bootstrap/service_bootstrap.go`, `internal/bootstrap/db_bootstrap.go``
Validate SessionMaxLifetime relative to SessionExpiry (non-zero case). Add SessionMaxLifetime to AuthService config; set SQLite db.SetMaxOpenConns(1).
Database Schema & Migrations
\internal/assets/migrations/000004_created_at.up.sql`, `internal/assets/migrations/000004_created_at.down.sql`, `schema.sql`, `query.sql``
Add created_at INTEGER NOT NULL column to sessions and update INSERT/SELECT/UPDATE SQL to include created_at.
Repository Layer
\internal/repository/models.go`, `internal/repository/query.sql.go``
Add CreatedAt int64 to Session and CreateSessionParams; propagate created_at through queries and scans.
Service Layer
\internal/service/auth_service.go``
Add SessionMaxLifetime to AuthServiceConfig; record CreatedAt on session creation; enforce max-lifetime in GetSessionCookie (delete session if exceeded); compute dynamic refresh threshold for expiry refresh and log refresh events.
Tests
\internal/controller/proxy_controller_test.go`, `internal/controller/user_controller_test.go``
Add SessionMaxLifetime: 0 to AuthServiceConfig in test setups to match new config shape.

Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthService
    participant Repository
    participant Database

    rect rgba(200,220,255,0.18)
    note over AuthService,Repository: Session Creation flow (new)
    Client->>AuthService: CreateSession(credentials)
    AuthService->>Repository: CreateSession(params + CreatedAt)
    Repository->>Database: INSERT INTO sessions(..., created_at, ...)
    Database-->>Repository: INSERT result (including created_at)
    Repository-->>AuthService: Session{CreatedAt, Expiry, ...}
    AuthService-->>Client: Set-Cookie (expiry)
    end

    rect rgba(220,255,200,0.18)
    note over Client,AuthService: Refresh with dynamic threshold
    Client->>AuthService: RefreshSession(cookie)
    AuthService->>Repository: GetSession(sessionID)
    Repository->>Database: SELECT ..., created_at FROM sessions WHERE id=...
    Database-->>Repository: Session{CreatedAt, Expiry, ...}
    Repository-->>AuthService: Session{CreatedAt, Expiry, ...}
    AuthService->>AuthService: calculate refreshThreshold = f(SessionExpiry)
    AuthService->>AuthService: newExpiry = currentExpiry + refreshThreshold
    AuthService-->>Client: Updated Set-Cookie (newExpiry)
    end

    rect rgba(255,230,200,0.18)
    note over Client,AuthService: Max-lifetime enforcement on access
    Client->>AuthService: GetSessionCookie(cookie)
    AuthService->>Repository: GetSession(sessionID)
    Repository->>Database: SELECT created_at, expiry, ... 
    Database-->>Repository: Session{CreatedAt, ...}
    Repository-->>AuthService: Session{CreatedAt, ...}
    AuthService->>AuthService: if SessionMaxLifetime != 0 && CreatedAt + SessionMaxLifetime < now
    alt max lifetime exceeded
        AuthService->>Repository: DeleteSession(sessionID)
        Repository->>Database: DELETE FROM sessions WHERE id=...
        AuthService-->>Client: Error (session expired)
    else valid
        AuthService-->>Client: SessionCookie (valid)
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I tuck a timestamp in each session's den,
A ticking carrot for the life of when,
Refreshes dance by a clever rule,
And max-lifetimes keep the burrow cool,
Hooray — secure naps for auths again! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: adding session max lifetime functionality and fixing the refresh logic for session management.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c83ce50 and 6f27b2b.

📒 Files selected for processing (1)
  • .env.example

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 3, 2026

Codecov Report

❌ Patch coverage is 0% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.25%. Comparing base (721f302) to head (6f27b2b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/service/auth_service.go 0.00% 16 Missing ⚠️
internal/bootstrap/service_bootstrap.go 0.00% 9 Missing ⚠️
cmd/tinyauth/tinyauth.go 0.00% 4 Missing ⚠️
internal/repository/query.sql.go 0.00% 4 Missing ⚠️
internal/bootstrap/app_bootstrap.go 0.00% 2 Missing ⚠️
internal/bootstrap/db_bootstrap.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #559      +/-   ##
==========================================
- Coverage   19.44%   19.25%   -0.19%     
==========================================
  Files          39       39              
  Lines        2273     2295      +22     
==========================================
  Hits          442      442              
- Misses       1803     1825      +22     
  Partials       28       28              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/service/auth_service.go (1)

207-219: LGTM!

The CreatedAt timestamp is correctly captured at session creation time using Unix epoch seconds, consistent with the Expiry field format.

Optional nit: Consider capturing time.Now() once to ensure Expiry and CreatedAt use the exact same instant:

🔎 Suggested improvement
+	now := time.Now()
+
 	session := repository.CreateSessionParams{
 		UUID:        uuid.String(),
 		Username:    data.Username,
 		Email:       data.Email,
 		Name:        data.Name,
 		Provider:    data.Provider,
 		TotpPending: data.TotpPending,
 		OAuthGroups: data.OAuthGroups,
-		Expiry:      time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
-		CreatedAt:   time.Now().Unix(),
+		Expiry:      now.Add(time.Duration(expiry) * time.Second).Unix(),
+		CreatedAt:   now.Unix(),
 		OAuthName:   data.OAuthName,
 		OAuthSub:    data.OAuthSub,
 	}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1e2b55 and c83ce50.

📒 Files selected for processing (16)
  • .env.example
  • cmd/tinyauth/tinyauth.go
  • config.example.yaml
  • internal/assets/migrations/000004_created_at.down.sql
  • internal/assets/migrations/000004_created_at.up.sql
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/db_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/config/config.go
  • internal/controller/proxy_controller_test.go
  • internal/controller/user_controller_test.go
  • internal/repository/models.go
  • internal/repository/query.sql.go
  • internal/service/auth_service.go
  • query.sql
  • schema.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-27T11:02:49.689Z
Learnt from: steveiliop56
Repo: steveiliop56/tinyauth PR: 326
File: internal/service/database_service.go:7-11
Timestamp: 2025-08-27T11:02:49.689Z
Learning: In internal/service/database_service.go, using glebarez/sqlite (modernc) for GORM with sqlite3 (mattn) driver for golang-migrate is a valid pattern that works correctly, despite appearing to be a driver mismatch. The golang-migrate sqlite driver causes panics when used with modernc.org/sqlite backend, so the mixed approach should be maintained.

Applied to files:

  • internal/bootstrap/db_bootstrap.go
🧬 Code graph analysis (3)
internal/bootstrap/service_bootstrap.go (1)
internal/config/config.go (1)
  • SessionCookieName (11-11)
internal/controller/user_controller_test.go (1)
internal/config/config.go (1)
  • SessionCookieName (11-11)
internal/controller/proxy_controller_test.go (1)
internal/config/config.go (1)
  • SessionCookieName (11-11)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 40-40: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 40-40: [UnorderedKey] The TINYAUTH_AUTH_SESSIONEXPIRY key should go before the TINYAUTH_AUTH_USERS key

(UnorderedKey)


[warning] 42-42: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 42-42: [UnorderedKey] The TINYAUTH_AUTH_SESSIONMAXLIFETIME key should go before the TINYAUTH_AUTH_USERS key

(UnorderedKey)

🔇 Additional comments (20)
config.example.yaml (1)

41-42: LGTM! Clear configuration addition.

The new sessionMaxLifetime parameter is well-documented with a sensible default of 0 (unlimited), ensuring backward compatibility while enabling hard session lifetime limits when configured.

.env.example (1)

41-42: Good addition of the session max lifetime configuration.

The new TINYAUTH_AUTH_SESSIONMAXLIFETIME environment variable aligns well with the auth configuration namespace.

Note: The static analysis warnings about quote characters and key ordering are false positives—quotes are standard practice in .env files, and alphabetical ordering is optional.

internal/assets/migrations/000004_created_at.down.sql (1)

1-1: LGTM! Rollback migration is correct.

The down migration properly reverses the addition of the created_at column.

query.sql (2)

27-39: Correctly preserves created_at as immutable.

The UpdateSession query appropriately excludes created_at from the SET clause, ensuring the session creation timestamp remains unchanged throughout the session's lifetime. This is the correct behavior for tracking session age.


1-17: Code correctly sets created_at to current timestamp.

The application properly sets CreatedAt to time.Now().Unix() in the CreateSessionCookie method within internal/service/auth_service.go when populating the CreateSessionParams before calling the repository's CreateSession function. The implementation matches the SQL parameter addition.

internal/bootstrap/app_bootstrap.go (1)

45-48: LGTM! Session configuration validation is correctly implemented.

The validation ensures that when SessionMaxLifetime is configured (non-zero), it must be at least as long as SessionExpiry. This prevents an invalid configuration where sessions would be forced to re-authenticate before they could be refreshed.

internal/repository/models.go (1)

16-16: LGTM! Auto-generated field correctly reflects the schema change.

The CreatedAt field is properly generated by sqlc from the schema definition, with the correct int64 type for Unix timestamp storage.

internal/config/config.go (1)

47-47: LGTM! SessionMaxLifetime field correctly implements hard session lifetime limit.

The new SessionMaxLifetime configuration field enables enforcement of an absolute maximum session duration, preventing indefinite session extension through activity. This is a common security best practice.

The field works alongside SessionExpiry (line 46):

  • SessionExpiry: Controls refresh interval for active sessions
  • SessionMaxLifetime: Enforces hard limit requiring re-authentication
schema.sql (1)

10-10: The migration properly handles the NOT NULL constraint with DEFAULT 0 for existing rows. No issues found.

cmd/tinyauth/tinyauth.go (1)

28-31: LGTM! SessionMaxLifetime field added with sensible default.

The new SessionMaxLifetime: 0 field is correctly initialized with a default value of 0 (unlimited), which maintains backward compatibility. This aligns with the PR objectives to add an optional hard session lifetime limit.

internal/controller/proxy_controller_test.go (1)

60-67: LGTM! Test configuration updated correctly.

The test setup properly includes the new SessionMaxLifetime field with the default value of 0, maintaining consistency with the updated AuthServiceConfig structure.

internal/controller/user_controller_test.go (1)

63-70: LGTM! Test configuration updated consistently.

The SessionMaxLifetime field is correctly added to the test setup, matching the service configuration structure changes across the codebase.

internal/bootstrap/service_bootstrap.go (1)

61-69: LGTM! Configuration properly wired to service layer.

The SessionMaxLifetime field is correctly propagated from the application config to the AuthServiceConfig, ensuring the new session lifetime limit is available to the authentication service.

internal/repository/query.sql.go (3)

12-74: LGTM! Generated CreateSession query correctly handles CreatedAt.

The CreatedAt field is properly integrated into the session creation flow:

  • Added to the INSERT column list (line 22)
  • Included in VALUES placeholders (line 26)
  • Returned via RETURNING clause (line 28)
  • Added to CreateSessionParams struct (line 40)
  • Correctly passed and scanned (lines 55, 69)

96-118: LGTM! GetSession query correctly retrieves CreatedAt.

The CreatedAt field is properly included in the SELECT query and correctly scanned into the Session struct.


120-176: LGTM! UpdateSession correctly preserves CreatedAt immutability.

The implementation correctly handles CreatedAt:

  • NOT included in the UPDATE SET clause (lines 121-130), ensuring immutability
  • Included in RETURNING clause (line 132) to return the existing value
  • Properly scanned (line 171)

This ensures that created_at remains unchanged after session creation, which is the correct behavior for tracking session age.

internal/service/auth_service.go (4)

28-38: LGTM!

The SessionMaxLifetime field addition is clean and follows the existing pattern for time-based configuration (using int for seconds, consistent with SessionExpiry).


247-259: LGTM!

The dynamic refresh threshold calculation is well-designed:

  • For short sessions (≤1 hour): threshold = half the session expiry (prevents threshold from exceeding session duration)
  • For longer sessions: capped at 1 hour

The incremental extension via session.Expiry + refreshThreshold (line 259) combined with the max lifetime enforcement in GetSessionCookie provides the intended behavior: active users get extended sessions, but SessionMaxLifetime enforces the hard cap.


278-279: LGTM!

The cookie's MaxAge is correctly set to the actual remaining time until the new expiry, ensuring browser cookie expiration aligns with server-side session state. The trace log is helpful for debugging.


320-328: LGTM!

The max lifetime enforcement is well-implemented:

  • Gracefully handles disabled feature (SessionMaxLifetime == 0) and legacy sessions (CreatedAt == 0)
  • Correctly positioned before the regular expiry check
  • Clear error message distinguishes max lifetime expiration from normal expiry
  • Delete failure is logged but doesn't block the rejection, ensuring security even if cleanup fails

Comment thread .env.example
Comment thread internal/assets/migrations/000004_created_at.up.sql
Comment thread internal/bootstrap/db_bootstrap.go
@steveiliop56 steveiliop56 merged commit e7bd64d into tinyauthapp:main Jan 7, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants