package exchange import ( "encoding/json" "fmt" "net/http" mainAtproto "atcr.io/pkg/atproto" "atcr.io/pkg/auth" "atcr.io/pkg/auth/atproto" "atcr.io/pkg/auth/token" ) // Handler handles /auth/exchange requests (OAuth token -> JWT token) type Handler struct { issuer *token.Issuer validator *atproto.TokenValidator defaultHoldEndpoint string } // NewHandler creates a new exchange handler func NewHandler(issuer *token.Issuer, defaultHoldEndpoint string) *Handler { return &Handler{ issuer: issuer, validator: atproto.NewTokenValidator(), defaultHoldEndpoint: defaultHoldEndpoint, } } // ExchangeRequest represents the request to exchange an OAuth token type ExchangeRequest struct { AccessToken string `json:"access_token"` // ATProto OAuth access token Handle string `json:"handle"` // User's handle (required for PDS resolution) 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 } 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 } if req.AccessToken == "" { http.Error(w, "access_token is required", http.StatusBadRequest) return } // Validate the ATProto OAuth token via the PDS // We need the handle to resolve the PDS endpoint if req.Handle == "" { http.Error(w, "handle required to validate token", http.StatusBadRequest) return } session, err := h.validator.ValidateTokenWithResolver(r.Context(), req.Handle, req.AccessToken) if err != nil { http.Error(w, fmt.Sprintf("token validation failed: %v", err), http.StatusUnauthorized) return } // Use DID and handle from validated session did := session.DID handle := session.Handle // 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(did, handle, access); err != nil { http.Error(w, fmt.Sprintf("access denied: %v", err), http.StatusForbidden) return } // Ensure user profile exists (creates with default hold if needed) // Resolve PDS endpoint for profile management resolver := mainAtproto.NewResolver() _, pdsEndpoint, err := resolver.ResolveIdentity(r.Context(), handle) if err != nil { // Log error but don't fail auth - profile management is not critical fmt.Printf("WARNING: failed to resolve PDS for profile management: %v\n", err) } else { // Create ATProto client with validated token atprotoClient := mainAtproto.NewClient(pdsEndpoint, did, req.AccessToken) // Ensure profile exists (will create with default hold if not exists and default is configured) if err := mainAtproto.EnsureProfile(r.Context(), atprotoClient, h.defaultHoldEndpoint); err != nil { // Log error but don't fail auth - profile management is not critical fmt.Printf("WARNING: failed to ensure profile for %s: %v\n", did, err) } } // Issue JWT token tokenString, err := h.issuer.Issue(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) }