feat(daemon): add authentication for daemon API routes

Issue daemon auth tokens (mdt_) on pairing session claim, bound to
workspace_id + daemon_id with 1-year expiry. Add DaemonAuth middleware
that validates these tokens and falls back to JWT/PAT for backward
compatibility. Apply middleware to all daemon routes except pairing
endpoints.
This commit is contained in:
Jiayuan 2026-03-31 14:41:26 +08:00 committed by Jiang Bohan
parent dc3dec8ebe
commit afdfee78b9
9 changed files with 306 additions and 16 deletions

View file

@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
@ -14,6 +15,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/auth"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
const daemonPairingTTL = 10 * time.Minute
@ -50,6 +53,7 @@ type DaemonPairingSessionResponse struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
LinkURL *string `json:"link_url,omitempty"`
DaemonToken *string `json:"daemon_token,omitempty"`
}
type CreateDaemonPairingSessionRequest struct {
@ -382,5 +386,39 @@ func (h *Handler) ClaimDaemonPairingSession(w http.ResponseWriter, r *http.Reque
return
}
writeJSON(w, http.StatusOK, daemonPairingSessionToResponse(rec, true))
resp := daemonPairingSessionToResponse(rec, true)
// Issue a daemon auth token bound to the workspace and daemon.
if rec.WorkspaceID.Valid {
plainToken, err := auth.GenerateDaemonToken()
if err != nil {
slog.Error("failed to generate daemon token", "error", err)
writeError(w, http.StatusInternalServerError, "failed to generate daemon token")
return
}
hash := auth.HashToken(plainToken)
// Revoke any existing tokens for this workspace+daemon pair.
_ = h.Queries.DeleteDaemonTokensByWorkspaceAndDaemon(r.Context(), db.DeleteDaemonTokensByWorkspaceAndDaemonParams{
WorkspaceID: rec.WorkspaceID,
DaemonID: rec.DaemonID,
})
_, err = h.Queries.CreateDaemonToken(r.Context(), db.CreateDaemonTokenParams{
TokenHash: hash,
WorkspaceID: rec.WorkspaceID,
DaemonID: rec.DaemonID,
ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(365 * 24 * time.Hour), Valid: true},
})
if err != nil {
slog.Error("failed to store daemon token", "error", err)
writeError(w, http.StatusInternalServerError, "failed to store daemon token")
return
}
resp.DaemonToken = &plainToken
slog.Info("daemon token issued", "daemon_id", rec.DaemonID, "workspace_id", uuidToPtr(rec.WorkspaceID))
}
writeJSON(w, http.StatusOK, resp)
}