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