orama/pkg/gateway/handlers/auth/verify_handler.go

139 lines
4.6 KiB
Go

package auth
import (
"encoding/json"
"net/http"
"strings"
"time"
)
// VerifyHandler verifies a wallet signature and issues JWT tokens and an API key.
// This completes the authentication flow by validating the signed nonce and returning
// access credentials. For non-default namespaces, may trigger cluster provisioning
// and return 202 Accepted with credentials + poll URL.
//
// POST /v1/auth/verify
// Request body: VerifyRequest
// Response 200: { "access_token", "token_type", "expires_in", "refresh_token", "subject", "namespace", "api_key", "nonce", "signature_verified" }
// Response 202: { "status": "provisioning", "cluster_id", "poll_url", "access_token", "refresh_token", "api_key", ... }
func (h *Handlers) VerifyHandler(w http.ResponseWriter, r *http.Request) {
if h.authService == nil {
writeError(w, http.StatusServiceUnavailable, "auth service not initialized")
return
}
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 64*1024) // 64KB
var req VerifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.Wallet) == "" || strings.TrimSpace(req.Nonce) == "" || strings.TrimSpace(req.Signature) == "" {
writeError(w, http.StatusBadRequest, "wallet, nonce and signature are required")
return
}
ctx := r.Context()
verified, err := h.authService.VerifySignature(ctx, req.Wallet, req.Nonce, req.Signature, req.ChainType)
if err != nil || !verified {
writeError(w, http.StatusUnauthorized, "signature verification failed")
return
}
// Mark nonce used
nsID, _ := h.resolveNamespace(ctx, req.Namespace)
h.markNonceUsed(ctx, nsID, strings.ToLower(req.Wallet), req.Nonce)
// Check if namespace cluster provisioning is needed (for non-default namespaces)
namespace := strings.TrimSpace(req.Namespace)
if namespace == "" {
namespace = "default"
}
if h.clusterProvisioner != nil && namespace != "default" {
clusterID, status, needsProvisioning, checkErr := h.clusterProvisioner.CheckNamespaceCluster(ctx, namespace)
if checkErr != nil {
_ = checkErr // Log but don't fail
} else if needsProvisioning || status == "provisioning" {
// Issue tokens and API key before returning provisioning status
token, refresh, expUnix, tokenErr := h.authService.IssueTokens(ctx, req.Wallet, req.Namespace)
if tokenErr != nil {
writeError(w, http.StatusInternalServerError, tokenErr.Error())
return
}
apiKey, keyErr := h.authService.GetOrCreateAPIKey(ctx, req.Wallet, req.Namespace)
if keyErr != nil {
writeError(w, http.StatusInternalServerError, keyErr.Error())
return
}
pollURL := ""
if needsProvisioning {
nsIDInt := 0
if id, ok := nsID.(int); ok {
nsIDInt = id
} else if id, ok := nsID.(int64); ok {
nsIDInt = int(id)
} else if id, ok := nsID.(float64); ok {
nsIDInt = int(id)
}
newClusterID, newPollURL, provErr := h.clusterProvisioner.ProvisionNamespaceCluster(ctx, nsIDInt, namespace, req.Wallet)
if provErr != nil {
writeError(w, http.StatusInternalServerError, "failed to start cluster provisioning")
return
}
clusterID = newClusterID
pollURL = newPollURL
} else {
pollURL = "/v1/namespace/status?id=" + clusterID
}
writeJSON(w, http.StatusAccepted, map[string]any{
"status": "provisioning",
"cluster_id": clusterID,
"poll_url": pollURL,
"estimated_time_seconds": 60,
"access_token": token,
"token_type": "Bearer",
"expires_in": int(expUnix - time.Now().Unix()),
"refresh_token": refresh,
"api_key": apiKey,
"namespace": req.Namespace,
"subject": req.Wallet,
"nonce": req.Nonce,
"signature_verified": true,
})
return
}
}
token, refresh, expUnix, err := h.authService.IssueTokens(ctx, req.Wallet, req.Namespace)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
apiKey, err := h.authService.GetOrCreateAPIKey(ctx, req.Wallet, req.Namespace)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"access_token": token,
"token_type": "Bearer",
"expires_in": int(expUnix - time.Now().Unix()),
"refresh_token": refresh,
"subject": req.Wallet,
"namespace": req.Namespace,
"api_key": apiKey,
"nonce": req.Nonce,
"signature_verified": true,
})
}