diff --git a/app/index.js b/app/index.js index 7b30aa8be..7a3d8effe 100644 --- a/app/index.js +++ b/app/index.js @@ -62,9 +62,9 @@ const OutsideStack = createStackNavigator({ defaultNavigationOptions: defaultHeader }); -const OAuthStack = createStackNavigator({ - OAuthView: { - getScreen: () => require('./views/OAuthView').default +const AuthenticationWebViewStack = createStackNavigator({ + AuthenticationWebView: { + getScreen: () => require('./views/AuthenticationWebView').default } }, { defaultNavigationOptions: defaultHeader @@ -72,7 +72,7 @@ const OAuthStack = createStackNavigator({ const OutsideStackModal = createStackNavigator({ OutsideStack, - OAuthStack + AuthenticationWebViewStack }, { mode: 'modal', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 325daa987..d3efaa4b3 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -297,7 +297,7 @@ const RocketChat = { } }, - async loginOAuth(params) { + async loginOAuthOrSso(params) { try { const result = await this.login(params); reduxStore.dispatch(loginRequest({ resume: result.token })); @@ -776,15 +776,15 @@ const RocketChat = { }, async getLoginServices(server) { try { - let loginServicesFilter = []; + let loginServices = []; const loginServicesResult = await fetch(`${ server }/api/v1/settings.oauth`).then(response => response.json()); if (loginServicesResult.success && loginServicesResult.services.length > 0) { const { services } = loginServicesResult; - loginServicesFilter = services.filter(item => item.custom !== undefined); // TODO: remove this after SAML and CAS + loginServices = services; - const loginServicesReducer = loginServicesFilter.reduce((ret, item) => { - const name = item.name ? item.name : item.service; + const loginServicesReducer = loginServices.reduce((ret, item) => { + const name = item.name || item.buttonLabelText || item.service; const authType = this._determineAuthType(item); if (authType !== 'not_supported') { @@ -795,21 +795,25 @@ const RocketChat = { }, {}); reduxStore.dispatch(setLoginServices(loginServicesReducer)); } - return Promise.resolve(loginServicesFilter.length); + return Promise.resolve(loginServices.length); } catch (error) { console.warn(error); return Promise.reject(); } }, - _determineAuthType(service) { - // TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise - const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter']; - const { name, custom } = service; + _determineAuthType(services) { + const { name, custom, service } = services; if (custom) { return 'oauth_custom'; } + if (service === 'saml') { + return 'saml'; + } + + // TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise + const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter']; return availableOAuth.includes(name) ? 'oauth' : 'not_supported'; }, getUsernameSuggestion() { diff --git a/app/views/OAuthView.js b/app/views/AuthenticationWebView.js similarity index 58% rename from app/views/OAuthView.js rename to app/views/AuthenticationWebView.js index eee46c158..2895f3cee 100644 --- a/app/views/OAuthView.js +++ b/app/views/AuthenticationWebView.js @@ -24,11 +24,14 @@ const styles = StyleSheet.create({ } }); -class OAuthView extends React.PureComponent { - static navigationOptions = ({ navigation }) => ({ - headerLeft: , - title: 'OAuth' - }) +class AuthenticationWebView extends React.PureComponent { + static navigationOptions = ({ navigation }) => { + const authType = navigation.getParam('authType', 'oauth'); + return { + headerLeft: , + title: authType === 'saml' ? 'SSO' : 'OAuth' + }; + } static propTypes = { navigation: PropTypes.object, @@ -41,6 +44,7 @@ class OAuthView extends React.PureComponent { logging: false, loading: false }; + this.authType = props.navigation.getParam('authType', 'oauth'); this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); } @@ -58,7 +62,7 @@ class OAuthView extends React.PureComponent { this.setState({ logging: true }); try { - await RocketChat.loginOAuth(params); + await RocketChat.loginOAuthOrSso(params); } catch (e) { console.warn(e); } @@ -66,29 +70,45 @@ class OAuthView extends React.PureComponent { this.dismiss(); } + onNavigationStateChange = (webViewState) => { + const url = decodeURIComponent(webViewState.url); + if (this.authType === 'saml') { + const { navigation } = this.props; + const ssoToken = navigation.getParam('ssoToken'); + if (url.includes('ticket') || url.includes('validate')) { + const payload = `{ "saml": true, "credentialToken": "${ ssoToken }" }`; + // We need to set a timeout when the login is done with SSO in order to make it work on our side. + // It is actually due to the SSO server processing the response. + setTimeout(() => { + this.login(JSON.parse(payload)); + }, 3000); + } + } + + if (this.authType === 'oauth') { + if (this.redirectRegex.test(url)) { + const parts = url.split('#'); + const credentials = JSON.parse(parts[1]); + this.login({ oauth: { ...credentials } }); + } + } + } + render() { const { navigation } = this.props; const { loading } = this.state; - const oAuthUrl = navigation.getParam('oAuthUrl'); + const uri = navigation.getParam('url'); return ( { - const url = decodeURIComponent(webViewState.url); - if (this.redirectRegex.test(url)) { - const parts = url.split('#'); - const credentials = JSON.parse(parts[1]); - this.login({ oauth: { ...credentials } }); - } - }} + onNavigationStateChange={this.onNavigationStateChange} onLoadStart={() => { this.setState({ loading: true }); }} - onLoadEnd={() => { this.setState({ loading: false }); }} @@ -103,4 +123,4 @@ const mapStateToProps = state => ({ server: state.server.server }); -export default connect(mapStateToProps)(OAuthView); +export default connect(mapStateToProps)(AuthenticationWebView); diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js index 8184c9a8a..36e530e5a 100644 --- a/app/views/LoginSignupView.js +++ b/app/views/LoginSignupView.js @@ -156,7 +156,7 @@ class LoginSignupView extends React.Component { const scope = 'email'; const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`; - this.openOAuth(`${ endpoint }${ params }`); + this.openOAuth({ url: `${ endpoint }${ params }` }); } onPressGithub = () => { @@ -167,7 +167,7 @@ class LoginSignupView extends React.Component { const scope = 'user:email'; const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`; - this.openOAuth(`${ endpoint }${ encodeURIComponent(params) }`); + this.openOAuth({ url: `${ endpoint }${ encodeURIComponent(params) }` }); } onPressGitlab = () => { @@ -179,7 +179,7 @@ class LoginSignupView extends React.Component { const scope = 'read_user'; const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; - this.openOAuth(`${ endpoint }${ params }`); + this.openOAuth({ url: `${ endpoint }${ params }` }); } onPressGoogle = () => { @@ -190,7 +190,7 @@ class LoginSignupView extends React.Component { const scope = 'email'; const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; - this.openOAuth(`${ endpoint }${ params }`); + this.openOAuth({ url: `${ endpoint }${ params }` }); } onPressLinkedin = () => { @@ -201,7 +201,7 @@ class LoginSignupView extends React.Component { const scope = 'r_emailaddress'; const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; - this.openOAuth(`${ endpoint }${ params }`); + this.openOAuth({ url: `${ endpoint }${ params }` }); } onPressMeteor = () => { @@ -211,14 +211,14 @@ class LoginSignupView extends React.Component { const redirect_uri = `${ server }/_oauth/meteor-developer`; const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`; - this.openOAuth(`${ endpoint }${ params }`); + this.openOAuth({ url: `${ endpoint }${ params }` }); } onPressTwitter = () => { const { server } = this.props; const state = this.getOAuthState(); const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`; - this.openOAuth(url); + this.openOAuth({ url }); } onPressCustomOAuth = (loginService) => { @@ -230,7 +230,16 @@ class LoginSignupView extends React.Component { const state = this.getOAuthState(); const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`; const url = `${ serverURL }${ authorizePath }${ params }`; - this.openOAuth(url); + this.openOAuth({ url }); + } + + onPressSaml = (loginService) => { + const { server } = this.props; + const { clientConfig } = loginService; + const { provider } = clientConfig; + const ssoToken = random(17); + const url = `${ server }/_saml/authorize/${ provider }/${ ssoToken }`; + this.openOAuth({ url, ssoToken, authType: 'saml' }); } getOAuthState = () => { @@ -238,9 +247,9 @@ class LoginSignupView extends React.Component { return Base64.encodeURI(JSON.stringify({ loginStyle: 'popup', credentialToken, isCordova: true })); } - openOAuth = (oAuthUrl) => { + openOAuth = ({ url, ssoToken, authType = 'oauth' }) => { const { navigation } = this.props; - navigation.navigate('OAuthView', { oAuthUrl }); + navigation.navigate('AuthenticationWebView', { url, authType, ssoToken }); } login = () => { @@ -317,7 +326,6 @@ class LoginSignupView extends React.Component { let { name } = service; name = name === 'meteor-developer' ? 'meteor' : name; const icon = `icon_${ name }`; - name = name.charAt(0).toUpperCase() + name.slice(1); let onPress = () => {}; switch (service.authType) { @@ -329,16 +337,29 @@ class LoginSignupView extends React.Component { onPress = () => this.onPressCustomOAuth(service); break; } + case 'saml': { + onPress = () => this.onPressSaml(service); + break; + } default: break; } + name = name.charAt(0).toUpperCase() + name.slice(1); + let buttonText; + if (service.service === 'saml') { + buttonText = {name}; + } else { + buttonText = ( + <> + {I18n.t('Continue_with')} {name} + + ); + } return ( {service.authType === 'oauth' ? : null} - - {I18n.t('Continue_with')} {name} - + {buttonText} );