Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions frontend/src/utils/external-login.ts
Original file line number Diff line number Diff line change
@@ -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;
};
35 changes: 18 additions & 17 deletions frontend/src/views/login/components/login-form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -429,22 +433,6 @@ const open = ref(false);
const loginBtnLinkColor = ref<string | null>(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;
Expand Down Expand Up @@ -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;
Expand All @@ -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();
};
Comment on lines 606 to 610

const handleLoginResult = async (result: LoginModel.ResLogin) => {
Expand Down Expand Up @@ -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()]);
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/views/login/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<div class="flex items-center justify-center min-h-screen relative bg-gray-100">
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat" :style="backgroundStyle"></div>
<div
v-if="externalLoginPending"
v-loading="true"
class="absolute inset-0 z-20 bg-gray-100"
aria-busy="true"
></div>
<div
v-show="!externalLoginPending"
:style="{ opacity: backgroundOpacity, width: containerWidth, height: containerHeight }"
class="bg-white shadow-lg relative z-10 border border-gray-200 flex overflow-hidden"
id="login-container"
Expand All @@ -18,7 +25,7 @@
/>
</div>
<div :class="loginFormClass">
<LoginForm ref="loginRef"></LoginForm>
<LoginForm ref="loginRef" @external-login-ready="externalLoginPending = false"></LoginForm>
</div>
</div>
</div>
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down
Loading