Enforce TOTP second factor on SSO login (#31)
SSO login minted a full session immediately, ignoring the account's totp_enabled flag — so a 2FA admin with a linked Google/Discord/OIDC identity could sign in without their authenticator code, silently downgrading the account to single-factor (the strength of the IdP login). The local password flow already gates on needsTotp(); SSO did not. Wire SSO through the same staged-TOTP gate: - ssoState: createTotpPending/verifyTotpPending + a short-lived httpOnly sso_totp cookie. The pending token carries stage:'totp' (session validation rejects it) + kind:'sso_totp' (scoped to the SSO endpoint) plus the resolved context (userId, provider, authMethod, returnTo). - sso.controller: finishLogin now stages the challenge and redirects to /admin/login?sso_totp=1 instead of creating a session when the account has TOTP on. New finishSsoTotp verifies the code (backoff + bot-scoring on failure, mirroring loginTotp) and only then mints the session. - sso.routes: POST /auth/sso/totp behind the same backoff/slow/limiter stack and code validation as the local TOTP endpoint. - client: AdminLogin detects ?sso_totp=1 and completes over fetch via api.ssoLoginTotp; the challenge never touches the URL or JS. Keeps the second factor httpOnly throughout, consistent with the SSO tx cookie. 12 new tests; full suite 106/106. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
This commit is contained in:
@@ -37,6 +37,14 @@ export function AuthProvider({ children }) {
|
||||
return data.user
|
||||
}, [])
|
||||
|
||||
// Step 2 for SSO logins whose account has 2FA on. The pending challenge lives in
|
||||
// an httpOnly cookie, so only the code is sent. Returns { user, returnTo }.
|
||||
const ssoLoginTotp = useCallback(async (code) => {
|
||||
const data = await api.ssoLoginTotp(code)
|
||||
setUser(data.user)
|
||||
return data
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await api.logout()
|
||||
@@ -46,7 +54,7 @@ export function AuthProvider({ children }) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, loginTotp, logout, refresh }}>
|
||||
<AuthContext.Provider value={{ user, loading, login, loginTotp, ssoLoginTotp, logout, refresh }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user