diff --git a/frontend/src/utils/external-login.ts b/frontend/src/utils/external-login.ts new file mode 100644 index 000000000000..8391aa8f8b2e --- /dev/null +++ b/frontend/src/utils/external-login.ts @@ -0,0 +1,42 @@ +export interface ExternalLoginTickets { + oidcTicket: string; + saml2Ticket: string; +} + +const oidcTicketParam = 'oidc_ticket'; +const saml2TicketParam = 'saml2_ticket'; + +const getFragmentParams = (url: URL) => { + return new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash); +}; + +export const parseExternalLoginTickets = (href: string): ExternalLoginTickets => { + const url = new URL(href); + const fragmentParams = getFragmentParams(url); + return { + oidcTicket: fragmentParams.get(oidcTicketParam) || url.searchParams.get(oidcTicketParam) || '', + saml2Ticket: fragmentParams.get(saml2TicketParam) || url.searchParams.get(saml2TicketParam) || '', + }; +}; + +export const hasExternalLoginTicket = () => { + const { oidcTicket, saml2Ticket } = parseExternalLoginTickets(window.location.href); + return Boolean(oidcTicket || saml2Ticket); +}; + +export const takeExternalTicketsFromURL = (): ExternalLoginTickets => { + const tickets = parseExternalLoginTickets(window.location.href); + if (!tickets.oidcTicket && !tickets.saml2Ticket) return tickets; + + const url = new URL(window.location.href); + const fragmentParams = getFragmentParams(url); + fragmentParams.delete(oidcTicketParam); + fragmentParams.delete(saml2TicketParam); + url.searchParams.delete(oidcTicketParam); + url.searchParams.delete(saml2TicketParam); + const sanitizedFragment = fragmentParams.toString(); + url.hash = sanitizedFragment ? `#${sanitizedFragment}` : ''; + const sanitizedURL = `${url.pathname}${url.search}${url.hash}`; + window.history.replaceState(window.history.state, '', sanitizedURL); + return tickets; +}; diff --git a/frontend/src/views/login/components/login-form.vue b/frontend/src/views/login/components/login-form.vue index 5d16638dae55..c8c3d248b675 100644 --- a/frontend/src/views/login/components/login-form.vue +++ b/frontend/src/views/login/components/login-form.vue @@ -379,6 +379,7 @@ import { MenuStore, TabsStore } from '@/store'; import { MsgError, MsgSuccess } from '@/utils/message'; import { useI18n } from 'vue-i18n'; import { encryptPassword, base64UrlToBuffer, bufferToBase64Url } from '@/utils/auth'; +import { takeExternalTicketsFromURL } from '@/utils/external-login'; import { getXpackSettingForTheme } from '@/utils/xpack'; import { routerToName } from '@/utils/router'; import { Key } from '@element-plus/icons-vue'; @@ -388,6 +389,9 @@ import { adjustColorToRGBA } from '@/utils/color'; import { useGlobalStore } from '@/composables/useGlobalStore'; import { submitSAML2Navigation } from '@/utils/saml2'; +const emit = defineEmits<{ + (e: 'external-login-ready'): void; +}>(); const i18n = useI18n(); const { globalStore, @@ -429,22 +433,6 @@ const open = ref(false); const loginBtnLinkColor = ref(null); let loginViewActive = true; -const takeExternalTicketsFromURL = () => { - const url = new URL(window.location.href); - const fragmentParams = new URLSearchParams(url.hash.startsWith('#') ? url.hash.slice(1) : url.hash); - const oidcTicket = fragmentParams.get('oidc_ticket') || url.searchParams.get('oidc_ticket') || ''; - const saml2Ticket = fragmentParams.get('saml2_ticket') || url.searchParams.get('saml2_ticket') || ''; - if (!oidcTicket && !saml2Ticket) return { oidcTicket: '', saml2Ticket: '' }; - fragmentParams.delete('oidc_ticket'); - fragmentParams.delete('saml2_ticket'); - url.searchParams.delete('oidc_ticket'); - url.searchParams.delete('saml2_ticket'); - const sanitizedFragment = fragmentParams.toString(); - url.hash = sanitizedFragment ? `#${sanitizedFragment}` : ''; - const sanitizedURL = `${url.pathname}${url.search}${url.hash}`; - window.history.replaceState(window.history.state, '', sanitizedURL); - return { oidcTicket, saml2Ticket }; -}; const pendingExternalTickets = takeExternalTicketsFromURL(); const pendingOIDCTicket = pendingExternalTickets.oidcTicket; const pendingSAML2Ticket = pendingOIDCTicket ? '' : pendingExternalTickets.saml2Ticket; @@ -596,6 +584,16 @@ const switchToLocalLogin = () => { }); }; +const navigateAfterLogin = async () => { + try { + await routerToName('home'); + } catch { + if (hasPendingExternalTicket) { + emit('external-login-ready'); + } + } +}; + const completeLogin = async (result: LoginModel.ResLogin) => { isLogin.value = true; agreeLicense.value = true; @@ -607,8 +605,8 @@ const completeLogin = async (result: LoginModel.ResLogin) => { MsgSuccess(i18n.t('commons.msg.loginSuccess')); localStorage.removeItem('dashboardCache'); localStorage.removeItem('upgradeChecked'); - routerToName('home'); clearLoginKeydownHandler(); + await navigateAfterLogin(); }; const handleLoginResult = async (result: LoginModel.ResLogin) => { @@ -984,6 +982,9 @@ onMounted(async () => { } else if (pendingSAML2Ticket) { await finishSAML2Login(pendingSAML2Ticket); } + if (hasPendingExternalTicket && !isLogin.value) { + emit('external-login-ready'); + } if (!loginViewActive || isLogin.value) return; if (!isLogin.value && !mfaShow.value) { await Promise.all([loadLDAPStatus(), loadOIDCStatus(), loadSAML2Status()]); diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index 3eeac77bc5e9..b99f4752cafd 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -2,6 +2,13 @@
+
- +
@@ -30,6 +37,7 @@ import LoginForm from './components/login-form.vue'; import { ref, onMounted } from 'vue'; import { useGlobalStore } from '@/composables/useGlobalStore'; import { preloadImage } from '@/utils/browser'; +import { hasExternalLoginTicket } from '@/utils/external-login'; defineOptions({ name: 'Login' }); const { entrance, isEnterprise, themeConfig } = useGlobalStore(); const backgroundOpacity = ref(1); @@ -42,6 +50,8 @@ const backgroundStyle = ref<{ backgroundImage?: string; backgroundColor?: string const imgLoaded = ref(false); const currentDefaultLoginImage = computed(() => (isEnterprise.value ? defaultEnterpriseLoginImage : defaultLoginImage)); +const externalLoginPending = ref(hasExternalLoginTicket()); + function onImgLoad() { imgLoaded.value = true; }