diff --git a/app/constants/settings.js b/app/constants/settings.js index ba63d4cab..83a269dfb 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -50,6 +50,15 @@ export default { Accounts_ManuallyApproveNewUsers: { type: 'valueAsBoolean' }, + Accounts_iframe_enabled: { + type: 'valueAsBoolean' + }, + Accounts_Iframe_api_url: { + type: 'valueAsString' + }, + Accounts_Iframe_api_method: { + type: 'valueAsString' + }, CROWD_Enable: { type: 'valueAsBoolean' }, diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js index 79e3db074..7a6e351cf 100644 --- a/app/lib/methods/getSettings.js +++ b/app/lib/methods/getSettings.js @@ -27,7 +27,10 @@ const loginSettings = [ 'Accounts_RegistrationForm_LinkReplacementText', 'Accounts_EmailOrUsernamePlaceholder', 'Accounts_PasswordPlaceholder', - 'Accounts_PasswordReset' + 'Accounts_PasswordReset', + 'Accounts_iframe_enabled', + 'Accounts_Iframe_api_url', + 'Accounts_Iframe_api_method' ]; const serverInfoUpdate = async(serverInfo, iconSetting) => { diff --git a/app/views/AuthenticationWebView.js b/app/views/AuthenticationWebView.js index 311fcd299..2eca58eda 100644 --- a/app/views/AuthenticationWebView.js +++ b/app/views/AuthenticationWebView.js @@ -16,11 +16,37 @@ const userAgent = isIOS ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1' : 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36'; +// iframe uses a postMessage to send the token to the client +// We'll handle this sending the token to the hash of the window.location +// https://docs.rocket.chat/guides/developer-guides/iframe-integration/authentication#iframe-url +// https://github.com/react-native-community/react-native-webview/issues/24#issuecomment-540130141 +const injectedJavaScript = ` +window.addEventListener('message', ({ data }) => { + if (typeof data === 'object') { + window.location.hash = JSON.stringify(data); + } +}); +function wrap(fn) { + return function wrapper() { + var res = fn.apply(this, arguments); + window.ReactNativeWebView.postMessage(window.location.href); + return res; + } +} +history.pushState = wrap(history.pushState); +history.replaceState = wrap(history.replaceState); +window.addEventListener('popstate', function() { + window.ReactNativeWebView.postMessage(window.location.href); +}); +`; + class AuthenticationWebView extends React.PureComponent { static propTypes = { navigation: PropTypes.object, route: PropTypes.object, server: PropTypes.string, + Accounts_Iframe_api_url: PropTypes.bool, + Accounts_Iframe_api_method: PropTypes.bool, theme: PropTypes.string } @@ -30,7 +56,8 @@ class AuthenticationWebView extends React.PureComponent { logging: false, loading: false }; - this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); + this.oauthRedirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); + this.iframeRedirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(event|loginToken|token))`, 'g'); } componentWillUnmount() { @@ -64,6 +91,15 @@ class AuthenticationWebView extends React.PureComponent { // eslint-disable-next-line react/sort-comp debouncedLogin = debounce(params => this.login(params), 3000); + tryLogin = debounce(async() => { + const { Accounts_Iframe_api_url, Accounts_Iframe_api_method } = this.props; + const data = await fetch(Accounts_Iframe_api_url, { method: Accounts_Iframe_api_method }).then(response => response.json()); + const resume = data?.login || data?.loginToken; + if (resume) { + this.login({ resume }); + } + }, 3000, true) + onNavigationStateChange = (webViewState) => { const url = decodeURIComponent(webViewState.url); const { route } = this.props; @@ -86,25 +122,47 @@ class AuthenticationWebView extends React.PureComponent { } if (authType === 'oauth') { - if (this.redirectRegex.test(url)) { + if (this.oauthRedirectRegex.test(url)) { const parts = url.split('#'); const credentials = JSON.parse(parts[1]); this.login({ oauth: { ...credentials } }); } } + + if (authType === 'iframe') { + if (this.iframeRedirectRegex.test(url)) { + const parts = url.split('#'); + const credentials = JSON.parse(parts[1]); + switch (credentials.event) { + case 'try-iframe-login': + this.tryLogin(); + break; + case 'login-with-token': + this.login({ resume: credentials.token || credentials.loginToken }); + break; + default: + // Do nothing + } + } + } } render() { const { loading } = this.state; const { route, theme } = this.props; - const { url } = route.params; + const { url, authType } = route.params; + const isIframe = authType === 'iframe'; + return ( <> this.onNavigationStateChange(nativeEvent)} onNavigationStateChange={this.onNavigationStateChange} + injectedJavaScript={isIframe ? injectedJavaScript : undefined} onLoadStart={() => { this.setState({ loading: true }); }} @@ -119,14 +177,16 @@ class AuthenticationWebView extends React.PureComponent { } const mapStateToProps = state => ({ - server: state.server.server + server: state.server.server, + Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url, + Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method }); AuthenticationWebView.navigationOptions = ({ route, navigation }) => { const { authType } = route.params; return { headerLeft: () => , - title: authType === 'saml' || authType === 'cas' ? 'SSO' : 'OAuth' + title: ['saml', 'cas', 'iframe'].includes(authType) ? 'SSO' : 'OAuth' }; }; diff --git a/app/views/WorkspaceView/index.js b/app/views/WorkspaceView/index.js index 723688ac3..70531b823 100644 --- a/app/views/WorkspaceView/index.js +++ b/app/views/WorkspaceView/index.js @@ -27,16 +27,23 @@ class WorkspaceView extends React.Component { registrationForm: PropTypes.string, registrationText: PropTypes.string, showLoginButton: PropTypes.bool, + Accounts_iframe_enabled: PropTypes.bool, inviteLinkToken: PropTypes.string } get showRegistrationButton() { - const { registrationForm, inviteLinkToken } = this.props; - return registrationForm === 'Public' || (registrationForm === 'Secret URL' && inviteLinkToken?.length); + const { registrationForm, inviteLinkToken, Accounts_iframe_enabled } = this.props; + return !Accounts_iframe_enabled && (registrationForm === 'Public' || (registrationForm === 'Secret URL' && inviteLinkToken?.length)); } login = () => { - const { navigation, Site_Name } = this.props; + const { + navigation, server, Site_Name, Accounts_iframe_enabled + } = this.props; + if (Accounts_iframe_enabled) { + navigation.navigate('AuthenticationWebView', { url: server, authType: 'iframe' }); + return; + } navigation.navigate('LoginView', { title: Site_Name }); } @@ -45,10 +52,20 @@ class WorkspaceView extends React.Component { navigation.navigate('RegisterView', { title: Site_Name }); } + renderRegisterDisabled = () => { + const { Accounts_iframe_enabled, registrationText, theme } = this.props; + if (Accounts_iframe_enabled) { + return null; + } + + return {registrationText}; + } + render() { const { - theme, Site_Name, Site_Url, Assets_favicon_512, server, registrationText, showLoginButton + theme, Site_Name, Site_Url, Assets_favicon_512, server, showLoginButton } = this.props; + return ( @@ -77,9 +94,7 @@ class WorkspaceView extends React.Component { theme={theme} testID='workspace-view-register' /> - ) : ( - {registrationText} - ) + ) : this.renderRegisterDisabled() } @@ -95,6 +110,7 @@ const mapStateToProps = state => ({ Assets_favicon_512: state.settings.Assets_favicon_512, registrationForm: state.settings.Accounts_RegistrationForm, registrationText: state.settings.Accounts_RegistrationForm_LinkReplacementText, + Accounts_iframe_enabled: state.settings.Accounts_iframe_enabled, showLoginButton: getShowLoginButton(state), inviteLinkToken: state.inviteLinks.token });