Skip to content

Commit

Permalink
Add support for SAML SLO in the WebUI (#43071) (#43449)
Browse files Browse the repository at this point in the history
  • Loading branch information
rudream authored Jun 26, 2024
1 parent 743809e commit f582e41
Show file tree
Hide file tree
Showing 18 changed files with 1,977 additions and 1,649 deletions.
5 changes: 5 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3484,6 +3484,9 @@ message ExternalIdentity {

// Username is username supplied by external identity provider
string Username = 2 [(gogoproto.jsontag) = "username,omitempty"];

// SAMLSingleLogoutURL is the SAML Single log-out URL to initiate SAML SLO (single log-out), if applicable.
string SAMLSingleLogoutURL = 3 [(gogoproto.jsontag) = "samlSingleLogoutUrl,omitempty"];
}

// LoginStatus is a login status of the user
Expand Down Expand Up @@ -4533,6 +4536,8 @@ message SAMLConnectorSpecV2 {
// ClientRedirectSettings defines which client redirect URLs are allowed for
// non-browser SSO logins other than the standard localhost ones.
SSOClientRedirectSettings ClientRedirectSettings = 15 [(gogoproto.jsontag) = "client_redirect_settings,omitempty"];
// SingleLogoutURL is the SAML Single log-out URL to initiate SAML SLO (single log-out). If this is not provided, SLO is disabled.
string SingleLogoutURL = 16 [(gogoproto.jsontag) = "single_logout_url,omitempty"];
}

// SAMLAuthRequest is a request to authenticate with SAML
Expand Down
14 changes: 14 additions & 0 deletions api/types/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ type SAMLConnector interface {
SetAllowIDPInitiated(bool)
// GetClientRedirectSettings returns the client redirect settings.
GetClientRedirectSettings() *SSOClientRedirectSettings
// GetSingleLogoutURL returns the SAML SLO (single logout) URL for the identity provider.
GetSingleLogoutURL() string
// SetSingleLogoutURL sets the SAML SLO (single logout) URL for the identity provider.
SetSingleLogoutURL(string)
}

// NewSAMLConnector returns a new SAMLConnector based off a name and SAMLConnectorSpecV2.
Expand Down Expand Up @@ -377,6 +381,16 @@ func (o *SAMLConnectorV2) GetClientRedirectSettings() *SSOClientRedirectSettings
return o.Spec.ClientRedirectSettings
}

// GetSingleLogoutURL returns the SAML SLO (single logout) URL for the identity provider.
func (o *SAMLConnectorV2) GetSingleLogoutURL() string {
return o.Spec.SingleLogoutURL
}

// SetSingleLogoutURL sets the SAML SLO (single logout) URL for the identity provider.
func (o *SAMLConnectorV2) SetSingleLogoutURL(url string) {
o.Spec.SingleLogoutURL = url
}

// setStaticFields sets static resource header and metadata fields.
func (o *SAMLConnectorV2) setStaticFields() {
o.Kind = KindSAMLConnector
Expand Down
3,379 changes: 1,737 additions & 1,642 deletions api/types/types.pb.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ spec:
description: PrivateKey is a PEM encoded x509 private key.
type: string
type: object
single_logout_url:
description: SingleLogoutURL is the SAML Single log-out URL to initiate
SAML SLO (single log-out). If this is not provided, SLO is disabled.
type: string
sso:
description: SSO is the URL of the identity provider's SSO service.
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ spec:
description: ConnectorID is id of registered OIDC connector,
e.g. 'google-example.com'
type: string
samlSingleLogoutUrl:
description: SAMLSingleLogoutURL is the SAML Single log-out
URL to initiate SAML SLO (single log-out), if applicable.
type: string
username:
description: Username is username supplied by external identity
provider
Expand All @@ -68,6 +72,10 @@ spec:
description: ConnectorID is id of registered OIDC connector,
e.g. 'google-example.com'
type: string
samlSingleLogoutUrl:
description: SAMLSingleLogoutURL is the SAML Single log-out
URL to initiate SAML SLO (single log-out), if applicable.
type: string
username:
description: Username is username supplied by external identity
provider
Expand All @@ -89,6 +97,10 @@ spec:
description: ConnectorID is id of registered OIDC connector,
e.g. 'google-example.com'
type: string
samlSingleLogoutUrl:
description: SAMLSingleLogoutURL is the SAML Single log-out
URL to initiate SAML SLO (single log-out), if applicable.
type: string
username:
description: Username is username supplied by external identity
provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ spec:
description: PrivateKey is a PEM encoded x509 private key.
type: string
type: object
single_logout_url:
description: SingleLogoutURL is the SAML Single log-out URL to initiate
SAML SLO (single log-out). If this is not provided, SLO is disabled.
type: string
sso:
description: SSO is the URL of the identity provider's SSO service.
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ spec:
description: ConnectorID is id of registered OIDC connector,
e.g. 'google-example.com'
type: string
samlSingleLogoutUrl:
description: SAMLSingleLogoutURL is the SAML Single log-out
URL to initiate SAML SLO (single log-out), if applicable.
type: string
username:
description: Username is username supplied by external identity
provider
Expand All @@ -68,6 +72,10 @@ spec:
description: ConnectorID is id of registered OIDC connector,
e.g. 'google-example.com'
type: string
samlSingleLogoutUrl:
description: SAMLSingleLogoutURL is the SAML Single log-out
URL to initiate SAML SLO (single log-out), if applicable.
type: string
username:
description: Username is username supplied by external identity
provider
Expand All @@ -89,6 +97,10 @@ spec:
description: ConnectorID is id of registered OIDC connector,
e.g. 'google-example.com'
type: string
samlSingleLogoutUrl:
description: SAMLSingleLogoutURL is the SAML Single log-out
URL to initiate SAML SLO (single log-out), if applicable.
type: string
username:
description: Username is username supplied by external identity
provider
Expand Down
6 changes: 6 additions & 0 deletions lib/client/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ const (
// failing (such as an unmet hardware key policy) and the first should be
// ignored.
LoginClose = "/web/msg/info/login_close"

// SAMLSingleLogoutFailedRedirectURL is the default redirect URL when an error was encountered during SAML Single Logout.
SAMLSingleLogoutFailedRedirectURL = "/web/msg/error/slo"

// DefaultLoginURL is the default login page.
DefaultLoginURL = "/web/login"
)

// Redirector handles SSH redirect flow with the Teleport server
Expand Down
26 changes: 26 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2166,11 +2166,37 @@ func (h *Handler) deleteWebSession(w http.ResponseWriter, r *http.Request, _ htt
}
}

