tangled
alpha
login
or
join now
cuducos.me
/
at-container-registry
forked from
evan.jarrett.net/at-container-registry
0
fork
atom
A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork
atom
overview
issues
pulls
pipelines
fix oauth login on admin panel for production
evan.jarrett.net
2 months ago
c80b5b29
f5979b8f
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
+46
-23
1 changed file
expand all
collapse all
unified
split
pkg
hold
admin
admin.go
+46
-23
pkg/hold/admin/admin.go
reviewed
···
11
11
"crypto/rand"
12
12
"embed"
13
13
"encoding/base64"
14
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
86
-
// Determine OAuth base URL
87
87
-
// - For IP addresses (Docker network IPs), substitute 127.0.0.1 (ATProto requires localhost for public clients)
88
88
-
// - For real domains, use as-is (works with ATProto OAuth)
89
89
-
oauthBaseURL := cfg.PublicURL
87
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
93
+
// Use in-memory store for OAuth sessions
94
94
+
oauthStore := indigooauth.NewMemStore()
95
95
+
96
96
+
// Use minimal scopes for admin (only need basic auth, no blob access)
97
97
+
adminScopes := []string{"atproto"}
98
98
+
99
99
+
var oauthConfig indigooauth.ClientConfig
100
100
+
var redirectURI string
101
101
+
95
102
host := u.Hostname()
96
96
-
if isIPAddress(host) {
97
97
-
// IP address (e.g., 172.28.0.3) - substitute localhost
103
103
+
if isIPAddress(host) || host == "localhost" || host == "127.0.0.1" {
104
104
+
// Development mode: IP address or localhost - use localhost OAuth config
105
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
102
-
oauthBaseURL = "http://127.0.0.1:" + port
103
103
-
}
110
110
+
oauthBaseURL := "http://127.0.0.1:" + port
111
111
+
redirectURI = oauthBaseURL + "/admin/auth/oauth/callback"
112
112
+
oauthConfig = indigooauth.NewLocalhostConfig(redirectURI, adminScopes)
104
113
105
105
-
// Use in-memory store for OAuth sessions
106
106
-
oauthStore := indigooauth.NewMemStore()
114
114
+
slog.Info("Admin OAuth configured (localhost mode)",
115
115
+
"redirect_uri", redirectURI,
116
116
+
"public_url", cfg.PublicURL)
117
117
+
} else {
118
118
+
// Production mode: real domain - use public client with metadata endpoint
119
119
+
clientID := cfg.PublicURL + "/admin/oauth-client-metadata.json"
120
120
+
redirectURI = cfg.PublicURL + "/admin/auth/oauth/callback"
121
121
+
oauthConfig = indigooauth.NewPublicConfig(clientID, redirectURI, adminScopes)
107
122
108
108
-
// Use minimal scopes for admin (only need basic auth, no blob access)
109
109
-
adminScopes := []string{"atproto"}
110
110
-
111
111
-
// Admin panel uses localhost callback for OAuth (ATProto requirement for public clients)
112
112
-
redirectURI := oauthBaseURL + "/admin/auth/oauth/callback"
113
113
-
114
114
-
// Use simple public client - no need for confidential client complexity
115
115
-
oauthConfig := indigooauth.NewLocalhostConfig(redirectURI, adminScopes)
123
123
+
slog.Info("Admin OAuth configured (production mode)",
124
124
+
"client_id", clientID,
125
125
+
"redirect_uri", redirectURI)
126
126
+
}
116
127
117
128
clientApp := indigooauth.NewClientApp(&oauthConfig, oauthStore)
118
129
clientApp.Dir = atproto.GetDirectory()
119
119
-
120
120
-
slog.Info("Admin OAuth configured",
121
121
-
"redirect_uri", redirectURI,
122
122
-
"public_url", cfg.PublicURL,
123
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
295
+
// OAuth client metadata endpoint (required for production OAuth)
296
296
+
r.Get("/admin/oauth-client-metadata.json", ui.handleClientMetadata)
297
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
330
+
}
331
331
+
332
332
+
// handleClientMetadata serves the OAuth client metadata for production deployments
333
333
+
func (ui *AdminUI) handleClientMetadata(w http.ResponseWriter, r *http.Request) {
334
334
+
metadata := ui.clientApp.Config.ClientMetadata()
335
335
+
336
336
+
// Set client name for display in OAuth consent screen
337
337
+
clientName := "Hold Admin Panel"
338
338
+
metadata.ClientName = &clientName
339
339
+
metadata.ClientURI = &ui.config.PublicURL
340
340
+
341
341
+
w.Header().Set("Content-Type", "application/json")
342
342
+
w.Header().Set("Cache-Control", "public, max-age=3600")
343
343
+
json.NewEncoder(w).Encode(metadata)
321
344
}
322
345
323
346
// Close cleans up resources (no-op now, but keeps interface consistent)