OAuth through OS browser, added google login & open id methods (#5075)
* Initialised SSO with linking * Fixed lint errors * Added translation and version check * Added translation & detox file: WIP * Added google logo and login button * Added openid login option * Added tests * Added fallback support for google * Added server check for openid * Removed unwated memos * Server support changed to 5.32 * Added try catch for Linking openurl * Removed 'or' * Changed test case version to 5.32 * Changed server version to 5.32 * Update assets/base/i18n/en.json Co-authored-by: Katie Wiersgalla <39744472+wiersgallak@users.noreply.github.com> * Update assets/base/i18n/en.json Co-authored-by: Katie Wiersgalla <39744472+wiersgallak@users.noreply.github.com> * Update assets/base/i18n/en.json Co-authored-by: Katie Wiersgalla <39744472+wiersgallak@users.noreply.github.com> * Update app/screens/sso/sso_with_redirect_url.tsx Co-authored-by: Hossein <hahmadia@users.noreply.github.com> * Update assets/base/i18n/en.json Co-authored-by: Katie Wiersgalla <39744472+wiersgallak@users.noreply.github.com> * Added new custom url scheme for auth * Added custom url scheme based on app build type, custom user agent for only google & misc typo fixes * Added check for Deviceinfo * Reverted to openLink * Using tryOpenURL instead of Linking.openURL * Changed office365 url * Handling cookie read error caused on localhost * Fixed lint errors * Updated version in tests Co-authored-by: Anurag Shivarathri <anurag@13.local> Co-authored-by: Katie Wiersgalla <39744472+wiersgallak@users.noreply.github.com> Co-authored-by: Hossein <hahmadia@users.noreply.github.com>
This commit is contained in:
parent
5197332d02
commit
d11d613815
18 changed files with 851 additions and 342 deletions
|
|
@ -43,6 +43,12 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mattermost" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mmauthbeta" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
<service android:name=".NotificationDismissService"
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ const ViewTypes = keyMirror({
|
|||
REMOVE_LAST_CHANNEL_FOR_TEAM: null,
|
||||
|
||||
GITLAB: null,
|
||||
GOOGLE: null,
|
||||
OFFICE365: null,
|
||||
OPENID: null,
|
||||
SAML: null,
|
||||
|
||||
SET_INITIAL_POST_VISIBILITY: null,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export type Config = {
|
|||
EnableSignUpWithGitLab: string;
|
||||
EnableSignUpWithGoogle: string;
|
||||
EnableSignUpWithOffice365: string;
|
||||
EnableSignUpWithOpenId: string;
|
||||
EnableSVGs: string;
|
||||
EnableTesting: string;
|
||||
EnableThemeSelection: string;
|
||||
|
|
@ -128,6 +129,8 @@ export type Config = {
|
|||
MaxFileSize: string;
|
||||
MaxNotificationsPerChannel: string;
|
||||
MinimumHashtagLength: string;
|
||||
OpenIdButtonColor: string;
|
||||
OpenIdButtonText: string;
|
||||
PasswordMinimumLength: string;
|
||||
PasswordRequireLowercase: string;
|
||||
PasswordRequireNumber: string;
|
||||
|
|
|
|||
1
app/mm-redux/types/module.d.ts
vendored
1
app/mm-redux/types/module.d.ts
vendored
|
|
@ -5,3 +5,4 @@ declare module 'remote-redux-devtools';
|
|||
declare module 'redux-action-buffer';
|
||||
declare module 'react-intl';
|
||||
declare module 'react-native-local-auth';
|
||||
declare module 'react-native-cookies';
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
|||
import {goToScreen} from '@actions/navigation';
|
||||
import LocalConfig from '@assets/config';
|
||||
import gitlab from '@assets/images/gitlab.png';
|
||||
import google from '@assets/images/google.png';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import {ViewTypes} from '@constants';
|
||||
import globalEventHandler from '@init/global_event_handler';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import {GlobalStyles} from 'app/styles';
|
||||
|
|
@ -105,14 +107,14 @@ export default class LoginOptions extends PureComponent {
|
|||
|
||||
if (!forceHideFromLocal && license.IsLicensed === 'true' && config.EnableLdap === 'true') {
|
||||
const backgroundColor = config.LDAPLoginButtonColor || '#2389d7';
|
||||
const additionalStyle = {
|
||||
const additionalButtonStyle = {
|
||||
backgroundColor,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
};
|
||||
|
||||
if (config.LDAPLoginButtonBorderColor) {
|
||||
additionalStyle.borderColor = config.LDAPLoginButtonBorderColor;
|
||||
additionalButtonStyle.borderColor = config.LDAPLoginButtonBorderColor;
|
||||
}
|
||||
|
||||
const textColor = config.LDAPLoginButtonTextColor || 'white';
|
||||
|
|
@ -138,7 +140,7 @@ export default class LoginOptions extends PureComponent {
|
|||
<Button
|
||||
key='ldap'
|
||||
onPress={this.goToLogin}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalStyle]}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalButtonStyle]}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
|
|
@ -154,18 +156,31 @@ export default class LoginOptions extends PureComponent {
|
|||
const forceHideFromLocal = LocalConfig.HideGitLabLoginExperimental;
|
||||
|
||||
if (!forceHideFromLocal && config.EnableSignUpWithGitLab === 'true') {
|
||||
const additionalButtonStyle = {
|
||||
backgroundColor: '#548',
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
};
|
||||
|
||||
const logoStyle = {
|
||||
height: 18,
|
||||
marginRight: 5,
|
||||
width: 18,
|
||||
};
|
||||
|
||||
const textColor = 'white';
|
||||
return (
|
||||
<Button
|
||||
key='gitlab'
|
||||
onPress={preventDoubleTap(() => this.goToSSO(ViewTypes.GITLAB))}
|
||||
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#548', borderColor: 'transparent', borderWidth: 0}]}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalButtonStyle]}
|
||||
>
|
||||
<Image
|
||||
source={gitlab}
|
||||
style={{height: 18, marginRight: 5, width: 18}}
|
||||
style={logoStyle}
|
||||
/>
|
||||
<Text
|
||||
style={[GlobalStyles.signupButtonText, {color: 'white'}]}
|
||||
style={[GlobalStyles.signupButtonText, {color: textColor}]}
|
||||
>
|
||||
{'GitLab'}
|
||||
</Text>
|
||||
|
|
@ -176,15 +191,53 @@ export default class LoginOptions extends PureComponent {
|
|||
return null;
|
||||
};
|
||||
|
||||
renderGoogleOption = () => {
|
||||
const {config} = this.props;
|
||||
|
||||
if (config.EnableSignUpWithGoogle === 'true') {
|
||||
const additionalButtonStyle = {
|
||||
backgroundColor: '#c23321',
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
};
|
||||
|
||||
const logoStyle = {
|
||||
height: 18,
|
||||
marginRight: 5,
|
||||
width: 18,
|
||||
};
|
||||
|
||||
const textColor = 'white';
|
||||
return (
|
||||
<Button
|
||||
key='google'
|
||||
onPress={preventDoubleTap(() => this.goToSSO(ViewTypes.GOOGLE))}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalButtonStyle]}
|
||||
>
|
||||
<Image
|
||||
source={google}
|
||||
style={logoStyle}
|
||||
/>
|
||||
<FormattedText
|
||||
id='signup.google'
|
||||
defaultMessage='Google Apps'
|
||||
style={[GlobalStyles.signupButtonText, {color: textColor}]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderO365Option = () => {
|
||||
const {config, license} = this.props;
|
||||
const forceHideFromLocal = LocalConfig.HideO365LoginExperimental;
|
||||
const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && license.IsLicensed === 'true' && license.Office365OAuth === 'true';
|
||||
|
||||
if (!forceHideFromLocal && o365Enabled) {
|
||||
const backgroundColor = '#2389d7';
|
||||
const additionalStyle = {
|
||||
backgroundColor,
|
||||
const additionalButtonStyle = {
|
||||
backgroundColor: '#2389d7',
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
};
|
||||
|
|
@ -195,7 +248,7 @@ export default class LoginOptions extends PureComponent {
|
|||
<Button
|
||||
key='o365'
|
||||
onPress={preventDoubleTap(() => this.goToSSO(ViewTypes.OFFICE365))}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalStyle]}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalButtonStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='signup.office365'
|
||||
|
|
@ -209,6 +262,37 @@ export default class LoginOptions extends PureComponent {
|
|||
return null;
|
||||
};
|
||||
|
||||
renderOpenIdOption = () => {
|
||||
const {config, license} = this.props;
|
||||
const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && license.IsLicensed === 'true' && isMinimumServerVersion(config.Version, 5, 33, 0);
|
||||
|
||||
if (openIdEnabled) {
|
||||
const additionalButtonStyle = {
|
||||
backgroundColor: config.OpenIdButtonColor || '#145DBF',
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
};
|
||||
|
||||
const textColor = 'white';
|
||||
|
||||
return (
|
||||
<Button
|
||||
key='openId'
|
||||
onPress={preventDoubleTap(() => this.goToSSO(ViewTypes.OPENID))}
|
||||
containerStyle={[GlobalStyles.signupButton, additionalButtonStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='signup.openid'
|
||||
defaultMessage={config.OpenIdButtonText || 'OpenID'}
|
||||
style={[GlobalStyles.signupButtonText, {color: textColor}]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
renderSamlOption = () => {
|
||||
const {config, license} = this.props;
|
||||
const forceHideFromLocal = LocalConfig.HideSAMLLoginExperimental;
|
||||
|
|
@ -279,8 +363,10 @@ export default class LoginOptions extends PureComponent {
|
|||
{this.renderEmailOption()}
|
||||
{this.renderLdapOption()}
|
||||
{this.renderGitlabOption()}
|
||||
{this.renderGoogleOption()}
|
||||
{this.renderSamlOption()}
|
||||
{this.renderO365Option()}
|
||||
{this.renderOpenIdOption()}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
63
app/screens/login_options/login_options.test.js
Normal file
63
app/screens/login_options/login_options.test.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import LoginOptions from './login_options';
|
||||
|
||||
describe('Login options', () => {
|
||||
const baseProps = {
|
||||
config: {
|
||||
Version: '5.32.0',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
test('should show google signin button only when enabled', () => {
|
||||
const basicWrapper = shallowWithIntl(<LoginOptions {...baseProps}/>);
|
||||
expect(basicWrapper.find(FormattedText).find({id: 'signup.google'}).exists()).toBe(false);
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
config: {
|
||||
...baseProps.config,
|
||||
EnableSignUpWithGoogle: 'true',
|
||||
},
|
||||
};
|
||||
const configuredWrapper = shallowWithIntl(<LoginOptions {...props}/>);
|
||||
expect(configuredWrapper.find(FormattedText).find({id: 'signup.google'}).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should show open id button only when enabled and from version 5.33', () => {
|
||||
const basicWrapper = shallowWithIntl(<LoginOptions {...baseProps}/>);
|
||||
expect(basicWrapper.find(FormattedText).find({id: 'signup.openid'}).exists()).toBe(false);
|
||||
|
||||
const newVersionProps = {
|
||||
...baseProps,
|
||||
config: {
|
||||
...baseProps.config,
|
||||
EnableSignUpWithOpenId: 'true',
|
||||
Version: '5.33.0',
|
||||
},
|
||||
};
|
||||
const newVersionWrapper = shallowWithIntl(<LoginOptions {...newVersionProps}/>);
|
||||
expect(newVersionWrapper.find(FormattedText).find({id: 'signup.openid'}).exists()).toBe(true);
|
||||
|
||||
const oldVersionProps = {
|
||||
...baseProps,
|
||||
config: {
|
||||
...baseProps.config,
|
||||
EnableSignUpWithOpenId: 'true',
|
||||
Version: '5.30.0',
|
||||
},
|
||||
};
|
||||
const oldVersionWrapper = shallowWithIntl(<LoginOptions {...oldVersionProps}/>);
|
||||
expect(oldVersionWrapper.find(FormattedText).find({id: 'signup.openid'}).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -34,6 +34,7 @@ import FormattedText from '@components/formatted_text';
|
|||
import fetchConfig from '@init/fetch';
|
||||
import globalEventHandler from '@init/global_event_handler';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
|
@ -255,10 +256,12 @@ export default class SelectServer extends PureComponent {
|
|||
const {config, license} = props;
|
||||
const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true';
|
||||
const gitlabEnabled = config.EnableSignUpWithGitLab === 'true';
|
||||
const googleEnabled = config.EnableSignUpWithGoogle === 'true' && license.IsLicensed === 'true';
|
||||
const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && license.IsLicensed === 'true' && license.Office365OAuth === 'true';
|
||||
const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && license.IsLicensed === 'true' && isMinimumServerVersion(config.Version, 5, 33, 0);
|
||||
|
||||
let options = 0;
|
||||
if (samlEnabled || gitlabEnabled || o365Enabled) {
|
||||
if (samlEnabled || gitlabEnabled || googleEnabled || o365Enabled || openIdEnabled) {
|
||||
options += 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {ssoLogin} from '@actions/views/user';
|
||||
import {scheduleExpiredNotification} from '@actions/views/session';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import SSO from './sso';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
...state.views.selectServer,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
scheduleExpiredNotification,
|
||||
ssoLogin,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SSO);
|
||||
130
app/screens/sso/index.tsx
Normal file
130
app/screens/sso/index.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {shallowEqual, useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
import {resetToChannel} from 'app/actions/navigation';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {scheduleExpiredNotification} from '@actions/views/session';
|
||||
import {ssoLogin} from '@actions/views/user';
|
||||
import {DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {ErrorApi} from '@mm-redux/types/client4';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
import SSOWithRedirectURL from './sso_with_redirect_url';
|
||||
import SSOWithWebView from './sso_with_webview';
|
||||
|
||||
interface SSOProps {
|
||||
intl: typeof intlShape;
|
||||
ssoType: string;
|
||||
}
|
||||
|
||||
function SSO({intl, ssoType}: SSOProps) {
|
||||
const [config, serverUrl, theme] = useSelector((state: GlobalState) => ([
|
||||
getConfig(state),
|
||||
state.views.selectServer.serverUrl,
|
||||
getTheme(state),
|
||||
]), shallowEqual);
|
||||
|
||||
const [loginError, setLoginError] = React.useState<string>('');
|
||||
|
||||
const asyncDispatch: DispatchFunc = useDispatch();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
let completeUrlPath = '';
|
||||
let loginUrl = '';
|
||||
switch (ssoType) {
|
||||
case ViewTypes.GOOGLE: {
|
||||
completeUrlPath = '/signup/google/complete';
|
||||
loginUrl = `${serverUrl}/oauth/google/mobile_login`;
|
||||
break;
|
||||
}
|
||||
case ViewTypes.GITLAB: {
|
||||
completeUrlPath = '/signup/gitlab/complete';
|
||||
loginUrl = `${serverUrl}/oauth/gitlab/mobile_login`;
|
||||
break;
|
||||
}
|
||||
case ViewTypes.SAML: {
|
||||
completeUrlPath = '/login/sso/saml';
|
||||
loginUrl = `${serverUrl}/login/sso/saml?action=mobile`;
|
||||
break;
|
||||
}
|
||||
case ViewTypes.OFFICE365: {
|
||||
completeUrlPath = '/signup/office365/complete';
|
||||
loginUrl = `${serverUrl}/oauth/office365/mobile_login`;
|
||||
break;
|
||||
}
|
||||
case ViewTypes.OPENID: {
|
||||
completeUrlPath = '/signup/openid/complete';
|
||||
loginUrl = `${serverUrl}/oauth/openid/mobile_login`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const onLoadEndError = (e: ErrorApi) => {
|
||||
console.warn('Failed to set store from local data', e); // eslint-disable-line no-console
|
||||
let errorMessage = e.message;
|
||||
if (e.url) {
|
||||
errorMessage += `\nURL: ${e.url}`;
|
||||
}
|
||||
setLoginError(errorMessage);
|
||||
};
|
||||
|
||||
const onMMToken = async (token: string) => {
|
||||
Client4.setToken(token);
|
||||
asyncDispatch(ssoLogin()).then((result: any) => {
|
||||
if (result && result.error) {
|
||||
onLoadEndError(result.error);
|
||||
return;
|
||||
}
|
||||
goToChannel();
|
||||
}).catch(() => {
|
||||
setLoginError('');
|
||||
});
|
||||
};
|
||||
|
||||
const goToChannel = () => {
|
||||
tracker.initialLoad = Date.now();
|
||||
scheduleSessionExpiredNotification();
|
||||
resetToChannel();
|
||||
};
|
||||
|
||||
const scheduleSessionExpiredNotification = () => {
|
||||
dispatch(scheduleExpiredNotification(intl));
|
||||
};
|
||||
|
||||
const isSSOWithRedirectURLAvailable = isMinimumServerVersion(config.Version, 5, 33, 0);
|
||||
|
||||
const props = {
|
||||
intl,
|
||||
loginError,
|
||||
loginUrl,
|
||||
onCSRFToken: Client4.setCSRF,
|
||||
onMMToken,
|
||||
setLoginError,
|
||||
theme,
|
||||
};
|
||||
|
||||
if (isSSOWithRedirectURLAvailable) {
|
||||
return (
|
||||
<SSOWithRedirectURL {...props}/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SSOWithWebView
|
||||
{...props}
|
||||
completeUrlPath={completeUrlPath}
|
||||
serverUrl={serverUrl}
|
||||
ssoType={ssoType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(injectIntl(SSO));
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
// 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 {
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {WebView} from 'react-native-webview';
|
||||
import CookieManager from 'react-native-cookies';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import Loading from 'app/components/loading';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {resetToChannel} from 'app/actions/navigation';
|
||||
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 = {
|
||||
actions: PropTypes.shape({
|
||||
scheduleExpiredNotification: PropTypes.func.isRequired,
|
||||
ssoLogin: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
serverUrl: PropTypes.string.isRequired,
|
||||
ssoType: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
useWebkit = true;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
renderWebView: true,
|
||||
jsCode: '',
|
||||
messagingEnabled: false,
|
||||
};
|
||||
|
||||
switch (props.ssoType) {
|
||||
case ViewTypes.GITLAB:
|
||||
this.loginUrl = `${props.serverUrl}/oauth/gitlab/mobile_login`;
|
||||
this.completeUrlPath = '/signup/gitlab/complete';
|
||||
break;
|
||||
case ViewTypes.SAML:
|
||||
this.loginUrl = `${props.serverUrl}/login/sso/saml?action=mobile`;
|
||||
this.completeUrlPath = '/login/sso/saml';
|
||||
break;
|
||||
case ViewTypes.OFFICE365:
|
||||
this.loginUrl = `${props.serverUrl}/oauth/office365/mobile_login`;
|
||||
this.completeUrlPath = '/signup/office365/complete';
|
||||
break;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
this.useWebkit = parseInt(Platform.Version, 10) >= 11;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.cookiesTimeout);
|
||||
}
|
||||
|
||||
extractCookie = (parsedUrl) => {
|
||||
const original = urlParse(this.props.serverUrl);
|
||||
|
||||
// Check whether we need to set a sub-path
|
||||
parsedUrl.set('pathname', original.pathname || '');
|
||||
|
||||
// Rebuild the server url without query string and/or hash
|
||||
const url = `${parsedUrl.origin}${parsedUrl.pathname}`;
|
||||
Client4.setUrl(url);
|
||||
|
||||
CookieManager.get(url, true).then((res) => {
|
||||
const mmtoken = res.MMAUTHTOKEN;
|
||||
const csrf = res.MMCSRF;
|
||||
const token = typeof mmtoken === 'object' ? mmtoken.value : mmtoken;
|
||||
const csrfToken = typeof csrf === 'object' ? csrf.value : csrf;
|
||||
|
||||
if (csrfToken) {
|
||||
Client4.setCSRF(csrfToken);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
clearTimeout(this.cookiesTimeout);
|
||||
this.setState({renderWebView: false});
|
||||
const {
|
||||
ssoLogin,
|
||||
} = this.props.actions;
|
||||
|
||||
Client4.setToken(token);
|
||||
ssoLogin().then((result) => {
|
||||
if (result.error) {
|
||||
this.onLoadEndError(result.error);
|
||||
return;
|
||||
}
|
||||
this.goToChannel();
|
||||
});
|
||||
} else if (this.webView && !this.state.error) {
|
||||
this.webView.injectJavaScript(postMessageJS);
|
||||
this.cookiesTimeout = setTimeout(this.extractCookie.bind(null, parsedUrl), 250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToChannel = () => {
|
||||
tracker.initialLoad = Date.now();
|
||||
|
||||
this.scheduleSessionExpiredNotification();
|
||||
|
||||
resetToChannel();
|
||||
};
|
||||
|
||||
onMessage = (event) => {
|
||||
try {
|
||||
const response = JSON.parse(event.nativeEvent.data);
|
||||
if (response) {
|
||||
const {
|
||||
id,
|
||||
message,
|
||||
status_code: statusCode,
|
||||
} = response;
|
||||
if (id && message && statusCode !== 200) {
|
||||
clearTimeout(this.cookiesTimeout);
|
||||
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.completeUrlPath) {
|
||||
// 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;
|
||||
const parsed = urlParse(url);
|
||||
|
||||
let isLastRedirect = url.includes(this.completeUrlPath);
|
||||
if (this.props.ssoType === ViewTypes.SAML) {
|
||||
isLastRedirect = isLastRedirect && !parsed.query;
|
||||
}
|
||||
|
||||
if (isLastRedirect) {
|
||||
this.extractCookie(parsed);
|
||||
}
|
||||
};
|
||||
|
||||
onLoadEndError = (e) => {
|
||||
console.warn('Failed to set store from local data', e); // eslint-disable-line no-console
|
||||
let error = e.message;
|
||||
if (e.details) {
|
||||
error += `\n${e.details.message}`;
|
||||
}
|
||||
|
||||
if (e.url) {
|
||||
error += `\nURL: ${e.url}`;
|
||||
}
|
||||
this.setState({error});
|
||||
};
|
||||
|
||||
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 (error) {
|
||||
content = (
|
||||
<View style={style.errorContainer}>
|
||||
<Text style={style.errorText}>{error}</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (renderWebView) {
|
||||
content = (
|
||||
<WebView
|
||||
ref={this.webViewRef}
|
||||
source={{uri: this.loginUrl, headers: HEADERS}}
|
||||
javaScriptEnabledAndroid={true}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
startInLoadingState={true}
|
||||
onNavigationStateChange={this.onNavigationStateChange}
|
||||
onShouldStartLoadWithRequest={() => true}
|
||||
injectedJavaScript={jsCode}
|
||||
onLoadEnd={this.onLoadEnd}
|
||||
onMessage={messagingEnabled ? this.onMessage : null}
|
||||
useSharedProcessPool={false}
|
||||
cacheEnabled={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = this.renderLoading();
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<StatusBar/>
|
||||
{content}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
50
app/screens/sso/sso.test.tsx
Normal file
50
app/screens/sso/sso.test.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Linking} from 'react-native';
|
||||
|
||||
import {renderWithReduxIntl} from 'test/testing_library';
|
||||
import configureStore from 'test/test_store';
|
||||
|
||||
import SSOComponent from './index';
|
||||
|
||||
describe('SSO', () => {
|
||||
const baseProps = {
|
||||
config: {},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
test('implement with webview when version is less than 5.32 version', async () => {
|
||||
const store = await configureStore({
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
Version: '5.30.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const basicWrapper = renderWithReduxIntl(<SSOComponent {...baseProps}/>, store);
|
||||
expect(basicWrapper.queryByTestId('sso.webview')).toBeTruthy();
|
||||
expect(basicWrapper.queryByTestId('sso.redirect_url')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('implement with OS browser & redirect url from version 5.33', async () => {
|
||||
(Linking.openURL as jest.Mock).mockResolvedValueOnce('');
|
||||
const store = await configureStore({
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
Version: '5.33.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const basicWrapper = renderWithReduxIntl(<SSOComponent {...baseProps}/>, store);
|
||||
expect(basicWrapper.queryByTestId('sso.webview')).toBeFalsy();
|
||||
expect(basicWrapper.queryByTestId('sso.redirect_url')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
41
app/screens/sso/sso_with_redirect_url.test.tsx
Normal file
41
app/screens/sso/sso_with_redirect_url.test.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import SSOWithRedirectURL from './sso_with_redirect_url';
|
||||
|
||||
describe('SSO with redirect url', () => {
|
||||
const baseProps = {
|
||||
customUrlScheme: 'mmauth://',
|
||||
intl: {},
|
||||
loginError: '',
|
||||
loginUrl: '',
|
||||
onCSRFToken: jest.fn(),
|
||||
onMMToken: jest.fn(),
|
||||
setLoginError: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should show message when user navigates to the page', () => {
|
||||
const wrapperWithBaseProps = shallow(<SSOWithRedirectURL {...baseProps}/>);
|
||||
expect(wrapperWithBaseProps.find(FormattedText).find({id: 'mobile.oauth.switch_to_browser'}).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should show "try again" and hide default message when error text is displayed', () => {
|
||||
const wrapperWithBaseProps = shallow(<SSOWithRedirectURL {...baseProps}/>);
|
||||
expect(wrapperWithBaseProps.find(FormattedText).find({id: 'mobile.oauth.try_again'}).exists()).toBe(false);
|
||||
const wrapper = shallow(
|
||||
<SSOWithRedirectURL
|
||||
{...baseProps}
|
||||
loginError='some error'
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.find(FormattedText).find({id: 'mobile.oauth.try_again'}).exists()).toBe(true);
|
||||
expect(wrapper.find(FormattedText).find({id: 'mobile.oauth.switch_to_browser'}).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
179
app/screens/sso/sso_with_redirect_url.tsx
Normal file
179
app/screens/sso/sso_with_redirect_url.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Linking, Text, TouchableOpacity, View} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import {setDeepLinkURL} from '@actions/views/root';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import Loading from '@components/loading';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import Store from '@store/store';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
interface SSOWithRedirectURLProps {
|
||||
intl: typeof intlShape;
|
||||
loginError: string;
|
||||
loginUrl: string;
|
||||
onCSRFToken: (token: string) => void;
|
||||
onMMToken: (token: string) => void;
|
||||
setLoginError: (value: string) => void;
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
function SSOWithRedirectURL({
|
||||
intl,
|
||||
loginError,
|
||||
loginUrl,
|
||||
onCSRFToken,
|
||||
onMMToken,
|
||||
setLoginError,
|
||||
theme,
|
||||
}: SSOWithRedirectURLProps) {
|
||||
const [error, setError] = React.useState<string>('');
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let customUrlScheme = 'mmauth://';
|
||||
if (DeviceInfo.getBundleId && DeviceInfo.getBundleId().includes('rnbeta')) {
|
||||
customUrlScheme = 'mmauthbeta://';
|
||||
}
|
||||
|
||||
const redirectUrl = customUrlScheme + 'callback';
|
||||
|
||||
const init = (resetErrors?: boolean) => {
|
||||
if (resetErrors !== false) {
|
||||
setError('');
|
||||
setLoginError('');
|
||||
}
|
||||
const parsedUrl = urlParse(loginUrl, true);
|
||||
parsedUrl.set('query', {
|
||||
...parsedUrl.query,
|
||||
redirect_to: redirectUrl,
|
||||
});
|
||||
const url = parsedUrl.toString();
|
||||
|
||||
const onError = () => setError(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.oauth.failed_to_open_link',
|
||||
defaultMessage: 'The link failed to open. Please try again.',
|
||||
}),
|
||||
);
|
||||
tryOpenURL(url, onError);
|
||||
};
|
||||
|
||||
const onURLChange = ({url}: { url: string }) => {
|
||||
if (url && url.startsWith(redirectUrl)) {
|
||||
Store?.redux?.dispatch(setDeepLinkURL(''));
|
||||
const parsedUrl = urlParse(url, true);
|
||||
if (parsedUrl.query && parsedUrl.query.MMCSRF && parsedUrl.query.MMAUTHTOKEN) {
|
||||
onCSRFToken(parsedUrl.query.MMCSRF);
|
||||
onMMToken(parsedUrl.query.MMAUTHTOKEN);
|
||||
} else {
|
||||
setError(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.oauth.failed_to_login',
|
||||
defaultMessage: 'Your login attempt failed. Please try again.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
Linking.addEventListener('url', onURLChange);
|
||||
init(false);
|
||||
return () => {
|
||||
Linking.removeEventListener('url', onURLChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={style.container}
|
||||
testID='sso.redirect_url'
|
||||
>
|
||||
{loginError || error ? (
|
||||
<View style={style.errorContainer}>
|
||||
<View style={style.errorTextContainer}>
|
||||
<Text style={style.errorText}>
|
||||
{`${loginError || error}.`}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => init()}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.try_again'
|
||||
defaultMessage='Try again'
|
||||
style={style.button}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View style={style.infoContainer}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.switch_to_browser'
|
||||
defaultMessage='Please use your browser to complete the login'
|
||||
style={style.infoText}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => init()}>
|
||||
<FormattedText
|
||||
id='mobile.oauth.restart_login'
|
||||
defaultMessage='Restart login'
|
||||
style={style.button}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Loading/>
|
||||
</View>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginTop: 40,
|
||||
},
|
||||
errorTextContainer: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
errorText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
lineHeight: 23,
|
||||
},
|
||||
infoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
marginTop: 40,
|
||||
},
|
||||
infoText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 16,
|
||||
fontWeight: '400',
|
||||
lineHeight: 23,
|
||||
marginBottom: 6,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
color: theme.buttonColor,
|
||||
fontSize: 16,
|
||||
paddingHorizontal: 9,
|
||||
paddingVertical: 9,
|
||||
marginTop: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default SSOWithRedirectURL;
|
||||
256
app/screens/sso/sso_with_webview.tsx
Normal file
256
app/screens/sso/sso_with_webview.tsx
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Alert, Text, View} from 'react-native';
|
||||
import CookieManager from 'react-native-cookies';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {WebView} from 'react-native-webview';
|
||||
import {WebViewErrorEvent, WebViewMessageEvent, WebViewNavigation, WebViewNavigationEvent} from 'react-native-webview/lib/WebViewTypes';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import type {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {popTopScreen} from '@actions/navigation';
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
interface SSOWithWebViewProps {
|
||||
completeUrlPath: string;
|
||||
intl: typeof intlShape;
|
||||
loginError: string;
|
||||
loginUrl: string;
|
||||
onCSRFToken: (token: string) => void;
|
||||
onMMToken: (token: string) => void;
|
||||
serverUrl: string;
|
||||
ssoType: string;
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
type CookieResponseType = {
|
||||
MMAUTHTOKEN: string | {
|
||||
value: string
|
||||
};
|
||||
MMCSRF: string | {
|
||||
value: string
|
||||
};
|
||||
}
|
||||
|
||||
function SSOWithWebView({completeUrlPath, intl, loginError, loginUrl, onCSRFToken, onMMToken, serverUrl, ssoType, theme}: SSOWithWebViewProps) {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const [error, setError] = React.useState(null);
|
||||
const [jsCode, setJSCode] = React.useState('');
|
||||
const [messagingEnabled, setMessagingEnabled] = React.useState(false);
|
||||
const [shouldRenderWebView, setShouldRenderWebView] = React.useState(true);
|
||||
const cookiesTimeout = React.useRef<number>();
|
||||
const webView = React.useRef<WebView>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (cookiesTimeout.current) {
|
||||
clearTimeout(cookiesTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const extractCookie = (parsedUrl: urlParse) => {
|
||||
try {
|
||||
const original = urlParse(serverUrl);
|
||||
|
||||
// Check whether we need to set a sub-path
|
||||
parsedUrl.set('pathname', original.pathname || '');
|
||||
|
||||
// Rebuild the server url without query string and/or hash
|
||||
const url = `${parsedUrl.origin}${parsedUrl.pathname}`;
|
||||
Client4.setUrl(url);
|
||||
|
||||
CookieManager.get(url, true).then((res: CookieResponseType) => {
|
||||
const mmtoken = res.MMAUTHTOKEN;
|
||||
const csrf = res.MMCSRF;
|
||||
const token = typeof mmtoken === 'object' ? mmtoken.value : mmtoken;
|
||||
const csrfToken = typeof csrf === 'object' ? csrf.value : csrf;
|
||||
|
||||
if (csrfToken) {
|
||||
onCSRFToken(csrfToken);
|
||||
}
|
||||
if (token) {
|
||||
onMMToken(token);
|
||||
if (cookiesTimeout.current) {
|
||||
clearTimeout(cookiesTimeout.current);
|
||||
}
|
||||
setShouldRenderWebView(false);
|
||||
} else if (webView.current && !error) {
|
||||
webView.current.injectJavaScript(postMessageJS);
|
||||
cookiesTimeout.current = setTimeout(extractCookie.bind(null, parsedUrl), 250);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.oauth.something_wrong',
|
||||
defaultMessage: 'Something went wrong',
|
||||
}),
|
||||
'',
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.oauth.something_wrong.okButon',
|
||||
defaultMessage: 'Ok',
|
||||
}),
|
||||
onPress: () => {
|
||||
popTopScreen();
|
||||
},
|
||||
}],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onMessage = (event: WebViewMessageEvent) => {
|
||||
try {
|
||||
const response = JSON.parse(event.nativeEvent.data);
|
||||
if (response) {
|
||||
const {
|
||||
id,
|
||||
message,
|
||||
status_code: statusCode,
|
||||
} = response;
|
||||
if (id && message && statusCode !== 200) {
|
||||
if (cookiesTimeout.current) {
|
||||
clearTimeout(cookiesTimeout.current);
|
||||
}
|
||||
setError(message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
const onNavigationStateChange = (navState: WebViewNavigation) => {
|
||||
const {url} = navState;
|
||||
let isMessagingEnabled = false;
|
||||
const parsed = urlParse(url);
|
||||
if (parsed.host.includes('.onelogin.com')) {
|
||||
setJSCode(oneLoginFormScalingJS);
|
||||
} else if (parsed.pathname === completeUrlPath) {
|
||||
// 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.
|
||||
isMessagingEnabled = true;
|
||||
}
|
||||
setMessagingEnabled(isMessagingEnabled);
|
||||
};
|
||||
|
||||
const onLoadEnd = (event: WebViewNavigationEvent | WebViewErrorEvent) => {
|
||||
const url = event.nativeEvent.url;
|
||||
const parsed = urlParse(url);
|
||||
|
||||
let isLastRedirect = url.includes(completeUrlPath);
|
||||
if (ssoType === ViewTypes.SAML) {
|
||||
isLastRedirect = isLastRedirect && !parsed.query;
|
||||
}
|
||||
|
||||
if (isLastRedirect) {
|
||||
extractCookie(parsed);
|
||||
}
|
||||
};
|
||||
|
||||
const renderWebView = () => {
|
||||
if (shouldRenderWebView) {
|
||||
const userAgent = ssoType === ViewTypes.GOOGLE ? 'Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/LMY48X) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Mobile Safari/608.2.11' : undefined;
|
||||
return (
|
||||
<WebView
|
||||
automaticallyAdjustContentInsets={false}
|
||||
cacheEnabled={false}
|
||||
injectedJavaScript={jsCode}
|
||||
javaScriptEnabled={true}
|
||||
onLoadEnd={onLoadEnd}
|
||||
onMessage={messagingEnabled ? onMessage : undefined}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
onShouldStartLoadWithRequest={() => true}
|
||||
ref={webView}
|
||||
source={{uri: loginUrl, headers: HEADERS}}
|
||||
startInLoadingState={true}
|
||||
userAgent={userAgent}
|
||||
useSharedProcessPool={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Loading/>;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={style.container}
|
||||
testID='sso.webview'
|
||||
>
|
||||
<StatusBar/>
|
||||
{error || loginError ? (
|
||||
<View style={style.errorContainer}>
|
||||
<Text style={style.errorText}>{error || loginError}</Text>
|
||||
</View>
|
||||
) : renderWebView()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: 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 SSOWithWebView;
|
||||
|
|
@ -362,8 +362,15 @@
|
|||
"mobile.notification_settings.modal_cancel": "CANCEL",
|
||||
"mobile.notification_settings.modal_save": "SAVE",
|
||||
"mobile.notification_settings.ooo_auto_responder": "Automatic Direct Message Replies",
|
||||
"mobile.notification_settings.save_failed_description": "The notification settings failed to save due to a connection issue, please try again.",
|
||||
"mobile.notification_settings.save_failed_description": "The notification settings failed to save due to a connection issue. Please try again.",
|
||||
"mobile.notification_settings.save_failed_title": "Connection issue",
|
||||
"mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.",
|
||||
"mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.",
|
||||
"mobile.oauth.restart_login": "Restart login",
|
||||
"mobile.oauth.something_wrong": "Something went wrong",
|
||||
"mobile.oauth.something_wrong.okButon": "OK",
|
||||
"mobile.oauth.switch_to_browser": "Please use your browser to complete the login process.",
|
||||
"mobile.oauth.try_again": "Try again",
|
||||
"mobile.offlineIndicator.connected": "Connected",
|
||||
"mobile.offlineIndicator.connecting": "Connecting...",
|
||||
"mobile.offlineIndicator.offline": "No internet connection",
|
||||
|
|
@ -591,6 +598,7 @@
|
|||
"sidebar.types.recent": "RECENT ACTIVITY",
|
||||
"sidebar.unreads": "More unreads",
|
||||
"signup.email": "Email and Password",
|
||||
"signup.google": "Google Apps",
|
||||
"signup.office365": "Office 365",
|
||||
"status_dropdown.set_away": "Away",
|
||||
"status_dropdown.set_dnd": "Do Not Disturb",
|
||||
|
|
|
|||
BIN
assets/base/images/google.png
Normal file
BIN
assets/base/images/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 601 B |
|
|
@ -429,12 +429,13 @@ platform :ios do
|
|||
|
||||
# Set the deep link prefix
|
||||
app_scheme = ENV['APP_SCHEME'] || 'mattermost'
|
||||
app_auth_scheme = ENV['BETA_BUILD'] == 'true' ? 'mmauthbeta' : 'mmauth'
|
||||
update_info_plist(
|
||||
xcodeproj: './ios/Mattermost.xcodeproj',
|
||||
plist_path: 'Mattermost/Info.plist',
|
||||
block: proc do |plist|
|
||||
urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "com.mattermost"}
|
||||
urlScheme[:CFBundleURLSchemes] = [app_scheme]
|
||||
urlScheme[:CFBundleURLSchemes] = [app_scheme, app_auth_scheme]
|
||||
end
|
||||
)
|
||||
|
||||
|
|
@ -660,6 +661,14 @@ platform :android do
|
|||
new_string: "scheme=\'#{app_scheme}\'"
|
||||
)
|
||||
|
||||
if ENV['BETA_BUILD'] != 'true'
|
||||
find_replace_string(
|
||||
path_to_file: "./android/app/src/main/AndroidManifest.xml",
|
||||
old_string: 'scheme="mmauthbeta"',
|
||||
new_string: 'scheme="mmauth"',
|
||||
)
|
||||
end
|
||||
|
||||
beta_dir = './android/app/src/main/java/com/mattermost/rnbeta/'
|
||||
release_dir = "./android/app/src/main/java/#{package_id.gsub '.', '/'}/"
|
||||
if ENV['BUILD_FOR_RELEASE'] == 'true'
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>mattermost</string>
|
||||
<string>mmauthbeta</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
|
|
|||
Loading…
Reference in a new issue