feat: support SSO while embedded (#31002)
* feat: send a message to the parent window if embeddd * feat: handle embedded sso flows * fix: use new `isEmbedded` logic * fix: postMessage targetOrigin --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e02e1aab10
Коммит
a09bee609a
@@ -48,6 +48,7 @@ import PasswordInput from 'components/widgets/inputs/password_input/password_inp
|
|||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
import DesktopApp from 'utils/desktop_api';
|
import DesktopApp from 'utils/desktop_api';
|
||||||
|
import {isEmbedded} from 'utils/embed';
|
||||||
import {t} from 'utils/i18n';
|
import {t} from 'utils/i18n';
|
||||||
import {showNotification} from 'utils/notifications';
|
import {showNotification} from 'utils/notifications';
|
||||||
import {isDesktopApp} from 'utils/user_agent';
|
import {isDesktopApp} from 'utils/user_agent';
|
||||||
@@ -164,7 +165,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
icon: <LoginGitlabIcon/>,
|
icon: <LoginGitlabIcon/>,
|
||||||
label: GitLabButtonText || formatMessage({id: 'login.gitlab', defaultMessage: 'GitLab'}),
|
label: GitLabButtonText || formatMessage({id: 'login.gitlab', defaultMessage: 'GitLab'}),
|
||||||
style: {color: GitLabButtonColor, borderColor: GitLabButtonColor},
|
style: {color: GitLabButtonColor, borderColor: GitLabButtonColor},
|
||||||
onClick: desktopExternalAuth(url),
|
onClick: handleExternalAuth(url, 'gitlab'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
url,
|
url,
|
||||||
icon: <LoginGoogleIcon/>,
|
icon: <LoginGoogleIcon/>,
|
||||||
label: formatMessage({id: 'login.google', defaultMessage: 'Google'}),
|
label: formatMessage({id: 'login.google', defaultMessage: 'Google'}),
|
||||||
onClick: desktopExternalAuth(url),
|
onClick: handleExternalAuth(url, 'google'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +187,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
url,
|
url,
|
||||||
icon: <EntraIdIcon/>,
|
icon: <EntraIdIcon/>,
|
||||||
label: formatMessage({id: 'login.office365', defaultMessage: 'Entra ID'}),
|
label: formatMessage({id: 'login.office365', defaultMessage: 'Entra ID'}),
|
||||||
onClick: desktopExternalAuth(url),
|
onClick: handleExternalAuth(url, 'office365'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
icon: <LoginOpenIDIcon/>,
|
icon: <LoginOpenIDIcon/>,
|
||||||
label: OpenIdButtonText || formatMessage({id: 'login.openid', defaultMessage: 'Open ID'}),
|
label: OpenIdButtonText || formatMessage({id: 'login.openid', defaultMessage: 'Open ID'}),
|
||||||
style: {color: OpenIdButtonColor, borderColor: OpenIdButtonColor},
|
style: {color: OpenIdButtonColor, borderColor: OpenIdButtonColor},
|
||||||
onClick: desktopExternalAuth(url),
|
onClick: handleExternalAuth(url, 'openid'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,21 +210,68 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
|
|||||||
url,
|
url,
|
||||||
icon: <LockIcon/>,
|
icon: <LockIcon/>,
|
||||||
label: SamlLoginButtonText || formatMessage({id: 'login.saml', defaultMessage: 'SAML'}),
|
label: SamlLoginButtonText || formatMessage({id: 'login.saml', defaultMessage: 'SAML'}),
|
||||||
onClick: desktopExternalAuth(url),
|
onClick: handleExternalAuth(url, 'saml'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return externalLoginOptions;
|
return externalLoginOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const desktopExternalAuth = (href: string) => {
|
const handleExternalAuth = (href: string, provider: string) => {
|
||||||
return (event: React.MouseEvent) => {
|
return (event: React.MouseEvent) => {
|
||||||
|
// If the user is running the desktop app, we need to redirect them to the desktop login page
|
||||||
if (isDesktopApp()) {
|
if (isDesktopApp()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
setDesktopLoginLink(href);
|
setDesktopLoginLink(href);
|
||||||
history.push(`/login/desktop${search}`);
|
history.push(`/login/desktop${search}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user is running the app in an embedded view, we need send the parent window a message
|
||||||
|
// to continue the login process if the parent frame answers a message to confirm that is going to
|
||||||
|
// take care for the authentication process.
|
||||||
|
if (isEmbedded()) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Create a promise that will resolve if the parent window responds
|
||||||
|
const messagePromise = new Promise<boolean>((resolve) => {
|
||||||
|
// Set up a one-time event listener for the response
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
// Right now we are embedding from a plugin so let's just check the origin is the same as this server origin.
|
||||||
|
if (event.origin !== window.location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data && event.data.type === 'mattermost_external_auth_login' && event.data.ack === true) {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
// Wait for at least one second for a response from the parent.
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
resolve(false);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify the parent
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'mattermost_external_auth_login',
|
||||||
|
provider,
|
||||||
|
href,
|
||||||
|
}, window.location.origin);
|
||||||
|
|
||||||
|
// Wait for response or timeout, following with the usual authentication flow
|
||||||
|
messagePromise.then((received) => {
|
||||||
|
if (!received) {
|
||||||
|
// If the parent didn't respond, navigate to the href directly
|
||||||
|
history.push(href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
17
webapp/channels/src/utils/embed.tsx
Обычный файл
17
webapp/channels/src/utils/embed.tsx
Обычный файл
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the application is running embedded in another platform
|
||||||
|
* This is determined by checking if window.self is not equal to window.parent.
|
||||||
|
* If accessing window.parent throws an error, it is assumed the app is in a cross-origin iframe.
|
||||||
|
* @returns True if the app is running embedded
|
||||||
|
*/
|
||||||
|
export function isEmbedded(): boolean {
|
||||||
|
try {
|
||||||
|
return window.self !== window.parent;
|
||||||
|
} catch (e) {
|
||||||
|
// If accessing window.parent throws an error, we're in a cross-origin iframe
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user