[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
});
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',

View File

@ -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() {

View File

@ -24,11 +24,14 @@ const styles = StyleSheet.create({
}
});
class OAuthView extends React.PureComponent {
static navigationOptions = ({ navigation }) => ({
headerLeft: <CloseModalButton navigation={navigation} />,
title: 'OAuth'
})
class AuthenticationWebView extends React.PureComponent {
static navigationOptions = ({ navigation }) => {
const authType = navigation.getParam('authType', 'oauth');
return {
headerLeft: <CloseModalButton navigation={navigation} />,
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 (
<React.Fragment>
<StatusBar />
<WebView
useWebKit
source={{ uri: oAuthUrl }}
source={{ uri }}
userAgent={userAgent}
onNavigationStateChange={(webViewState) => {
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);

View File

@ -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 = <Text style={styles.serviceName}>{name}</Text>;
} else {
buttonText = (
<>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{name}</Text>
</>
);
}
return (
<RectButton key={service.name} onPress={onPress} style={styles.serviceButton}>
<View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' ? <Image source={{ uri: icon }} style={styles.serviceIcon} /> : null}
<Text style={styles.serviceText}>
{I18n.t('Continue_with')} <Text style={styles.serviceName}>{name}</Text>
</Text>
<Text style={styles.serviceText}>{buttonText}</Text>
</View>
</RectButton>
);