fix(oidc): security + robustness hardening from codex review rounds 1-2
Round 1: - bind SSO identities to (external_issuer, external_subject): OIDC only guarantees sub uniqueness within an issuer, so a sub-only lookup let a newly configured IdP's user inherit an old IdP's admin account on subject collision; migration 162 gains external_issuer + composite unique index (unmerged migration, edited in place) - fetch UserInfo (with sub cross-check) when the ID token carries no email — spec-compliant providers may serve email/profile claims only there; ID-token claims win on merge - allowlist /admin/sso/login + /callback in maintenance mode, or SSO-only (JIT) admins are locked out exactly when they need in - strip reserved keys (oidc_client_secret, setup_token) from BOTH generic settings reads (GET / and GET /:type) Round 2: - redirect_uri prefers API_URL (the API's public origin — where the state cookie lives); final redirects absolute to the frontend base; login button builds its URL via buildResourceUrl — split-origin deployments (absolute VITE_API_URL) work end to end - PUT /sso validates the MERGED resulting state (partial update cannot blank issuer/client while enabled=true survives; enabling requires a derivable redirect URI) - openid scope forced into oidc_scopes on save - discovery-cache key includes a secret fingerprint (multi-worker secret rotation) - email→admin linking claims the row atomically (conditional update on external_subject IS NULL) — concurrent first-time callbacks with the same verified email but different subjects can't both authenticate Tests: mock IdP gains userinfo endpoint + email-via-userinfo-only mode; new cases pin the userinfo merge and issuer-collision non-inheritance; redirect assertions updated for absolute URLs. 13/13.
This commit is contained in:
@@ -13,6 +13,7 @@ import { setupService } from '../../services/setup.service';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { useAdminDarkMode } from '../../contexts/AdminDarkModeContext';
|
||||
import { resolveLoginLogoClasses } from '../../utils/loginLogoSize';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
import { api } from '../../config/api';
|
||||
|
||||
export const AdminLoginPage: React.FC = () => {
|
||||
@@ -366,7 +367,11 @@ export const AdminLoginPage: React.FC = () => {
|
||||
size="lg"
|
||||
className="w-full"
|
||||
leftIcon={<KeyRound className="w-4 h-4" />}
|
||||
onClick={() => { window.location.href = '/api/auth/admin/sso/login'; }}
|
||||
// buildResourceUrl respects an absolute VITE_API_URL, so
|
||||
// split-origin deployments start the flow on the API host
|
||||
// (where the state cookie must live) instead of 404ing on
|
||||
// the frontend origin.
|
||||
onClick={() => { window.location.href = buildResourceUrl('/api/auth/admin/sso/login'); }}
|
||||
>
|
||||
{settingsData.oidc_button_label?.trim() || t('adminLogin.ssoSignIn', 'Sign in with SSO')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user