High severity7.4NVD Advisory· Published Apr 10, 2026· Updated Apr 20, 2026
CVE-2026-34727
CVE-2026-34727
Description
Vikunja is an open-source self-hosted task management platform. Prior to 2.3.0, 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. This vulnerability is fixed in 2.3.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
code.vikunja.io/apiGo | < 2.3.0 | 2.3.0 |
Affected products
1Patches
1b642b2a4536afeat(auth): prompt for TOTP code in the OIDC callback flow
2 files changed · +83 −3
frontend/src/i18n/lang/en.json+2 −0 modified@@ -81,6 +81,8 @@ "authenticating": "Authenticating…", "openIdStateError": "State does not match, refusing to continue!", "openIdGeneralError": "An error occurred while authenticating against the third party.", + "openIdTotpRequired": "Your account requires two-factor authentication. Enter your TOTP code and sign in again.", + "openIdTotpSubmit": "Continue", "oauthMissingParams": "Missing required OAuth parameters: {params}", "oauthRedirectedToApp": "You have been redirected to the app. You can close this tab now.", "desktopTryDemo": "Try the Demo",
frontend/src/views/user/OpenIdAuth.vue+81 −3 modified@@ -13,9 +13,38 @@ > {{ errorMessageFromQuery }} </Message> - <Message v-if="loading"> + <Message v-if="loading && !needsTotp"> {{ $t('user.auth.authenticating') }} </Message> + + <form + v-if="needsTotp" + @submit.prevent="submitTotpAndRestart" + > + <Message class="mbe-2"> + {{ $t('user.auth.openIdTotpRequired') }} + </Message> + <FormField + id="openIdTotpPasscode" + ref="totpInput" + v-model="totpPasscode" + v-focus + :label="$t('user.auth.totpTitle')" + autocomplete="one-time-code" + :placeholder="$t('user.auth.totpPlaceholder')" + required + type="text" + inputmode="numeric" + /> + <XButton + :loading="loading" + :disabled="!totpPasscode" + class="mbs-2" + @click="submitTotpAndRestart" + > + {{ $t('user.auth.openIdTotpSubmit') }} + </XButton> + </form> </div> </template> @@ -27,9 +56,13 @@ import {useI18n} from 'vue-i18n' import {getErrorText} from '@/message' import Message from '@/components/misc/Message.vue' +import FormField from '@/components/input/FormField.vue' import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited' +import {redirectToProvider} from '@/helpers/redirectToProvider' import {useAuthStore} from '@/stores/auth' +import {useConfigStore} from '@/stores/config' +import type {IProvider} from '@/types/IProvider' defineOptions({name: 'Auth'}) @@ -39,11 +72,23 @@ const route = useRoute() const {redirectIfSaved} = useRedirectToLastVisited() const authStore = useAuthStore() +const configStore = useConfigStore() const loading = computed(() => authStore.isLoading) const errorMessage = ref('') const errorMessageFromQuery = computed(() => route.query.error) +const needsTotp = ref(false) +const totpPasscode = ref('') + +function pendingTotpKey(provider: string): string { + return `openid_pending_totp_${provider}` +} + +function findProvider(providerKey: string): IProvider | undefined { + return configStore.auth.openidConnect.providers?.find((p: IProvider) => p.key === providerKey) +} + async function authenticateWithCode() { // This component gets mounted twice: The first time when the actual auth request hits the frontend, // the second time after that auth request succeeded and the outer component "content-no-auth" isn't used @@ -60,8 +105,11 @@ async function authenticateWithCode() { errorMessage.value = '' + const providerKey = route.params.provider as string + if (typeof route.query.error !== 'undefined') { localStorage.removeItem('authenticating') + sessionStorage.removeItem(pendingTotpKey(providerKey)) errorMessage.value = typeof route.query.message !== 'undefined' ? route.query.message as string : t('user.auth.openIdGeneralError') @@ -71,23 +119,53 @@ async function authenticateWithCode() { const state = localStorage.getItem('state') if (typeof route.query.state === 'undefined' || route.query.state !== state) { localStorage.removeItem('authenticating') + sessionStorage.removeItem(pendingTotpKey(providerKey)) errorMessage.value = t('user.auth.openIdStateError') return } + // sessionStorage (not localStorage): per-tab, cleared on tab close. + const pendingPasscode = sessionStorage.getItem(pendingTotpKey(providerKey)) ?? undefined + if (pendingPasscode) { + sessionStorage.removeItem(pendingTotpKey(providerKey)) + } + try { await authStore.openIdAuth({ - provider: route.params.provider, - code: route.query.code, + provider: providerKey, + code: route.query.code as string, + totpPasscode: pendingPasscode, }) redirectIfSaved() } catch (e) { + const err = e as {response?: {data?: {code?: number}}} + if (err?.response?.data?.code === 1017) { + needsTotp.value = true + return + } errorMessage.value = getErrorText(e) } finally { localStorage.removeItem('authenticating') } } +async function submitTotpAndRestart() { + if (!totpPasscode.value) { + return + } + + const providerKey = route.params.provider as string + const provider = findProvider(providerKey) + if (!provider) { + errorMessage.value = t('user.auth.openIdGeneralError') + return + } + + sessionStorage.setItem(pendingTotpKey(providerKey), totpPasscode.value) + // The auth code is single-use; restart the OIDC flow so the next callback reads the stashed passcode. + redirectToProvider(provider) +} + onMounted(() => authenticateWithCode()) </script>
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/go-vikunja/vikunja/security/advisories/GHSA-8jvc-mcx6-r4cgnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-8jvc-mcx6-r4cgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34727ghsaADVISORY
- github.com/go-vikunja/vikunja/commit/b642b2a4536a3846e627a78dce2fdd1be425e6a1ghsaWEB
- github.com/go-vikunja/vikunja/pull/2582ghsaWEB
- github.com/go-vikunja/vikunja/releases/tag/v2.3.0ghsaWEB
News mentions
0No linked articles in our index yet.