feat(auth): support OIDC RP-initiated logout
When a user logs out, redirect them to the configured OIDC provider's end-session endpoint with `client_id` and `post_logout_redirect_uri` query parameters so the IdP can identify the relying party and bounce the user back to the Vikunja login page. Also set a `justLoggedOut` flag in sessionStorage so the single-provider auto-redirect on the login page does not silently re-authenticate the user immediately after logout. Made-with: Cursor
This commit is contained in:
parent
066791ec00
commit
774bf257a2
|
|
@ -24,8 +24,45 @@ export const redirectToProvider = (provider: IProvider) => {
|
|||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}`
|
||||
}
|
||||
|
||||
// JUST_LOGGED_OUT_KEY is read by Login.vue to short-circuit any single-provider
|
||||
// auto-redirect right after a logout. Without it, an immediate bounce back to the
|
||||
// IdP would silently re-authenticate the user, defeating the logout entirely.
|
||||
export const JUST_LOGGED_OUT_KEY = 'justLoggedOut'
|
||||
|
||||
export const redirectToProviderOnLogout = (provider: IProvider) => {
|
||||
if (provider.logoutUrl.length > 0) {
|
||||
window.location.href = `${provider.logoutUrl}`
|
||||
if (!provider.logoutUrl || provider.logoutUrl.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark that we just logged out so Login.vue skips its auto-redirect when the
|
||||
// IdP sends the user back via post_logout_redirect_uri. sessionStorage
|
||||
// survives the round-trip to the IdP within the same tab.
|
||||
sessionStorage.setItem(JUST_LOGGED_OUT_KEY, '1')
|
||||
|
||||
let target = provider.logoutUrl
|
||||
try {
|
||||
const url = new URL(provider.logoutUrl)
|
||||
|
||||
// client_id lets the IdP identify the relying party when no id_token_hint
|
||||
// is available, so it can skip the "are you sure you want to log out?"
|
||||
// prompt for known clients (Authentik does this).
|
||||
if (provider.clientId) {
|
||||
url.searchParams.set('client_id', provider.clientId)
|
||||
}
|
||||
|
||||
// post_logout_redirect_uri tells the IdP where to send the user after
|
||||
// signing them out. We send them back to the frontend root, which the
|
||||
// router resolves to /login. Combined with JUST_LOGGED_OUT_KEY above,
|
||||
// the login page will render normally instead of auto-redirecting.
|
||||
const current = parseURL(window.location.href)
|
||||
const base = getFullBaseUrl()
|
||||
url.searchParams.set('post_logout_redirect_uri', `${current.protocol}//${current.host}${base}`)
|
||||
|
||||
target = url.toString()
|
||||
} catch {
|
||||
// Fall back to the raw URL if it's not parseable as an absolute URL.
|
||||
// We still want logout to navigate even if the admin misconfigured it.
|
||||
}
|
||||
|
||||
window.location.href = target
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ import FormCheckbox from '@/components/input/FormCheckbox.vue'
|
|||
import DesktopLogin from '@/views/user/DesktopLogin.vue'
|
||||
|
||||
import {getErrorText} from '@/message'
|
||||
import {redirectToProvider} from '@/helpers/redirectToProvider'
|
||||
import {JUST_LOGGED_OUT_KEY, redirectToProvider} from '@/helpers/redirectToProvider'
|
||||
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
|
||||
import {isDesktopApp} from '@/helpers/desktopAuth'
|
||||
|
||||
|
|
@ -185,7 +185,13 @@ onBeforeMount(() => {
|
|||
}
|
||||
|
||||
// When local and LDAP auth are both off and there's exactly one OIDC provider,
|
||||
// skip the login page and redirect straight to the provider.
|
||||
// skip the login page and redirect straight to the provider — unless the user
|
||||
// just logged out, in which case we'd immediately re-authenticate them.
|
||||
const justLoggedOut = sessionStorage.getItem(JUST_LOGGED_OUT_KEY) !== null
|
||||
if (justLoggedOut) {
|
||||
sessionStorage.removeItem(JUST_LOGGED_OUT_KEY)
|
||||
return
|
||||
}
|
||||
if (!localAuthEnabled.value && !ldapAuthEnabled.value
|
||||
&& hasOpenIdProviders.value
|
||||
&& openidConnect.value.providers?.length === 1) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue