package api import ( "context" "errors" "net/http" "time" "codeberg.org/SquidSE/ClusterCanvas/service/internal/auth" "codeberg.org/SquidSE/ClusterCanvas/service/internal/settings" ) const reauthRequiredMessage = "reauth_required" type reauthRequest struct { Password string `json:"password"` TOTPCode string `json:"totp_code"` } func SessionFromContext(ctx context.Context) (settings.SessionRecord, bool) { session, ok := ctx.Value(contextSessionKey).(settings.SessionRecord) return session, ok } func verifyActorCredentials(user settings.UserCredential, password string, totpCode string, security settings.SecuritySettings) error { ok, err := auth.VerifyPassword(password, user.PasswordHash) if err != nil || !ok { return errors.New("invalid credentials") } requiresTOTP := security.TotpEnabled && user.TOTPConfirmed && user.TOTPSecret != "" if requiresTOTP { if !auth.VerifyTOTPCode(user.TOTPSecret, totpCode) { return errors.New("invalid or missing TOTP code") } } return nil } func (app *App) requireInlineReauth( request *http.Request, password string, totpCode string, ) error { user, ok := UserFromContext(request.Context()) if !ok { return errors.New("authentication required") } settingsPayload, err := loadSettingsOrDefault(app.ConfigDir) if err != nil { return err } security := effectiveSecurity(settingsPayload.Security) return verifyActorCredentials(user, password, totpCode, security) } // requireGraceReauth returns an error with message reauth_required when the // session is outside the grace window. When reauth_sensitive_actions is off, it // is a no-op. func (app *App) requireGraceReauth(request *http.Request) error { settingsPayload, err := loadSettingsOrDefault(app.ConfigDir) if err != nil { return err } security := effectiveSecurity(settingsPayload.Security) if !security.ReauthSensitiveActions { return nil } session, ok := SessionFromContext(request.Context()) if !ok { return errors.New("authentication required") } grace := time.Duration(security.ReauthGraceMinutes) * time.Minute elapsed := time.Since(session.LastReauthAt) // Allow a short post-reauth window so grace=0 still supports reauth-then-retry. const justReauthedWindow = 30 * time.Second if elapsed <= grace || elapsed <= justReauthedWindow { return nil } return errors.New(reauthRequiredMessage) } func writeReauthRequired(writer http.ResponseWriter) { writeJSON(writer, http.StatusUnauthorized, apiErrorResponse{Error: reauthRequiredMessage}) }