A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.

fix oauth login on admin panel for production

authored by evan.jarrett.net and committed by

Eduardo Cuducos 6060e288 35f59dbd

+46 -23
+46 -23
pkg/hold/admin/admin.go
··· 11 11 "crypto/rand" 12 12 "embed" 13 13 "encoding/base64" 14 + "encoding/json" 14 15 "fmt" 15 16 "html/template" 16 17 "io/fs" ··· 83 84 return nil, fmt.Errorf("PublicURL is required for admin panel") 84 85 } 85 86 86 - // Determine OAuth base URL 87 - // - For IP addresses (Docker network IPs), substitute 127.0.0.1 (ATProto requires localhost for public clients) 88 - // - For real domains, use as-is (works with ATProto OAuth) 89 - oauthBaseURL := cfg.PublicURL 87 + // Determine OAuth configuration based on URL type 90 88 u, err := url.Parse(cfg.PublicURL) 91 89 if err != nil { 92 90 return nil, fmt.Errorf("invalid PublicURL: %w", err) 93 91 } 94 92 93 + // Use in-memory store for OAuth sessions 94 + oauthStore := indigooauth.NewMemStore() 95 + 96 + // Use minimal scopes for admin (only need basic auth, no blob access) 97 + adminScopes := []string{"atproto"} 98 + 99 + var oauthConfig indigooauth.ClientConfig 100 + var redirectURI string 101 + 95 102 host := u.Hostname() 96 - if isIPAddress(host) { 97 - // IP address (e.g., 172.28.0.3) - substitute localhost 103 + if isIPAddress(host) || host == "localhost" || host == "127.0.0.1" { 104 + // Development mode: IP address or localhost - use localhost OAuth config 105 + // Substitute 127.0.0.1 for Docker network IPs 98 106 port := u.Port() 99 107 if port == "" { 100 108 port = "8080" 101 109 } 102 - oauthBaseURL = "http://127.0.0.1:" + port 103 - } 110 + oauthBaseURL := "http://127.0.0.1:" + port 111 + redirectURI = oauthBaseURL + "/admin/auth/oauth/callback" 112 + oauthConfig = indigooauth.NewLocalhostConfig(redirectURI, adminScopes) 104 113 105 - // Use in-memory store for OAuth sessions 106 - oauthStore := indigooauth.NewMemStore() 114 + slog.Info("Admin OAuth configured (localhost mode)", 115 + "redirect_uri", redirectURI, 116 + "public_url", cfg.PublicURL) 117 + } else { 118 + // Production mode: real domain - use public client with metadata endpoint 119 + clientID := cfg.PublicURL + "/admin/oauth-client-metadata.json" 120 + redirectURI = cfg.PublicURL + "/admin/auth/oauth/callback" 121 + oauthConfig = indigooauth.NewPublicConfig(clientID, redirectURI, adminScopes) 107 122 108 - // Use minimal scopes for admin (only need basic auth, no blob access) 109 - adminScopes := []string{"atproto"} 110 - 111 - // Admin panel uses localhost callback for OAuth (ATProto requirement for public clients) 112 - redirectURI := oauthBaseURL + "/admin/auth/oauth/callback" 113 - 114 - // Use simple public client - no need for confidential client complexity 115 - oauthConfig := indigooauth.NewLocalhostConfig(redirectURI, adminScopes) 123 + slog.Info("Admin OAuth configured (production mode)", 124 + "client_id", clientID, 125 + "redirect_uri", redirectURI) 126 + } 116 127 117 128 clientApp := indigooauth.NewClientApp(&oauthConfig, oauthStore) 118 129 clientApp.Dir = atproto.GetDirectory() 119 - 120 - slog.Info("Admin OAuth configured", 121 - "redirect_uri", redirectURI, 122 - "public_url", cfg.PublicURL, 123 - "oauth_base_url", oauthBaseURL) 124 130 125 131 // Parse templates 126 132 templates, err := parseTemplates() ··· 286 292 staticSub, _ := fs.Sub(staticFS, "static") 287 293 r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", http.FileServer(http.FS(staticSub)))) 288 294 295 + // OAuth client metadata endpoint (required for production OAuth) 296 + r.Get("/admin/oauth-client-metadata.json", ui.handleClientMetadata) 297 + 289 298 // Public auth routes 290 299 r.Get("/admin/auth/login", ui.handleLogin) 291 300 r.Get("/admin/auth/oauth/authorize", ui.handleAuthorize) ··· 318 327 // Logout 319 328 r.Get("/admin/auth/logout", ui.handleLogout) 320 329 }) 330 + } 331 + 332 + // handleClientMetadata serves the OAuth client metadata for production deployments 333 + func (ui *AdminUI) handleClientMetadata(w http.ResponseWriter, r *http.Request) { 334 + metadata := ui.clientApp.Config.ClientMetadata() 335 + 336 + // Set client name for display in OAuth consent screen 337 + clientName := "Hold Admin Panel" 338 + metadata.ClientName = &clientName 339 + metadata.ClientURI = &ui.config.PublicURL 340 + 341 + w.Header().Set("Content-Type", "application/json") 342 + w.Header().Set("Cache-Control", "public, max-age=3600") 343 + json.NewEncoder(w).Encode(metadata) 321 344 } 322 345 323 346 // Close cleans up resources (no-op now, but keeps interface consistent)