mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-01 07:46:55 +00:00
117 lines
3.3 KiB
Go
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)
|
|
}
|