clt, err := ctx.GetClient()
if err != nil {
h.log.
WithError(err).
Warnf("Failed to retrieve user client, SAML single logout will be skipped for user %s.", ctx.GetUser())
}

var user types.User
// Only run this if we successfully retrieved the client.
if err == nil {
user, err = clt.GetUser(r.Context(), ctx.GetUser(), false)
if err != nil {
h.log.
WithError(err).
Warnf("Failed to retrieve user during logout, SAML single logout will be skipped for user %s.", ctx.GetUser())
}
}

err = h.logout(r.Context(), w, ctx)
if err != nil {
return nil, trace.Wrap(err)
}

// If the user has SAML SLO (single logout) configured, return a redirect link to the SLO URL.
if user != nil && len(user.GetSAMLIdentities()) > 0 && user.GetSAMLIdentities()[0].SAMLSingleLogoutURL != "" {
// The WebUI will redirect the user to this URL to initiate the SAML SLO on the IdP side. This is safe because this URL
// is hard-coded in the auth connector and can't be modified by the end user. Additionally, the user's Teleport session has already
// been invalidated by this point so there is nothing to hijack.
return map[string]interface{}{"samlSloUrl": user.GetSAMLIdentities()[0].SAMLSingleLogoutURL}, nil
}

return OK(), nil
}

Expand Down
14 changes: 14 additions & 0 deletions web/packages/design/src/CardError/CardError.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ LoginFailed.propTypes = {
loginUrl: PropTypes.string.isRequired,
};

export const LogoutFailed = ({ message, loginUrl }) => (
<CardError>
<Header>Logout Unsuccessful</Header>
<Content
message={message}
desc={
<Text typography="paragraph" textAlign="center">
<HyperLink href={loginUrl}>Return to login.</HyperLink>
</Text>
}
/>
</CardError>
);

const HyperLink = styled.a`
color: ${({ theme }) => theme.colors.buttons.link.default};
`;
3 changes: 2 additions & 1 deletion web/packages/design/src/CardError/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import { Router } from 'react-router';
import { createMemoryHistory } from 'history';

import { SingleLogoutFailed } from './SingleLogoutFailed';

export default {
title: 'Teleport/LogoutError',
};

export const FailedDefault = () => {
const history = createMemoryHistory({
initialEntries: ['/web/msg/error/slo'],
initialIndex: 0,
});

return (
<Router history={history}>
<SingleLogoutFailed />
</Router>
);
};

export const FailedOkta = () => {
const history = createMemoryHistory({
initialEntries: ['/web/msg/error/slo?connectorName=Okta'],
initialIndex: 0,
});

return (
<Router history={history}>
<SingleLogoutFailed />
</Router>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/

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 || 'your SAML identity provider';
return (
<>
<LogoHero />
<LogoutFailed
loginUrl={cfg.routes.login}
message={`You have been logged out of Teleport, but we were unable to log you out of ${connectorNameText}. See the Teleport logs for details.`}
/>
</>
);
}
19 changes: 19 additions & 0 deletions web/packages/teleport/src/SingleLogoutFailed/index.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

export { SingleLogoutFailed } from './SingleLogoutFailed';
7 changes: 7 additions & 0 deletions web/packages/teleport/src/Teleport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,6 +144,12 @@ export function getSharedPublicRoutes() {
path={cfg.routes.userReset}
render={() => <Welcome NewCredentials={NewCredentials} />}
/>,
<Route
key="saml-slo-failed"
title="SAML Single Logout Failed"
path={cfg.routes.samlSloFailed}
component={SingleLogoutFailed}
/>,
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const Authenticated: React.FC<PropsWithChildren> = ({ children }) => {
const checkIfUserIsAuthenticated = async () => {
if (!session.isValid()) {
logger.warn('invalid session');
session.logout(true /* rememberLocation */);
session.clearBrowserSession(true /* rememberLocation */);
return;
}

Expand All @@ -66,7 +66,7 @@ const Authenticated: React.FC<PropsWithChildren> = ({ children }) => {
} catch (e) {
if (e instanceof ApiError && e.response?.status == 403) {
logger.warn('invalid session');
session.logout(true /* rememberLocation */);
session.clearBrowserSession(true /* rememberLocation */);
// No need to update attempt, as `logout` will
// redirect user to login page.
return;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);

Expand Down
1 change: 1 addition & 0 deletions web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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',
Expand Down
Loading

0 comments on commit f582e41

Please sign in to comment.