2525
2626def _check_security ():
2727 """Warn loudly about insecure defaults on startup."""
28- if settings .SECRET_KEY in _INSECURE_SECRETS :
28+ if settings .SECRET_KEY . get_secret_value () in _INSECURE_SECRETS :
2929 safe_key = secrets .token_urlsafe (32 )
3030 logger .critical (
3131 "\n "
@@ -37,11 +37,11 @@ def _check_security():
3737 "╚══════════════════════════════════════════════════════╝" ,
3838 safe_key [:44 ],
3939 )
40- if settings .ADMIN_PASS in {"admin" , "password" , "123456" , "" }:
40+ admin_pass = settings .ADMIN_PASS .get_secret_value ()
41+ if admin_pass in {"admin" , "password" , "123456" , "" }:
4142 logger .warning (
42- "ADMIN_PASS is set to a weak default ('%s') . "
43+ "ADMIN_PASS is set to a weak default. "
4344 "Change it in .env before deploying to production." ,
44- settings .ADMIN_PASS ,
4545 )
4646 # Either SSO path issues the session cookie (OIDC state/nonce/PKCE) over
4747 # the wire. Without TLS marking, a passive attacker on the path can
@@ -56,6 +56,38 @@ def _check_security():
5656 "COOKIE_SECURE=true before exposing it."
5757 )
5858
59+ # Fail loudly if the SSO toggles are on but no usable provider is
60+ # configured. Previously this surfaced only on the first login attempt
61+ # as an opaque 500; catching it here saves operators a debugging loop.
62+ if settings .OIDC_ENABLED :
63+ providers = [p .strip () for p in settings .OIDC_PROVIDERS .split ("," ) if p .strip ()]
64+ if not providers :
65+ logger .error (
66+ "OIDC_ENABLED=true but OIDC_PROVIDERS is empty. "
67+ "Login page will show no SSO buttons."
68+ )
69+ else :
70+ any_configured = False
71+ if "google" in providers and settings .OIDC_GOOGLE_CLIENT_ID and settings .OIDC_GOOGLE_CLIENT_SECRET .get_secret_value ():
72+ any_configured = True
73+ if "github" in providers and settings .OIDC_GITHUB_CLIENT_ID and settings .OIDC_GITHUB_CLIENT_SECRET .get_secret_value ():
74+ any_configured = True
75+ if "generic" in providers and settings .OIDC_GENERIC_CLIENT_ID and settings .OIDC_GENERIC_CLIENT_SECRET .get_secret_value () and settings .OIDC_GENERIC_DISCOVERY :
76+ any_configured = True
77+ if not any_configured :
78+ logger .error (
79+ "OIDC_ENABLED=true but no provider is fully configured. "
80+ "Set the matching OIDC_{provider}_CLIENT_ID and _SECRET "
81+ "(and _DISCOVERY for 'generic')."
82+ )
83+
84+ if settings .LDAP_ENABLED :
85+ if not settings .LDAP_SERVER or not settings .LDAP_BIND_DN or not settings .LDAP_USER_BASE :
86+ logger .error (
87+ "LDAP_ENABLED=true but LDAP_SERVER/LDAP_BIND_DN/LDAP_USER_BASE "
88+ "is missing. LDAP login will fail."
89+ )
90+
5991
6092@asynccontextmanager
6193async def lifespan (app : FastAPI ):
@@ -94,15 +126,17 @@ async def csrf_guard(request: Request, call_next):
94126 if request .url .path .rstrip ("/" ) in _CSRF_EXEMPT_PATHS :
95127 return await call_next (request )
96128
97- # Bearer-token requests don't ride on the browser-ambient cookie, so CSRF
98- # doesn't reach them. Skip the check.
99- auth_header = request .headers .get ("Authorization" , "" )
100- if auth_header .startswith ("Bearer " ):
101- return await call_next (request )
102-
103129 # If there's no session cookie, there's nothing to protect: any request
104130 # without credentials will be rejected by the route's auth dependency,
105- # and CSRF only matters for ambient-credential flows.
131+ # and CSRF only matters for ambient-credential flows. This branch also
132+ # covers legitimate Bearer-token clients (API tokens, tests), which never
133+ # attach the session cookie.
134+ #
135+ # We deliberately do NOT exempt based on the Authorization header alone:
136+ # `get_current_user` falls back to the cookie when a Bearer token is
137+ # invalid, so a cross-origin attacker could otherwise pair a bogus
138+ # `Authorization: Bearer xyz` with the victim's session cookie to skip
139+ # CSRF protection on every mutating endpoint.
106140 if not request .cookies .get ("token" ):
107141 return await call_next (request )
108142
@@ -149,7 +183,7 @@ def _origin_of(url: str | None) -> str | None:
149183_session_https_only = settings .COOKIE_SECURE or settings .PUBLIC_BASE_URL .startswith ("https://" )
150184app .add_middleware (
151185 SessionMiddleware ,
152- secret_key = settings .SECRET_KEY ,
186+ secret_key = settings .SECRET_KEY . get_secret_value () ,
153187 session_cookie = "justwiki_oauth" ,
154188 same_site = "lax" ,
155189 https_only = _session_https_only ,
0 commit comments