Files
foundry-core/internal/auth/session.go
codex-bot 34c461a61c
Foundry Go checks / check (push) Successful in 2m53s
Foundry Go checks / check (pull_request) Successful in 3m9s
feat(auth): renew sessions with 30-day idle and 90-day absolute limits
2026-11-06 12:32:04 +08:00

51 lines
1.6 KiB
Go

package auth
import (
"context"
"database/sql"
"errors"
"net/http"
"strconv"
"strings"
)
const SessionIdleSeconds = 30 * 24 * 60 * 60
const SessionExpiryHeader = "X-Foundry-Session-Expires-In"
var ErrInvalidSession = errors.New("invalid session")
type Session struct {
AccountID string
ExpiresIn int64
}
// AuthenticateSession atomically validates and renews a live session, bounded by
// its absolute deadline. Expired, revoked and wrong-project tokens never renew.
func AuthenticateSession(ctx context.Context, db *sql.DB, project, authorization string) (Session, error) {
value, ok := strings.CutPrefix(authorization, "Bearer ")
if !ok || !tokenPattern.MatchString(value) {
return Session{}, ErrInvalidSession
}
var session Session
err := db.QueryRowContext(ctx, `
UPDATE project_sessions
SET expires_at=LEAST(now()+interval '30 days',absolute_expires_at)
WHERE project=$1 AND token_hash=$2 AND expires_at>now() AND absolute_expires_at>now()
RETURNING account_id::text,GREATEST(0,FLOOR(EXTRACT(EPOCH FROM (expires_at-now()))))::bigint
`, project, digest(value)).Scan(&session.AccountID, &session.ExpiresIn)
if errors.Is(err, sql.ErrNoRows) {
return Session{}, ErrInvalidSession
}
return session, err
}
func WriteSessionHeaders(w http.ResponseWriter, session Session) {
w.Header().Set(SessionExpiryHeader, strconv.FormatInt(session.ExpiresIn, 10))
}
// Authenticate preserves the existing API for clients that only need an account ID.
func Authenticate(ctx context.Context, db *sql.DB, project, authorization string) (string, error) {
session, err := AuthenticateSession(ctx, db, project, authorization)
return session.AccountID, err
}