diff --git a/web/packages/design/src/CardError/CardError.jsx b/web/packages/design/src/CardError/CardError.jsx
index 5ae1ce4d15727..7ae9537f66fb5 100644
--- a/web/packages/design/src/CardError/CardError.jsx
+++ b/web/packages/design/src/CardError/CardError.jsx
@@ -111,6 +111,20 @@ LoginFailed.propTypes = {
loginUrl: PropTypes.string.isRequired,
};
+export const LogoutFailed = ({ message, loginUrl }) => (
+
+
+
+ Return to login.
+
+ }
+ />
+
+);
+
const HyperLink = styled.a`
color: ${({ theme }) => theme.colors.buttons.link.default};
`;
diff --git a/web/packages/design/src/CardError/index.js b/web/packages/design/src/CardError/index.js
index 32416b26dde51..dfb3d91963b94 100644
--- a/web/packages/design/src/CardError/index.js
+++ b/web/packages/design/src/CardError/index.js
@@ -22,7 +22,8 @@ import CardError, {
LoginFailed,
AccessDenied,
NotFound,
+ LogoutFailed,
} from './CardError';
export default CardError;
-export { Failed, LoginFailed, AccessDenied, NotFound, Offline };
+export { Failed, LoginFailed, AccessDenied, NotFound, Offline, LogoutFailed };
diff --git a/web/packages/teleport/src/Console/Console.tsx b/web/packages/teleport/src/Console/Console.tsx
index a5464052fd7d7..698c6eebae84e 100644
--- a/web/packages/teleport/src/Console/Console.tsx
+++ b/web/packages/teleport/src/Console/Console.tsx
@@ -25,6 +25,8 @@ import useAttempt from 'shared/hooks/useAttemptNext';
import AjaxPoller from 'teleport/components/AjaxPoller';
+import { useTeleport } from '..';
+
import { useConsoleContext, useStoreDocs } from './consoleContextProvider';
import * as stores from './stores/types';
import Tabs from './Tabs';
@@ -50,6 +52,7 @@ export default function Console() {
const activeDoc = documents.find(d => d.id === activeDocId);
const hasSshSessions = storeDocs.getSshDocuments().length > 0;
const { attempt, run } = useAttempt();
+ const teleportCtx = useTeleport();
React.useEffect(() => {
run(() => consoleCtx.initStoreUser());
@@ -78,7 +81,7 @@ export default function Console() {
}
function onLogout() {
- consoleCtx.logout();
+ consoleCtx.logout(teleportCtx);
}
const disableNewTab = storeDocs.getNodeDocuments().length > 0;
diff --git a/web/packages/teleport/src/Console/consoleContext.tsx b/web/packages/teleport/src/Console/consoleContext.tsx
index e875bb557f73c..6c36bd3281b0b 100644
--- a/web/packages/teleport/src/Console/consoleContext.tsx
+++ b/web/packages/teleport/src/Console/consoleContext.tsx
@@ -37,6 +37,8 @@ import ClustersService from 'teleport/services/clusters';
import { StoreUserContext } from 'teleport/stores';
import usersService from 'teleport/services/user';
+import TeleportContext from 'teleport/teleportContext';
+
import {
StoreParties,
StoreDocs,
@@ -208,8 +210,8 @@ export default class ConsoleContext {
});
}
- logout() {
- webSession.logout();
+ logout(ctx: TeleportContext) {
+ webSession.logout(false, ctx.storeUser);
}
createTty(session: Session, mode?: ParticipantMode): Tty {
diff --git a/web/packages/teleport/src/Player/Player.tsx b/web/packages/teleport/src/Player/Player.tsx
index 7e5012660098e..d9399945529bb 100644
--- a/web/packages/teleport/src/Player/Player.tsx
+++ b/web/packages/teleport/src/Player/Player.tsx
@@ -31,6 +31,8 @@ import { getUrlParameter } from 'teleport/services/history';
import { RecordingType } from 'teleport/services/recordings';
+import { useTeleport } from '..';
+
import ActionBar from './ActionBar';
import { DesktopPlayer } from './DesktopPlayer';
import SshPlayer from './SshPlayer';
@@ -41,6 +43,7 @@ const validRecordingTypes = ['ssh', 'k8s', 'desktop'];
export function Player() {
const { sid, clusterId } = useParams();
const { search } = useLocation();
+ const ctx = useTeleport();
const recordingType = getUrlParameter(
'recordingType',
@@ -54,7 +57,7 @@ export function Player() {
document.title = `Play ${sid} • ${clusterId}`;
function onLogout() {
- session.logout();
+ session.logout(false, ctx.storeUser);
}
if (!validRecordingType) {
diff --git a/web/packages/teleport/src/SingleLogoutFailed/SingleLogoutFailed.tsx b/web/packages/teleport/src/SingleLogoutFailed/SingleLogoutFailed.tsx
new file mode 100644
index 0000000000000..ff92f79895cf0
--- /dev/null
+++ b/web/packages/teleport/src/SingleLogoutFailed/SingleLogoutFailed.tsx
@@ -0,0 +1,43 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { useLocation } from 'react-router';
+import { LogoutFailed } from 'design/CardError';
+
+import { LogoHero } from 'teleport/components/LogoHero';
+import cfg from 'teleport/config';
+
+export function SingleLogoutFailed() {
+ const { search } = useLocation();
+ const params = new URLSearchParams(search);
+ const connectorName = params.get('connectorName');
+
+ const connectorNameText = connectorName
+ ? connectorName
+ : 'your SAML identity provider.';
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/web/packages/teleport/src/SingleLogoutFailed/index.ts b/web/packages/teleport/src/SingleLogoutFailed/index.ts
new file mode 100644
index 0000000000000..2ea4b7dadc5df
--- /dev/null
+++ b/web/packages/teleport/src/SingleLogoutFailed/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export { SingleLogoutFailed } from './SingleLogoutFailed';
diff --git a/web/packages/teleport/src/Teleport.tsx b/web/packages/teleport/src/Teleport.tsx
index de9525488f507..b5c15090b3d00 100644
--- a/web/packages/teleport/src/Teleport.tsx
+++ b/web/packages/teleport/src/Teleport.tsx
@@ -39,6 +39,7 @@ import { LoginTerminalRedirect } from './Login/LoginTerminalRedirect';
import { LoginClose } from './Login/LoginClose';
import { Login } from './Login';
import { Welcome } from './Welcome';
+import { SingleLogoutFailed } from './SingleLogoutFailed';
import { ConsoleWithContext as Console } from './Console';
import { Player } from './Player';
@@ -143,6 +144,12 @@ export function getSharedPublicRoutes() {
path={cfg.routes.userReset}
render={() => }
/>,
+ ,
];
}
diff --git a/web/packages/teleport/src/components/Authenticated/Authenticated.tsx b/web/packages/teleport/src/components/Authenticated/Authenticated.tsx
index bd02604e5558b..af8fd1860e683 100644
--- a/web/packages/teleport/src/components/Authenticated/Authenticated.tsx
+++ b/web/packages/teleport/src/components/Authenticated/Authenticated.tsx
@@ -53,7 +53,7 @@ const Authenticated: React.FC = ({ children }) => {
const checkIfUserIsAuthenticated = async () => {
if (!session.isValid()) {
logger.warn('invalid session');
- session.logout(true /* rememberLocation */);
+ session.logoutWithoutSlo(true /* rememberLocation */);
return;
}
@@ -66,7 +66,7 @@ const Authenticated: React.FC = ({ children }) => {
} catch (e) {
if (e instanceof ApiError && e.response?.status == 403) {
logger.warn('invalid session');
- session.logout(true /* rememberLocation */);
+ session.logoutWithoutSlo(true /* rememberLocation */);
// No need to update attempt, as `logout` will
// redirect user to login page.
return;
@@ -125,7 +125,7 @@ function startActivityChecker(ttl = 0) {
// ie. browser still openend but all app tabs closed.
if (isInactive(adjustedTtl)) {
logger.warn('inactive session');
- session.logout();
+ session.logoutWithoutSlo();
return;
}
@@ -135,7 +135,7 @@ function startActivityChecker(ttl = 0) {
const intervalId = setInterval(() => {
if (isInactive(adjustedTtl)) {
logger.warn('inactive session');
- session.logout();
+ session.logoutWithoutSlo();
}
}, ACTIVITY_CHECKER_INTERVAL_MS);
diff --git a/web/packages/teleport/src/components/UserMenuNav/UserMenuNav.tsx b/web/packages/teleport/src/components/UserMenuNav/UserMenuNav.tsx
index 3760d962dd923..3e88ee5293ffe 100644
--- a/web/packages/teleport/src/components/UserMenuNav/UserMenuNav.tsx
+++ b/web/packages/teleport/src/components/UserMenuNav/UserMenuNav.tsx
@@ -159,17 +159,6 @@ export function UserMenuNav({ username, iconSize }: UserMenuNavProps) {
transitionDelay += INCREMENT_TRANSITION_DELAY;
}
- function logout() {
- if (ctx.storeUser.getIsSamlSloEnabled()) {
- const sloUrl = ctx.storeUser.getSamlSloUrl();
-
- session.logoutWithoutRedirect();
- window.open(sloUrl);
- } else {
- session.logout();
- }
- }
-
return (
setOpen(!open)} open={open}>
@@ -202,7 +191,9 @@ export function UserMenuNav({ username, iconSize }: UserMenuNavProps) {
)}
- logout()}>
+ session.logout(false, ctx.storeUser)}
+ >
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index 72d5388c2d055..3fd67fc8a3e30 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -171,6 +171,7 @@ const cfg = {
loginError: '/web/msg/error/login',
loginErrorCallback: '/web/msg/error/login/callback',
loginErrorUnauthorized: '/web/msg/error/login/auth',
+ samlSloFailed: '/web/msg/error/slo',
userInvite: '/web/invite/:tokenId',
userInviteContinue: '/web/invite/:tokenId/continue',
userReset: '/web/reset/:tokenId',
diff --git a/web/packages/teleport/src/services/websession/websession.ts b/web/packages/teleport/src/services/websession/websession.ts
index f661a6a268f6d..26a1862fda9cf 100644
--- a/web/packages/teleport/src/services/websession/websession.ts
+++ b/web/packages/teleport/src/services/websession/websession.ts
@@ -23,6 +23,8 @@ import history from 'teleport/services/history';
import api from 'teleport/services/api';
import { KeysEnum, storageService } from 'teleport/services/storageService';
+import { StoreUserContext } from 'teleport/stores';
+
import makeBearerToken from './makeBearerToken';
import { RenewSessionRequest } from './types';
@@ -35,14 +37,31 @@ const logger = Logger.create('services/session');
let sesstionCheckerTimerId = null;
const session = {
- logout(rememberLocation = false) {
+ /** logout logs the user out of Teleport, and if SAML SLO is enabled, also log them out of their IdP. */
+ logout(rememberLocation = false, userCtx: StoreUserContext) {
api.delete(cfg.api.webSessionPath).finally(() => {
- history.goToLogin(rememberLocation);
+ // If SAML SLO (single logout) is enabled, the user will be redirected to the IdP's SLO URL, after which they will automatically be redirected back
+ // to the login page.
+ if (userCtx?.getIsSamlSloEnabled()) {
+ const sloUrl = userCtx.getSamlSloUrl();
+ window.open(sloUrl, '_self');
+ } else {
+ history.goToLogin(rememberLocation);
+ }
});
this.clear();
},
+ /** logoutWithoutSlo logs the user out of Teleport, but not their SAML IdP. */
+ logoutWithoutSlo(rememberLocation = false) {
+ api
+ .delete(cfg.api.webSessionPath)
+ .finally(() => history.goToLogin(rememberLocation));
+
+ this.clear();
+ },
+
logoutWithoutRedirect() {
api.delete(cfg.api.webSessionPath);
@@ -254,7 +273,7 @@ function receiveMessage(event) {
// check if logout was triggered from other tabs
if (storageService.getBearerToken() === null) {
- session.logout();
+ session.logoutWithoutSlo();
}
// check if token is being renewed from another tab