[NEW] SAML authentication support (#1108)

This commit is contained in:
Filipe Brito 2019-08-09 14:28:46 -03:00 committed by Diego Mello
parent b8d9848e6d
commit 59426f470b
4 changed files with 91 additions and 46 deletions

View File

@ -62,9 +62,9 @@ const OutsideStack = createStackNavigator({
defaultNavigationOptions: defaultHeader defaultNavigationOptions: defaultHeader
}); });
const OAuthStack = createStackNavigator({ const AuthenticationWebViewStack = createStackNavigator({
OAuthView: { AuthenticationWebView: {
getScreen: () => require('./views/OAuthView').default getScreen: () => require('./views/AuthenticationWebView').default
} }
}, { }, {
defaultNavigationOptions: defaultHeader defaultNavigationOptions: defaultHeader
@ -72,7 +72,7 @@ const OAuthStack = createStackNavigator({
const OutsideStackModal = createStackNavigator({ const OutsideStackModal = createStackNavigator({
OutsideStack, OutsideStack,
OAuthStack AuthenticationWebViewStack
}, },
{ {
mode: 'modal', mode: 'modal',

View File

@ -297,7 +297,7 @@ const RocketChat = {
} }
}, },
async loginOAuth(params) { async loginOAuthOrSso(params) {
try { try {
const result = await this.login(params); const result = await this.login(params);
reduxStore.dispatch(loginRequest({ resume: result.token })); reduxStore.dispatch(loginRequest({ resume: result.token }));
@ -776,15 +776,15 @@ const RocketChat = {
}, },
async getLoginServices(server) { async getLoginServices(server) {
try { try {
let loginServicesFilter = []; let loginServices = [];
const loginServicesResult = await fetch(`${ server }/api/v1/settings.oauth`).then(response => response.json()); const loginServicesResult = await fetch(`${ server }/api/v1/settings.oauth`).then(response => response.json());
if (loginServicesResult.success && loginServicesResult.services.length > 0) { if (loginServicesResult.success && loginServicesResult.services.length > 0) {
const { services } = loginServicesResult; 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 loginServicesReducer = loginServices.reduce((ret, item) => {
const name = item.name ? item.name : item.service; const name = item.name || item.buttonLabelText || item.service;
const authType = this._determineAuthType(item); const authType = this._determineAuthType(item);
if (authType !== 'not_supported') { if (authType !== 'not_supported') {
@ -795,21 +795,25 @@ const RocketChat = {
}, {}); }, {});
reduxStore.dispatch(setLoginServices(loginServicesReducer)); reduxStore.dispatch(setLoginServices(loginServicesReducer));
} }
return Promise.resolve(loginServicesFilter.length); return Promise.resolve(loginServices.length);
} catch (error) { } catch (error) {
console.warn(error); console.warn(error);
return Promise.reject(); return Promise.reject();
} }
}, },
_determineAuthType(service) { _determineAuthType(services) {
// TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise const { name, custom, service } = services;
const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter'];
const { name, custom } = service;
if (custom) { if (custom) {
return 'oauth_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'; return availableOAuth.includes(name) ? 'oauth' : 'not_supported';
}, },
getUsernameSuggestion() { getUsernameSuggestion() {

View File

@ -24,11 +24,14 @@ const styles = StyleSheet.create({
} }
}); });
class OAuthView extends React.PureComponent { class AuthenticationWebView extends React.PureComponent {
static navigationOptions = ({ navigation }) => ({ static navigationOptions = ({ navigation }) => {
headerLeft: <CloseModalButton navigation={navigation} />, const authType = navigation.getParam('authType', 'oauth');
title: 'OAuth' return {
}) headerLeft: <CloseModalButton navigation={navigation} />,
title: authType === 'saml' ? 'SSO' : 'OAuth'
};
}
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
@ -41,6 +44,7 @@ class OAuthView extends React.PureComponent {
logging: false, logging: false,
loading: false loading: false
}; };
this.authType = props.navigation.getParam('authType', 'oauth');
this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g'); this.redirectRegex = new RegExp(`(?=.*(${ props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
} }
@ -58,7 +62,7 @@ class OAuthView extends React.PureComponent {
this.setState({ logging: true }); this.setState({ logging: true });
try { try {
await RocketChat.loginOAuth(params); await RocketChat.loginOAuthOrSso(params);
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
@ -66,29 +70,45 @@ class OAuthView extends React.PureComponent {
this.dismiss(); 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() { render() {
const { navigation } = this.props; const { navigation } = this.props;
const { loading } = this.state; const { loading } = this.state;
const oAuthUrl = navigation.getParam('oAuthUrl'); const uri = navigation.getParam('url');
return ( return (
<React.Fragment> <React.Fragment>
<StatusBar /> <StatusBar />
<WebView <WebView
useWebKit useWebKit
source={{ uri: oAuthUrl }} source={{ uri }}
userAgent={userAgent} userAgent={userAgent}
onNavigationStateChange={(webViewState) => { onNavigationStateChange={this.onNavigationStateChange}
const url = decodeURIComponent(webViewState.url);
if (this.redirectRegex.test(url)) {
const parts = url.split('#');
const credentials = JSON.parse(parts[1]);
this.login({ oauth: { ...credentials } });
}
}}
onLoadStart={() => { onLoadStart={() => {
this.setState({ loading: true }); this.setState({ loading: true });
}} }}
onLoadEnd={() => { onLoadEnd={() => {
this.setState({ loading: false }); this.setState({ loading: false });
}} }}
@ -103,4 +123,4 @@ const mapStateToProps = state => ({
server: state.server.server server: state.server.server
}); });
export default connect(mapStateToProps)(OAuthView); export default connect(mapStateToProps)(AuthenticationWebView);

View File

@ -156,7 +156,7 @@ class LoginSignupView extends React.Component {
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`; 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 = () => { onPressGithub = () => {
@ -167,7 +167,7 @@ class LoginSignupView extends React.Component {
const scope = 'user:email'; const scope = 'user:email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`; 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 = () => { onPressGitlab = () => {
@ -179,7 +179,7 @@ class LoginSignupView extends React.Component {
const scope = 'read_user'; const scope = 'read_user';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; 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 = () => { onPressGoogle = () => {
@ -190,7 +190,7 @@ class LoginSignupView extends React.Component {
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; 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 = () => { onPressLinkedin = () => {
@ -201,7 +201,7 @@ class LoginSignupView extends React.Component {
const scope = 'r_emailaddress'; const scope = 'r_emailaddress';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; 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 = () => { onPressMeteor = () => {
@ -211,14 +211,14 @@ class LoginSignupView extends React.Component {
const redirect_uri = `${ server }/_oauth/meteor-developer`; const redirect_uri = `${ server }/_oauth/meteor-developer`;
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`; const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`); this.openOAuth({ url: `${ endpoint }${ params }` });
} }
onPressTwitter = () => { onPressTwitter = () => {
const { server } = this.props; const { server } = this.props;
const state = this.getOAuthState(); const state = this.getOAuthState();
const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`; const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`;
this.openOAuth(url); this.openOAuth({ url });
} }
onPressCustomOAuth = (loginService) => { onPressCustomOAuth = (loginService) => {
@ -230,7 +230,16 @@ class LoginSignupView extends React.Component {
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`; const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`;
const url = `${ serverURL }${ authorizePath }${ params }`; 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 = () => { getOAuthState = () => {
@ -238,9 +247,9 @@ class LoginSignupView extends React.Component {
return Base64.encodeURI(JSON.stringify({ loginStyle: 'popup', credentialToken, isCordova: true })); return Base64.encodeURI(JSON.stringify({ loginStyle: 'popup', credentialToken, isCordova: true }));
} }
openOAuth = (oAuthUrl) => { openOAuth = ({ url, ssoToken, authType = 'oauth' }) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('OAuthView', { oAuthUrl }); navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
} }
login = () => { login = () => {
@ -317,7 +326,6 @@ class LoginSignupView extends React.Component {
let { name } = service; let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name; name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `icon_${ name }`; const icon = `icon_${ name }`;
name = name.charAt(0).toUpperCase() + name.slice(1);
let onPress = () => {}; let onPress = () => {};
switch (service.authType) { switch (service.authType) {
@ -329,16 +337,29 @@ class LoginSignupView extends React.Component {
onPress = () => this.onPressCustomOAuth(service); onPress = () => this.onPressCustomOAuth(service);
break; break;
} }
case 'saml': {
onPress = () => this.onPressSaml(service);
break;
}
default: default:
break; break;
} }
name = name.charAt(0).toUpperCase() + name.slice(1);
let buttonText;
if (service.service === 'saml') {
buttonText = <Text style={styles.serviceName}>{name}</Text>;
} else {
buttonText = (
<>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{name}</Text>
</>
);
}
return ( return (
<RectButton key={service.name} onPress={onPress} style={styles.serviceButton}> <RectButton key={service.name} onPress={onPress} style={styles.serviceButton}>
<View style={styles.serviceButtonContainer}> <View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' ? <Image source={{ uri: icon }} style={styles.serviceIcon} /> : null} {service.authType === 'oauth' ? <Image source={{ uri: icon }} style={styles.serviceIcon} /> : null}
<Text style={styles.serviceText}> <Text style={styles.serviceText}>{buttonText}</Text>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{name}</Text>
</Text>
</View> </View>
</RectButton> </RectButton>
); );