Register hosts under Containers/VMs/Docker with encrypted key storage, and require re-authentication for sensitive account changes.
89 lines
2.5 KiB
Go
89 lines
2.5 KiB
Go
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})
|
|
}
|