Skip to content

Commit 769ee4e

Browse files
committed
fix: oidc config is hidden behind auth
1 parent fb47c3e commit 769ee4e

4 files changed

Lines changed: 107 additions & 5 deletions

File tree

internal/api/auth.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -631,15 +631,22 @@ func handleRegister(database *db.DB, sessions *SessionStore) http.HandlerFunc {
631631
}
632632
}
633633

634-
// handleAuthStatus returns the current auth state (are there users? is user logged in?)
635-
func handleAuthStatus(database *db.DB, sessions *SessionStore) http.HandlerFunc {
634+
// handleAuthStatus returns the current auth state (are there users? is user logged in? is oidc available?)
635+
func handleAuthStatus(cfg *config.Config, database *db.DB, sessions *SessionStore) http.HandlerFunc {
636636
return func(w http.ResponseWriter, r *http.Request) {
637637
userCount, _ := database.CountUsers()
638638

639-
resp := map[string]interface{}{
639+
resp := map[string]any{
640640
"multi_user": userCount > 0,
641641
"has_users": userCount > 0,
642642
"authenticated": false,
643+
"oidc_enabled": false,
644+
}
645+
646+
// OIDC hints
647+
if cfg != nil && cfg.HasOIDC() {
648+
resp["oidc_enabled"] = true
649+
resp["oidc_provider_name"] = cfg.OIDCProviderName
643650
}
644651

645652
// Check session.

internal/api/auth_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package api
22

33
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"path/filepath"
48
"testing"
59
"time"
10+
11+
"github.com/JeremiahM37/librarr/internal/config"
12+
"github.com/JeremiahM37/librarr/internal/db"
613
)
714

815
func TestSessionStore_CreateAndGet(t *testing.T) {
@@ -219,3 +226,91 @@ func TestIsExempt(t *testing.T) {
219226
})
220227
}
221228
}
229+
230+
func TestHandleAuthStatus_OIDCHints(t *testing.T) {
231+
// /api/auth/status is the canonical pre-auth endpoint the login modal
232+
// hits to decide whether to render the SSO button. /api/config is gated
233+
// behind the multi-user middleware once any user exists, so the modal
234+
// MUST be able to read the OIDC hint here or the button silently
235+
// disappears after the first user registers.
236+
dir := t.TempDir()
237+
database, err := db.New(filepath.Join(dir, "test.db"))
238+
if err != nil {
239+
t.Fatalf("create test db: %v", err)
240+
}
241+
t.Cleanup(func() { database.Close() })
242+
sessions := NewSessionStore()
243+
244+
tests := []struct {
245+
name string
246+
cfg *config.Config
247+
wantEnabled bool
248+
wantProviderName string
249+
}{
250+
{
251+
name: "nil config",
252+
cfg: nil,
253+
wantEnabled: false,
254+
},
255+
{
256+
name: "OIDC disabled",
257+
cfg: &config.Config{OIDCEnabled: false},
258+
wantEnabled: false,
259+
},
260+
{
261+
name: "OIDC partially configured (no client secret)",
262+
cfg: &config.Config{
263+
OIDCEnabled: true,
264+
OIDCIssuer: "https://idp.example.com",
265+
OIDCClientID: "client",
266+
OIDCProviderName: "Ignored",
267+
},
268+
wantEnabled: false,
269+
},
270+
{
271+
name: "OIDC fully configured",
272+
cfg: &config.Config{
273+
OIDCEnabled: true,
274+
OIDCIssuer: "https://idp.example.com",
275+
OIDCClientID: "client",
276+
OIDCClientSecret: "secret",
277+
OIDCProviderName: "PocketID",
278+
},
279+
wantEnabled: true,
280+
wantProviderName: "PocketID",
281+
},
282+
}
283+
284+
for _, tt := range tests {
285+
t.Run(tt.name, func(t *testing.T) {
286+
req := httptest.NewRequest(http.MethodGet, "/api/auth/status", nil)
287+
rr := httptest.NewRecorder()
288+
handleAuthStatus(tt.cfg, database, sessions)(rr, req)
289+
290+
if rr.Code != http.StatusOK {
291+
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
292+
}
293+
var resp map[string]any
294+
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
295+
t.Fatalf("decode body: %v", err)
296+
}
297+
enabled, _ := resp["oidc_enabled"].(bool)
298+
if enabled != tt.wantEnabled {
299+
t.Errorf("oidc_enabled = %v, want %v", enabled, tt.wantEnabled)
300+
}
301+
if tt.wantEnabled {
302+
if got, _ := resp["oidc_provider_name"].(string); got != tt.wantProviderName {
303+
t.Errorf("oidc_provider_name = %q, want %q", got, tt.wantProviderName)
304+
}
305+
// MUST NOT leak the client secret on the public endpoint.
306+
for k, v := range resp {
307+
if s, ok := v.(string); ok && s == "secret" {
308+
t.Errorf("response leaks secret-like value at key %q", k)
309+
}
310+
}
311+
} else if _, present := resp["oidc_provider_name"]; present {
312+
t.Errorf("oidc_provider_name MUST be absent when OIDC is disabled, got %v", resp["oidc_provider_name"])
313+
}
314+
})
315+
}
316+
}

internal/api/router.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func (s *Server) registerRoutes() {
193193
s.mux.HandleFunc("POST /api/login/totp", handleLoginTOTP(s.db, s.sessions))
194194
s.mux.HandleFunc("POST /api/register", handleRegister(s.db, s.sessions))
195195
s.mux.HandleFunc("POST /api/logout", handleLogout(s.sessions, s.db))
196-
s.mux.HandleFunc("GET /api/auth/status", handleAuthStatus(s.db, s.sessions))
196+
s.mux.HandleFunc("GET /api/auth/status", handleAuthStatus(s.cfg, s.db, s.sessions))
197197

198198
// User management (admin only).
199199
s.mux.HandleFunc("GET /api/users", requireAdmin(handleListUsers(s.db)))

web/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,7 @@ <h3 class="text-sm font-semibold text-slate-300 uppercase tracking-wider mb-4" d
13861386
}).catch(() => {});
13871387

13881388
// Check OIDC config.
1389-
fetch('/api/config', { credentials: 'include' }).then(r => r.json()).then(data => {
1389+
fetch('/api/auth/status', { credentials: 'include' }).then(r => r.json()).then(data => {
13901390
if (data.oidc_enabled) {
13911391
document.getElementById('login-oidc-btn').classList.remove('hidden');
13921392
document.getElementById('oidc-login-link').textContent = t('login_with', {provider: data.oidc_provider_name || 'SSO'});

0 commit comments

Comments
 (0)