mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-01 15:56:58 +00:00
135 lines
4.1 KiB
Go
135 lines
4.1 KiB
Go
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)
|
|
}
|