Fix session cookie max-age bug; add UI hardening roadmap

Cookie max-age was 900s (15 min, matching access token) but refresh
token lives 7 days — users got bounced to login after 15 min idle
because the Go SSR middleware rejected the expired cookie before JS
could refresh. Now cookie max-age = 604800s (7 days) on both the
client (auth.js) and server (auth.go OIDC callback).

Go page-auth middleware accepts expired-but-signed JWTs via new
parseJWTIgnoringExpiry() so the page shell renders and the Preact SDK
can refresh client-side. API middleware still validates expiry strictly.
6 new middleware tests cover strict/lenient/tampered/garbage cases.

VERSION bumped to 0.6.8 (rebrand was already shipped but file missed).
ROADMAP-UI.md added with 7 milestones (v0.6.9–v0.6.15).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 09:12:43 +00:00
parent 680ec3b897
commit a675d0440d
7 changed files with 295 additions and 8 deletions

View File

@@ -142,9 +142,30 @@ func parseAndValidateJWT(tokenString string, jwtSecret string) (*Claims, bool) {
return claims, true
}
// parseJWTIgnoringExpiry parses a JWT and validates the signature but
// tolerates an expired token. Used by page-auth middleware so the Go
// template can render the page shell even when the access token has
// lapsed — the Preact SDK will refresh the token client-side.
// Returns (claims, signatureValid). A tampered or unsigned token
// returns (nil, false).
func parseJWTIgnoringExpiry(tokenString string, jwtSecret string) (*Claims, bool) {
claims := &Claims{}
_, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(jwtSecret), nil
}, jwt.WithoutClaimsValidation())
if err != nil {
return nil, false
}
return claims, true
}
// UserIDFromCookie extracts the user ID from the arm_token cookie without
// requiring authentication. Returns "" if no valid token is found.
// Used by unauthenticated routes that want optional user context.
// Tolerates expired tokens (signature must be valid) so that user preferences
// still apply even when the access token has lapsed.
func UserIDFromCookie(c *gin.Context, jwtSecret string) string {
cookie, err := c.Cookie("arm_token")
if err != nil || cookie == "" {
@@ -152,7 +173,11 @@ func UserIDFromCookie(c *gin.Context, jwtSecret string) string {
}
claims, ok := parseAndValidateJWT(cookie, jwtSecret)
if !ok {
return ""
// Accept expired-but-signed token for optional user context
claims, ok = parseJWTIgnoringExpiry(cookie, jwtSecret)
if !ok {
return ""
}
}
return claims.UserID
}