mattermost-mobile/app/screens/sso/sso.js
Antti Seppälä a52bc4ca2e Do not use WebKit for CookieManager operations in SSO (#2515)
* Do not use WebKit for CookieManager operations in SSO

This allows the SSO login to function again with devices that are not running IOS-11.

* Use WebKit for CookieManager operations only with iOS 11 or later

Detect OS version and use WebKit only when iOS 11 or later is detected.
This allows the SSO login to function again with older devices.
2019-03-22 13:41:06 -03:00

283 lines
8.5 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
InteractionManager,
Text,
View,
Platform,
} from 'react-native';
import {WebView} from 'react-native-webview';
import CookieManager from 'react-native-cookies';
import urlParse from 'url-parse';
import {Client4} from 'mattermost-redux/client';
import {ViewTypes} from 'app/constants';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import tracker from 'app/utils/time_tracker';
const HEADERS = {
'X-Mobile-App': 'mattermost',
};
const postMessageJS = "window.postMessage(document.body.innerText, '*');";
// Used to make sure that OneLogin forms scale appropriately on both platforms.
const oneLoginFormScalingJS = `
(function() {
var loginPage = document.getElementById('login-page');
var submitButton = document.getElementById('user_submit');
if (loginPage) {
loginPage.setAttribute('style', 'background-repeat: repeat-y;');
}
function resetPadding() {
var mainBody = document.getElementById('body-main');
if (mainBody) {
mainBody.setAttribute('style', 'height: auto; padding: 10px 0;');
}
if (submitButton) {
submitButton.removeEventListener('click', resetPadding);
}
}
resetPadding();
if (submitButton) {
submitButton.addEventListener('click', resetPadding);
}
})();
`;
class SSO extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object,
serverUrl: PropTypes.string.isRequired,
ssoType: PropTypes.string.isRequired,
actions: PropTypes.shape({
scheduleExpiredNotification: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
setStoreFromLocalData: PropTypes.func.isRequired,
}).isRequired,
};
useWebkit = true;
constructor(props) {
super(props);
this.state = {
error: null,
renderWebView: false,
jsCode: '',
messagingEnabled: false,
};
switch (props.ssoType) {
case ViewTypes.GITLAB:
this.loginUrl = `${props.serverUrl}/oauth/gitlab/mobile_login`;
this.completedUrl = '/signup/gitlab/complete';
break;
case ViewTypes.SAML:
this.loginUrl = `${props.serverUrl}/login/sso/saml?action=mobile`;
this.completedUrl = '/login/sso/saml';
break;
case ViewTypes.OFFICE365:
this.loginUrl = `${props.serverUrl}/oauth/office365/mobile_login`;
this.completedUrl = '/signup/office365/complete';
break;
}
if (Platform.OS === 'ios') {
this.useWebkit = parseInt(Platform.Version, 10) >= 11;
}
}
componentDidMount() {
InteractionManager.runAfterInteractions(this.clearPreviousCookies);
}
clearPreviousCookies = () => {
CookieManager.clearAll(this.useWebkit).then(() => {
this.setState({renderWebView: true});
});
};
goToChannel = () => {
const {navigator} = this.props;
tracker.initialLoad = Date.now();
this.scheduleSessionExpiredNotification();
navigator.resetTo({
screen: 'Channel',
title: '',
animated: false,
backButtonTitle: '',
navigatorStyle: {
animated: true,
animationType: 'fade',
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
},
});
};
onMessage = (event) => {
try {
const response = JSON.parse(event.nativeEvent.data);
if (response) {
const {
id,
message,
status_code: statusCode,
} = response;
if (id && message && statusCode !== 200) {
this.setState({error: message});
}
}
} catch (e) {
// do nothing
}
};
onNavigationStateChange = (navState) => {
const {url} = navState;
const nextState = {
messagingEnabled: false,
};
const parsed = urlParse(url);
if (parsed.host.includes('.onelogin.com')) {
nextState.jsCode = oneLoginFormScalingJS;
} else if (parsed.pathname === this.completedUrl) {
// To avoid `window.postMessage` conflicts in any of the SSO flows
// we enable the onMessage handler only When the webView navigates to the final SSO URL.
nextState.messagingEnabled = true;
}
this.setState(nextState);
};
onLoadEnd = (event) => {
const url = event.nativeEvent.url;
if (url.includes(this.completedUrl)) {
CookieManager.get(urlParse(url).origin, this.useWebkit).then((res) => {
const token = res.MMAUTHTOKEN;
if (token) {
this.setState({renderWebView: false});
const {
handleSuccessfulLogin,
setStoreFromLocalData,
} = this.props.actions;
Client4.setToken(token);
setStoreFromLocalData({url: Client4.getUrl(), token}).
then(handleSuccessfulLogin).
then(this.goToChannel).
catch(this.onLoadEndError);
} else if (this.webView && !this.state.error) {
this.webView.injectJavaScript(postMessageJS);
}
});
}
};
onLoadEndError = (e) => {
console.warn('Failed to set store from local data', e); // eslint-disable-line no-console
this.setState({error: e.message});
};
scheduleSessionExpiredNotification = () => {
const {actions, intl} = this.props;
actions.scheduleExpiredNotification(intl);
};
renderLoading = () => {
return <Loading/>;
};
webViewRef = (ref) => {
this.webView = ref;
};
render() {
const {theme} = this.props;
const {error, messagingEnabled, renderWebView, jsCode} = this.state;
const style = getStyleSheet(theme);
let content;
if (!renderWebView) {
content = this.renderLoading();
} else if (error) {
content = (
<View style={style.errorContainer}>
<Text style={style.errorText}>{error}</Text>
</View>
);
} else {
content = (
<WebView
ref={this.webViewRef}
source={{uri: this.loginUrl, headers: HEADERS}}
javaScriptEnabledAndroid={true}
automaticallyAdjustContentInsets={false}
startInLoadingState={true}
onNavigationStateChange={this.onNavigationStateChange}
onShouldStartLoadWithRequest={() => true}
renderLoading={this.renderLoading}
injectedJavaScript={jsCode}
onLoadEnd={this.onLoadEnd}
onMessage={messagingEnabled ? this.onMessage : null}
useWebKit={this.useWebkit}
useSharedProcessPool={true}
cacheEnabled={true}
/>
);
}
return (
<View style={style.container}>
<StatusBar/>
{content}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
},
errorContainer: {
alignItems: 'center',
flex: 1,
marginTop: 40,
},
errorText: {
color: changeOpacity(theme.centerChannelColor, 0.4),
fontSize: 16,
fontWeight: '400',
lineHeight: 23,
paddingHorizontal: 30,
},
};
});
export default injectIntl(SSO);