network/pkg/gateway/handlers/auth/jwt_handler.go
2026-01-20 10:12:33 +02:00

198 lines
5.7 KiB
Go

package auth
import (
"encoding/json"
"net/http"
"strings"
"time"
authsvc "github.com/DeBrosOfficial/network/pkg/gateway/auth"
)
// APIKeyToJWTHandler issues a short-lived JWT from a valid API key.
// This allows API key holders to obtain JWT tokens for use with the gateway.
//
// POST /v1/auth/token
// Requires: Authorization header with API key (Bearer, ApiKey, or X-API-Key header)
// Response: { "access_token", "token_type", "expires_in", "namespace" }
func (h *Handlers) APIKeyToJWTHandler(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
}
key := extractAPIKey(r)
if strings.TrimSpace(key) == "" {
writeError(w, http.StatusUnauthorized, "missing API key")
return
}
// Validate and get namespace
db := h.netClient.Database()
ctx := r.Context()
internalCtx := h.internalAuthFn(ctx)
q := "SELECT namespaces.name FROM api_keys JOIN namespaces ON api_keys.namespace_id = namespaces.id WHERE api_keys.key = ? LIMIT 1"
res, err := db.Query(internalCtx, q, key)
if err != nil || res == nil || res.Count == 0 || len(res.Rows) == 0 {
writeError(w, http.StatusUnauthorized, "invalid API key")
return
}
// Extract namespace from first row
row, ok := res.Rows[0].([]interface{})
if !ok || len(row) == 0 {
writeError(w, http.StatusUnauthorized, "invalid API key")
return
}
var ns string
if s, ok := row[0].(string); ok {
ns = s
} else {
writeError(w, http.StatusUnauthorized, "invalid API key")
return
}
token, expUnix, err := h.authService.GenerateJWT(ns, key, 15*time.Minute)
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()),
"namespace": ns,
})
}
// RefreshHandler refreshes an access token using a refresh token.
//
// POST /v1/auth/refresh
// Request body: RefreshRequest
// Response: { "access_token", "token_type", "expires_in", "refresh_token", "subject", "namespace" }
func (h *Handlers) RefreshHandler(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
}
var req RefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(req.RefreshToken) == "" {
writeError(w, http.StatusBadRequest, "refresh_token is required")
return
}
token, subject, expUnix, err := h.authService.RefreshToken(r.Context(), req.RefreshToken, req.Namespace)
if err != nil {
writeError(w, http.StatusUnauthorized, 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": req.RefreshToken,
"subject": subject,
"namespace": req.Namespace,
})
}
// LogoutHandler revokes refresh tokens.
// If a refresh_token is provided, it will be revoked.
// If all=true is provided (and the request is authenticated via JWT),
// all tokens for the JWT subject within the namespace are revoked.
//
// POST /v1/auth/logout
// Request body: LogoutRequest
// Response: { "status": "ok" }
func (h *Handlers) LogoutHandler(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
}
var req LogoutRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
ctx := r.Context()
var subject string
if req.All {
if v := ctx.Value(CtxKeyJWT); v != nil {
if claims, ok := v.(*authsvc.JWTClaims); ok && claims != nil {
subject = strings.TrimSpace(claims.Sub)
}
}
if subject == "" {
writeError(w, http.StatusUnauthorized, "jwt required for all=true")
return
}
}
if err := h.authService.RevokeToken(ctx, req.Namespace, req.RefreshToken, req.All, subject); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
}
// extractAPIKey extracts API key from Authorization, X-API-Key header, or query parameters
func extractAPIKey(r *http.Request) string {
// Prefer X-API-Key header (most explicit)
if v := strings.TrimSpace(r.Header.Get("X-API-Key")); v != "" {
return v
}
// Check Authorization header for ApiKey scheme or non-JWT Bearer tokens
auth := r.Header.Get("Authorization")
if auth != "" {
lower := strings.ToLower(auth)
if strings.HasPrefix(lower, "bearer ") {
tok := strings.TrimSpace(auth[len("Bearer "):])
// Skip Bearer tokens that look like JWTs (have 2 dots)
if strings.Count(tok, ".") != 2 {
return tok
}
} else if strings.HasPrefix(lower, "apikey ") {
return strings.TrimSpace(auth[len("ApiKey "):])
} else if !strings.Contains(auth, " ") {
// If header has no scheme, treat the whole value as token
tok := strings.TrimSpace(auth)
if strings.Count(tok, ".") != 2 {
return tok
}
}
}
// Fallback to query parameter
if v := strings.TrimSpace(r.URL.Query().Get("api_key")); v != "" {
return v
}
if v := strings.TrimSpace(r.URL.Query().Get("token")); v != "" {
return v
}
return ""
}