/* * Wire * Copyright (C) 2018 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import React, {useRef, useState} from 'react'; import {BackendError, SyntheticErrorLabel} from '@wireapp/api-client/lib/http'; import {amplify} from 'amplify'; import {StatusCodes as HTTP_STATUS, StatusCodes} from 'http-status-codes'; import {connect} from 'react-redux'; import {useParams} from 'react-router-dom'; import {AnyAction, Dispatch} from 'redux'; import { ArrowIcon, COLOR, Column, Columns, Container, ContainerXS, H1, IsMobile, Link, Logo, Muted, Overlay, Text, } from '@wireapp/react-ui-kit'; import {WebAppEvents} from '@wireapp/webapp-events'; import {calculateChildWindowPosition} from 'Util/DOM/caculateChildWindowPosition'; import {t} from 'Util/LocalizerUtil'; import {getLogger} from 'Util/Logger'; import {Page} from './Page'; import {SingleSignOnForm} from './SingleSignOnForm'; import {Config} from '../../Config'; import {AppAlreadyOpen} from '../component/AppAlreadyOpen'; import {RouterLink} from '../component/RouterLink'; import {RootState, bindActionCreators} from '../module/reducer'; import * as AuthSelector from '../module/selector/AuthSelector'; import {ROUTE} from '../route'; type Props = React.HTMLAttributes; const logger = getLogger('SingleSignOn'); const SingleSignOnComponent = ({hasDefaultSSOCode}: Props & ConnectedProps & DispatchProps) => { const ssoWindowRef = useRef(); const params = useParams<{code?: string}>(); const [isOverlayOpen, setIsOverlayOpen] = useState(false); const handleSSOWindow = (code: string): Promise => { const POPUP_HEIGHT = 520; const POPUP_WIDTH = 480; const SSO_WINDOW_CLOSE_POLLING_INTERVAL = 1000; return new Promise((resolve, reject) => { let timerId: number = undefined; let onReceiveChildWindowMessage: (event: MessageEvent) => void = undefined; let onParentWindowClose: (event: Event) => void = undefined; const onChildWindowClose = () => { clearInterval(timerId); window.removeEventListener('message', onReceiveChildWindowMessage); window.removeEventListener('unload', onParentWindowClose); setIsOverlayOpen(false); }; onReceiveChildWindowMessage = (event: MessageEvent) => { // We need to copy properties to `JSON.stringify` because `event` is not serializable const serializedEvent = JSON.stringify({data: event.data, origin: event.origin}); logger.log(`Received SSO login event from wrapper: ${serializedEvent}`, event); const isExpectedOrigin = event.origin === Config.getConfig().BACKEND_REST; if (!isExpectedOrigin) { onChildWindowClose(); closeSSOWindow(); return reject( new BackendError( `Origin "${event.origin}" of event "${serializedEvent}" not matching "${ Config.getConfig().BACKEND_REST }"`, SyntheticErrorLabel.SSO_GENERIC_ERROR, HTTP_STATUS.INTERNAL_SERVER_ERROR, ), ); } const eventType = event.data && event.data.type; switch (eventType) { case 'AUTH_SUCCESS': { onChildWindowClose(); closeSSOWindow(); return resolve(); } case 'AUTH_ERROR': case 'AUTH_ERROR_COOKIE': { onChildWindowClose(); closeSSOWindow(); return reject( new BackendError( `Authentication error: "${JSON.stringify(event.data.payload)}"`, event.data.payload.label || SyntheticErrorLabel.SSO_GENERIC_ERROR, HTTP_STATUS.UNAUTHORIZED, ), ); } default: { logger.warn(`Received unmatched event type: "${eventType}"`); } } }; window.addEventListener('message', onReceiveChildWindowMessage, {once: false}); const childPosition = calculateChildWindowPosition(POPUP_HEIGHT, POPUP_WIDTH); ssoWindowRef.current = window.open( `${Config.getConfig().BACKEND_REST}/sso/initiate-login/${code}`, 'WIRE_SSO', ` height=${POPUP_HEIGHT}, left=${childPosition.left} location=no, menubar=no, resizable=no, status=no, toolbar=no, top=${childPosition.top}, width=${POPUP_WIDTH} `, ); setIsOverlayOpen(true); const closeSSOWindow = () => { amplify.publish(WebAppEvents.LIFECYCLE.SSO_WINDOW_CLOSE); ssoWindowRef.current?.close(); }; amplify.subscribe(WebAppEvents.LIFECYCLE.SSO_WINDOW_CLOSED, () => { onChildWindowClose(); reject(new BackendError('', SyntheticErrorLabel.SSO_USER_CANCELLED_ERROR, StatusCodes.INTERNAL_SERVER_ERROR)); }); if (ssoWindowRef.current) { timerId = window.setInterval(() => { if (ssoWindowRef.current && ssoWindowRef.current.closed) { onChildWindowClose(); reject( new BackendError('', SyntheticErrorLabel.SSO_USER_CANCELLED_ERROR, StatusCodes.INTERNAL_SERVER_ERROR), ); } }, SSO_WINDOW_CLOSE_POLLING_INTERVAL); onParentWindowClose = () => { closeSSOWindow(); reject(new BackendError('', SyntheticErrorLabel.SSO_USER_CANCELLED_ERROR, StatusCodes.INTERNAL_SERVER_ERROR)); }; window.addEventListener('unload', onParentWindowClose); } }); }; const focusChildWindow = () => { amplify.publish(WebAppEvents.LIFECYCLE.SSO_WINDOW_FOCUS); ssoWindowRef.current?.focus(); }; const backArrow = ( ); return ( {isOverlayOpen && (
{t('ssoLogin.overlayDescription')} {t('ssoLogin.overlayFocusLink')}
)} {!hasDefaultSSOCode && (
{backArrow}
)} {!hasDefaultSSOCode &&
{backArrow}
}

{t('ssoLogin.headline')}

{Config.getConfig().FEATURE.ENABLE_DOMAIN_DISCOVERY ? ( <> {t('ssoLogin.subheadCodeOrEmail')} {t('ssoLogin.subheadEmailEnvironmentSwitchWarning', { brandName: Config.getConfig().BRAND_NAME, })} ) : ( {t('ssoLogin.subheadCode')} )}
{!hasDefaultSSOCode && (
)} ); }; type ConnectedProps = ReturnType; const mapStateToProps = (state: RootState) => ({ hasDefaultSSOCode: AuthSelector.hasDefaultSSOCode(state), }); type DispatchProps = ReturnType; const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({}, dispatch); const SingleSignOn = connect(mapStateToProps, mapDispatchToProps)(SingleSignOnComponent); export {SingleSignOn};