fix: IDEAS.md quickfixes — idle-timeout app config, id_token out of Claims, FormEvent import

session.idle_timeout (minutes, default 15) joins the app-config registry
and drives the onboarding Garmin session eviction, distinct from
session.duration (the login cookie lifetime in hours). The raw Keycloak
ID token no longer rides in auth.Claims through every request context:
it's minted into the session cookie separately and read back only by the
logout handler via IDTokenFromSessionCookie. OnboardingWizard uses the
type-imported FormEvent<HTMLFormElement> instead of the React.FormEvent
namespace alias.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 17:44:37 +02:00
parent dcbb1d8bb0
commit 8c9285f33c
15 changed files with 160 additions and 61 deletions

View File

@@ -5,8 +5,10 @@
// tokens are never stored or refreshed -- once HandleCallback verifies the
// ID token and role, only this package's own cookie matters for subsequent
// requests. The one exception is the raw ID token itself, carried opaquely
// inside the signed session cookie (Claims.IDToken) solely so a later
// logout can pass it back to Keycloak as id_token_hint -- letting Keycloak
// inside the signed session cookie (apart from Claims -- see
// MintSessionCookie's idToken parameter and IDTokenFromSessionCookie)
// solely so a later logout can pass it back to Keycloak as
// id_token_hint -- letting Keycloak
// skip its own logout-confirmation prompt for a session it can positively
// identify, rather than leaving the user a chance to cancel out of it after
// their geniusrun account (and profile) is already gone.
@@ -32,14 +34,15 @@ const (
)
// Claims identifies the authenticated user, carried in the signed session
// cookie. IDToken is the raw Keycloak ID token JWT from the OIDC callback,
// carried opaquely so a later logout can pass it back to Keycloak as
// id_token_hint (see EndSessionURL) -- geniusrun never inspects it itself.
// cookie and stashed in every request's context. Deliberately does NOT
// carry the raw Keycloak ID token: that JWT lives in the cookie payload
// separately (see MintSessionCookie/IDTokenFromSessionCookie) because it's
// only ever needed once more, at logout, and has no business riding
// through every handler's context.
type Claims struct {
Sub string
Name string
Email string
IDToken string
Sub string
Name string
Email string
}
type sessionClaims struct {
@@ -65,13 +68,15 @@ type txnClaims struct {
// MintSessionCookie signs claims into a JWT valid for duration and wraps it
// in a cookie. secure should be true whenever the app is served over HTTPS.
func MintSessionCookie(claims Claims, secret []byte, duration time.Duration, secure bool) (*http.Cookie, error) {
// idToken is the raw Keycloak ID token to carry for the eventual logout's
// id_token_hint (empty is fine, e.g. in tests -- the hint is optional).
func MintSessionCookie(claims Claims, idToken string, secret []byte, duration time.Duration, secure bool) (*http.Cookie, error) {
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims{
Sub: claims.Sub,
Name: claims.Name,
Email: claims.Email,
IDToken: claims.IDToken,
IDToken: idToken,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(duration)),
@@ -102,7 +107,21 @@ func ParseSessionCookie(cookie *http.Cookie, secret []byte) (Claims, error) {
if _, err := jwt.ParseWithClaims(cookie.Value, &sc, keyfunc(secret), jwt.WithValidMethods([]string{"HS256"})); err != nil {
return Claims{}, fmt.Errorf("parse session token: %w", err)
}
return Claims{Sub: sc.Sub, Name: sc.Name, Email: sc.Email, IDToken: sc.IDToken}, nil
return Claims{Sub: sc.Sub, Name: sc.Name, Email: sc.Email}, nil
}
// IDTokenFromSessionCookie verifies the cookie and returns the raw Keycloak
// ID token it carries, for logout's id_token_hint. Only the logout handler
// needs this -- everything else uses ParseSessionCookie's Claims.
func IDTokenFromSessionCookie(cookie *http.Cookie, secret []byte) (string, error) {
if cookie == nil {
return "", fmt.Errorf("no session cookie")
}
var sc sessionClaims
if _, err := jwt.ParseWithClaims(cookie.Value, &sc, keyfunc(secret), jwt.WithValidMethods([]string{"HS256"})); err != nil {
return "", fmt.Errorf("parse session token: %w", err)
}
return sc.IDToken, nil
}
// MintTxnCookie signs an OIDC login transaction into a short-lived cookie.