Files
at-container-registry/pkg/auth/exchange/handler.go
T
2025-10-04 13:50:28 -05:00

117 lines
3.3 KiB
Go

package exchange
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"atcr.io/pkg/auth"
"atcr.io/pkg/auth/session"
"atcr.io/pkg/auth/token"
)
// Handler handles /auth/exchange requests (session token -> registry JWT)
type Handler struct {
issuer *token.Issuer
sessionManager *session.Manager
}
// NewHandler creates a new exchange handler
func NewHandler(issuer *token.Issuer, sessionManager *session.Manager) *Handler {
return &Handler{
issuer: issuer,
sessionManager: sessionManager,
}
}
// ExchangeRequest represents the request to exchange a session token for registry JWT
type ExchangeRequest struct {
Scope []string `json:"scope"` // Requested Docker scopes
}
// ExchangeResponse represents the response from /auth/exchange
type ExchangeResponse struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
// ServeHTTP handles the exchange request
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract session token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "authorization header required", http.StatusUnauthorized)
return
}
// Parse Bearer token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "invalid authorization header format", http.StatusUnauthorized)
return
}
sessionToken := parts[1]
// Validate session token
sessionClaims, err := h.sessionManager.Validate(sessionToken)
if err != nil {
fmt.Printf("DEBUG [exchange]: session validation failed: %v\n", err)
http.Error(w, fmt.Sprintf("invalid session token: %v", err), http.StatusUnauthorized)
return
}
fmt.Printf("DEBUG [exchange]: session validated for DID=%s, handle=%s\n", sessionClaims.DID, sessionClaims.Handle)
// Parse request body for scopes
var req ExchangeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
return
}
// Parse and validate scopes
access, err := auth.ParseScope(req.Scope)
if err != nil {
http.Error(w, fmt.Sprintf("invalid scope: %v", err), http.StatusBadRequest)
return
}
// Validate access permissions
if err := auth.ValidateAccess(sessionClaims.DID, sessionClaims.Handle, access); err != nil {
http.Error(w, fmt.Sprintf("access denied: %v", err), http.StatusForbidden)
return
}
// Issue registry JWT token
tokenString, err := h.issuer.Issue(sessionClaims.DID, access)
if err != nil {
http.Error(w, fmt.Sprintf("failed to issue token: %v", err), http.StatusInternalServerError)
return
}
// Return response
resp := ExchangeResponse{
Token: tokenString,
AccessToken: tokenString,
ExpiresIn: int(h.issuer.Expiration().Seconds()),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, fmt.Sprintf("failed to encode response: %v", err), http.StatusInternalServerError)
return
}
}
// RegisterRoutes registers the exchange handler with the provided mux
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.Handle("/auth/exchange", h)
}