auth: Registrierung, Login, Logout, per-user Sessions (Backend)

This commit is contained in:
OpenClaw Dev 2026-10-31 11:28:17 +01:00
parent c7b4acea02
commit 0ef39cffd6
12 changed files with 703 additions and 40 deletions

View file

@ -3,8 +3,7 @@ package server
import (
"context"
"crypto/subtle"
"encoding/json"
"encoding/json"
"errors"
"log/slog"
"net/http"
@ -35,6 +34,13 @@ type Server struct {
Info Info
// Providers enables runtime provider switching (optional).
Providers *providers.Manager
// AllowRegistration enables POST /api/auth/register.
AllowRegistration bool
// TokenTTL is the lifetime of user tokens (default 30 days).
TokenTTL time.Duration
authOnce sync.Once
limiter *limiter
}
type Info struct {
@ -47,7 +53,12 @@ type Info struct {
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, map[string]string{"status": "ok"}) })
s.initAuth()
mux.HandleFunc("POST /api/auth/register", s.register)
mux.HandleFunc("POST /api/auth/login", s.login)
api := http.NewServeMux()
api.HandleFunc("POST /api/auth/logout", s.logout)
api.HandleFunc("GET /api/auth/me", s.me)
api.HandleFunc("GET /api/info", s.info)
api.HandleFunc("GET /api/providers", s.listProviders)
api.HandleFunc("PUT /api/providers/active", s.setActiveProvider)
@ -64,8 +75,10 @@ func (s *Server) Handler() http.Handler {
api.HandleFunc("GET /api/sessions/{id}/messages", s.listMessages)
api.HandleFunc("POST /api/chat", s.chat)
api.HandleFunc("GET /api/ws", s.ws)
mux.Handle("/api/", s.originCheck(s.auth(api)))
return securityHeaders(mux)
mux.Handle("/api/", s.auth(api))
h := http.Handler(mux)
h = s.originCheck(h)
return securityHeaders(h)
}
func securityHeaders(h http.Handler) http.Handler {
@ -76,18 +89,6 @@ func securityHeaders(h http.Handler) http.Handler {
})
}
// auth requires "Authorization: Bearer <token>" (constant-time compare).
func (s *Server) auth(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if !ok || subtle.ConstantTimeCompare([]byte(got), []byte(s.Token)) != 1 {
writeErr(w, http.StatusUnauthorized, "unauthorized")
return
}
h.ServeHTTP(w, r)
})
}
// OriginAllowed: requests without Origin (native apps, curl) pass; browser
// requests must come from the same host or an explicitly configured origin.
func (s *Server) OriginAllowed(r *http.Request) bool {
@ -158,7 +159,7 @@ func (s *Server) storeErr(w http.ResponseWriter, err error) {
}
func (s *Server) listSessions(w http.ResponseWriter, r *http.Request) {
list, err := s.Store.ListSessions(r.Context(), 200)
list, err := s.Store.ListSessionsFor(r.Context(), principal(r.Context()).Scope(), 200)
if err != nil {
s.storeErr(w, err)
return
@ -173,7 +174,7 @@ func (s *Server) createSession(w http.ResponseWriter, r *http.Request) {
if r.ContentLength != 0 && !decode(w, r, &b) {
return
}
ss, err := s.Store.CreateSession(r.Context(), b.Title)
ss, err := s.Store.CreateSessionFor(r.Context(), principal(r.Context()).OwnerID(), b.Title)
if err != nil {
s.storeErr(w, err)
return
@ -182,6 +183,9 @@ func (s *Server) createSession(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) getSession(w http.ResponseWriter, r *http.Request) {
if !s.owns(w, r, r.PathValue("id")) {
return
}
ss, err := s.Store.GetSession(r.Context(), r.PathValue("id"))
if err != nil {
s.storeErr(w, err)
@ -197,6 +201,9 @@ func (s *Server) renameSession(w http.ResponseWriter, r *http.Request) {
if !decode(w, r, &b) {
return
}
if !s.owns(w, r, r.PathValue("id")) {
return
}
if err := s.Store.RenameSession(r.Context(), r.PathValue("id"), b.Title); err != nil {
s.storeErr(w, err)
return
@ -205,6 +212,9 @@ func (s *Server) renameSession(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) deleteSession(w http.ResponseWriter, r *http.Request) {
if !s.owns(w, r, r.PathValue("id")) {
return
}
if err := s.Store.DeleteSession(r.Context(), r.PathValue("id")); err != nil {
s.storeErr(w, err)
return
@ -214,8 +224,7 @@ func (s *Server) deleteSession(w http.ResponseWriter, r *http.Request) {
func (s *Server) listMessages(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if _, err := s.Store.GetSession(r.Context(), id); err != nil {
s.storeErr(w, err)
if !s.owns(w, r, id) {
return
}
msgs, err := s.Store.Messages(r.Context(), id, 0)
@ -256,14 +265,13 @@ func (s *Server) startSession(ctx context.Context, req chatReq) (string, error)
// ensureSession returns an existing session id or creates one titled after the message.
func (s *Server) ensureSession(ctx context.Context, id, text string) (string, error) {
if id != "" {
_, err := s.Store.GetSession(ctx, id)
return id, err
return id, s.Store.SessionOwnedBy(ctx, principal(ctx).Scope(), id)
}
title := strings.TrimSpace(strings.ReplaceAll(text, "\n", " "))
if utf8.RuneCountInString(title) > 48 {
title = string([]rune(title)[:48]) + "…"
}
ss, err := s.Store.CreateSession(ctx, title)
ss, err := s.Store.CreateSessionFor(ctx, principal(ctx).OwnerID(), title)
return ss.ID, err
}
@ -337,7 +345,7 @@ func (s *Server) ws(w http.ResponseWriter, r *http.Request) {
send(agent.Event{Type: "pong"})
continue
case "cancel":
if req.SessionID == "" || !s.Agent.Cancel(req.SessionID) {
if req.SessionID == "" || s.Store.SessionOwnedBy(ctx, principal(ctx).Scope(), req.SessionID) != nil || !s.Agent.Cancel(req.SessionID) {
send(agent.Event{Type: "error", Text: "nothing to cancel", Session: req.SessionID})
}
continue