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

fix oauth login on admin panel for production

evan.jarrett.net c80b5b29 f5979b8f

verified
+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)