fix oauth login on admin panel for production

This commit is contained in:
Evan Jarrett
2026-01-05 21:47:30 -06:00
parent f5979b8f08
commit c80b5b2941
+46 -23
View File
@@ -11,6 +11,7 @@ import (
"crypto/rand"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io/fs"
@@ -83,45 +84,50 @@ func NewAdminUI(ctx context.Context, holdPDS *pds.HoldPDS, quotaMgr *quota.Manag
return nil, fmt.Errorf("PublicURL is required for admin panel")
}
// Determine OAuth base URL
// - For IP addresses (Docker network IPs), substitute 127.0.0.1 (ATProto requires localhost for public clients)
// - For real domains, use as-is (works with ATProto OAuth)
oauthBaseURL := cfg.PublicURL
// Determine OAuth configuration based on URL type
u, err := url.Parse(cfg.PublicURL)
if err != nil {
return nil, fmt.Errorf("invalid PublicURL: %w", err)
}
host := u.Hostname()
if isIPAddress(host) {
// IP address (e.g., 172.28.0.3) - substitute localhost
port := u.Port()
if port == "" {
port = "8080"
}
oauthBaseURL = "http://127.0.0.1:" + port
}
// Use in-memory store for OAuth sessions
oauthStore := indigooauth.NewMemStore()
// Use minimal scopes for admin (only need basic auth, no blob access)
adminScopes := []string{"atproto"}
// Admin panel uses localhost callback for OAuth (ATProto requirement for public clients)
redirectURI := oauthBaseURL + "/admin/auth/oauth/callback"
var oauthConfig indigooauth.ClientConfig
var redirectURI string
// Use simple public client - no need for confidential client complexity
oauthConfig := indigooauth.NewLocalhostConfig(redirectURI, adminScopes)
host := u.Hostname()
if isIPAddress(host) || host == "localhost" || host == "127.0.0.1" {
// Development mode: IP address or localhost - use localhost OAuth config
// Substitute 127.0.0.1 for Docker network IPs
port := u.Port()
if port == "" {
port = "8080"
}
oauthBaseURL := "http://127.0.0.1:" + port
redirectURI = oauthBaseURL + "/admin/auth/oauth/callback"
oauthConfig = indigooauth.NewLocalhostConfig(redirectURI, adminScopes)
slog.Info("Admin OAuth configured (localhost mode)",
"redirect_uri", redirectURI,
"public_url", cfg.PublicURL)
} else {
// Production mode: real domain - use public client with metadata endpoint
clientID := cfg.PublicURL + "/admin/oauth-client-metadata.json"
redirectURI = cfg.PublicURL + "/admin/auth/oauth/callback"
oauthConfig = indigooauth.NewPublicConfig(clientID, redirectURI, adminScopes)
slog.Info("Admin OAuth configured (production mode)",
"client_id", clientID,
"redirect_uri", redirectURI)
}
clientApp := indigooauth.NewClientApp(&oauthConfig, oauthStore)
clientApp.Dir = atproto.GetDirectory()
slog.Info("Admin OAuth configured",
"redirect_uri", redirectURI,
"public_url", cfg.PublicURL,
"oauth_base_url", oauthBaseURL)
// Parse templates
templates, err := parseTemplates()
if err != nil {
@@ -286,6 +292,9 @@ func (ui *AdminUI) RegisterRoutes(r chi.Router) {
staticSub, _ := fs.Sub(staticFS, "static")
r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", http.FileServer(http.FS(staticSub))))
// OAuth client metadata endpoint (required for production OAuth)
r.Get("/admin/oauth-client-metadata.json", ui.handleClientMetadata)
// Public auth routes
r.Get("/admin/auth/login", ui.handleLogin)
r.Get("/admin/auth/oauth/authorize", ui.handleAuthorize)
@@ -320,6 +329,20 @@ func (ui *AdminUI) RegisterRoutes(r chi.Router) {
})
}
// handleClientMetadata serves the OAuth client metadata for production deployments
func (ui *AdminUI) handleClientMetadata(w http.ResponseWriter, r *http.Request) {
metadata := ui.clientApp.Config.ClientMetadata()
// Set client name for display in OAuth consent screen
clientName := "Hold Admin Panel"
metadata.ClientName = &clientName
metadata.ClientURI = &ui.config.PublicURL
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=3600")
json.NewEncoder(w).Encode(metadata)
}
// Close cleans up resources (no-op now, but keeps interface consistent)
func (ui *AdminUI) Close() error {
return nil