[NEW] SAML authentication support (#1108)
This commit is contained in:
parent
b8d9848e6d
commit
59426f470b
|
@ -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',
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue