From 5f04dc4f45b9657d46380499f92ae6e5c1bf5506 Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 5 Jul 2016 15:49:00 -0400 Subject: SAML support (#3494) * PLT-3073: Implement SAML/Okta Server side (EE) (#3422) * PLT-3137 Support for SAML configuration * PLT-3410 SAML Database Store * PLT-3411 CLI to add Identity Provider Certificate and Service Provider Private Key * PLT-3409 SAML Interface for EE * PLT-3139 Handle SAML authentication server side * Add localization messages * PLT-3443 SAML Obtain SP metadata * PLT-3142 Login & Switch to/from SAML * Remove Certs for Database & Clean SAML Request * Make required Username, FirstName and LastName * PLT-3140 Add SAML to System Console (#3476) * PLT-3140 Add SAML to System Console * Move web_client functions to client.jsx * Fix issues found by PM * update package.json mattermost driver * Fix text messages for SAML --- webapp/components/admin_console/admin_sidebar.jsx | 16 + .../admin_console/file_upload_setting.jsx | 124 +++++ .../admin_console/remove_file_setting.jsx | 72 +++ webapp/components/admin_console/saml_settings.jsx | 518 +++++++++++++++++++++ webapp/components/admin_console/user_item.jsx | 4 +- .../components/claim/components/email_to_oauth.jsx | 8 +- .../components/claim/components/oauth_to_email.jsx | 5 +- webapp/components/login/login_controller.jsx | 18 +- webapp/components/signup_user_complete.jsx | 14 + .../user_settings/user_settings_general.jsx | 28 ++ .../user_settings/user_settings_security.jsx | 27 ++ 11 files changed, 827 insertions(+), 7 deletions(-) create mode 100644 webapp/components/admin_console/file_upload_setting.jsx create mode 100644 webapp/components/admin_console/remove_file_setting.jsx create mode 100644 webapp/components/admin_console/saml_settings.jsx (limited to 'webapp/components') diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx index 8f88afab4..5a31519c9 100644 --- a/webapp/components/admin_console/admin_sidebar.jsx +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -176,6 +176,7 @@ export default class AdminSidebar extends React.Component { render() { let ldapSettings = null; + let samlSettings = null; let complianceSettings = null; let license = null; @@ -198,6 +199,20 @@ export default class AdminSidebar extends React.Component { ); } + if (global.window.mm_license.SAML === 'true') { + samlSettings = ( + + } + /> + ); + } + if (global.window.mm_license.Compliance === 'true') { complianceSettings = ( {ldapSettings} + {samlSettings} 0) { + this.setState({fileSelected: true, fileName: files[0].name}); + } + } + + handleSubmit(e) { + e.preventDefault(); + + $(this.refs.upload_button).button('loading'); + this.props.onSubmit(this.props.id, this.refs.fileInput.files[0], (error) => { + $(this.refs.upload_button).button('reset'); + if (error) { + Utils.clearFileInput(this.refs.fileInput); + } + this.setState({fileSelected: false, fileName: null, serverError: error}); + }); + } + + render() { + let serverError; + if (this.state.serverError) { + serverError =
; + } + + var btnClass = 'btn'; + if (this.state.fileSelected) { + btnClass = 'btn btn-primary'; + } + + let fileName; + if (this.state.fileName) { + fileName = this.state.fileName; + } else { + fileName = ( + + ); + } + + return ( + +
+
+ + +
+ +
+ {fileName} +
+ {serverError} +
+
+ ); + } +} diff --git a/webapp/components/admin_console/remove_file_setting.jsx b/webapp/components/admin_console/remove_file_setting.jsx new file mode 100644 index 000000000..5a76faae2 --- /dev/null +++ b/webapp/components/admin_console/remove_file_setting.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import React from 'react'; + +import Setting from './setting.jsx'; + +export default class RemoveFileSetting extends Setting { + static get propTypes() { + return { + id: React.PropTypes.string.isRequired, + label: React.PropTypes.node.isRequired, + helpText: React.PropTypes.node, + removeButtonText: React.PropTypes.node.isRequired, + removingText: React.PropTypes.node, + fileName: React.PropTypes.string.isRequired, + onSubmit: React.PropTypes.func.isRequired, + disabled: React.PropTypes.bool + }; + } + + constructor(props) { + super(props); + this.handleRemove = this.handleRemove.bind(this); + + this.state = { + serverError: null + }; + } + + handleRemove(e) { + e.preventDefault(); + + $(this.refs.remove_button).button('loading'); + this.props.onSubmit(this.props.id, (error) => { + $(this.refs.remove_button).button('reset'); + this.setState({serverError: error}); + }); + } + + render() { + let serverError; + if (this.state.serverError) { + serverError =
; + } + + return ( + +
+
+ {this.props.fileName} +
+ + {serverError} +
+
+ ); + } +} \ No newline at end of file diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx new file mode 100644 index 000000000..db841aa83 --- /dev/null +++ b/webapp/components/admin_console/saml_settings.jsx @@ -0,0 +1,518 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import AdminSettings from './admin_settings.jsx'; +import BooleanSetting from './boolean_setting.jsx'; +import TextSetting from './text_setting.jsx'; +import FileUploadSetting from './file_upload_setting.jsx'; +import RemoveFileSetting from './remove_file_setting.jsx'; + +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import SettingsGroup from './settings_group.jsx'; + +import Client from 'utils/web_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class SamlSettings extends AdminSettings { + constructor(props) { + super(props); + + this.getConfigFromState = this.getConfigFromState.bind(this); + + this.renderSettings = this.renderSettings.bind(this); + this.uploadCertificate = this.uploadCertificate.bind(this); + this.removeCertificate = this.removeCertificate.bind(this); + + const settings = props.config.SamlSettings; + + this.state = Object.assign(this.state, { + enable: settings.Enable, + verify: settings.Verify, + encrypt: settings.Encrypt, + idpUrl: settings.IdpUrl, + idpDescriptorUrl: settings.IdpDescriptorUrl, + assertionConsumerServiceURL: settings.AssertionConsumerServiceURL, + idpCertificateFile: settings.IdpCertificateFile, + publicCertificateFile: settings.PublicCertificateFile, + privateKeyFile: settings.PrivateKeyFile, + firstNameAttribute: settings.FirstNameAttribute, + lastNameAttribute: settings.LastNameAttribute, + emailAttribute: settings.EmailAttribute, + usernameAttribute: settings.UsernameAttribute, + nicknameAttribute: settings.NicknameAttribute, + localeAttribute: settings.LocaleAttribute, + loginButtonText: settings.LoginButtonText + }); + } + + getConfigFromState(config) { + config.SamlSettings.Enable = this.state.enable; + config.SamlSettings.Verify = this.state.verify; + config.SamlSettings.Encrypt = this.state.encrypt; + config.SamlSettings.IdpUrl = this.state.idpUrl; + config.SamlSettings.IdpDescriptorUrl = this.state.idpDescriptorUrl; + config.SamlSettings.AssertionConsumerServiceURL = this.state.assertionConsumerServiceURL; + config.SamlSettings.IdpCertificateFile = this.state.idpCertificateFile; + config.SamlSettings.PublicCertificateFile = this.state.publicCertificateFile; + config.SamlSettings.PrivateKeyFile = this.state.privateKeyFile; + config.SamlSettings.FirstNameAttribute = this.state.firstNameAttribute; + config.SamlSettings.LastNameAttribute = this.state.lastNameAttribute; + config.SamlSettings.EmailAttribute = this.state.emailAttribute; + config.SamlSettings.UsernameAttribute = this.state.usernameAttribute; + config.SamlSettings.NicknameAttribute = this.state.nicknameAttribute; + config.SamlSettings.LocaleAttribute = this.state.localeAttribute; + config.SamlSettings.LoginButtonText = this.state.loginButtonText; + + return config; + } + + uploadCertificate(id, file, callback) { + Client.uploadCertificateFile( + file, + () => { + const fileName = file.name; + this.handleChange(id, fileName); + this.setState({[id]: fileName}); + if (callback && typeof callback === 'function') { + callback(); + } + }, + (error) => { + if (callback && typeof callback === 'function') { + callback(error.message); + } + } + ); + } + + removeCertificate(id, callback) { + Client.removeCertificateFile( + this.state[id], + () => { + this.handleChange(id, ''); + this.setState({[id]: null}); + }, + (error) => { + if (callback && typeof callback === 'function') { + callback(error.message); + } + } + ); + } + + renderTitle() { + return ( +

+ +

+ ); + } + + renderSettings() { + const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true'; + if (!licenseEnabled) { + return null; + } + + let idpCert; + let privKey; + let pubCert; + + if (this.state.idpCertificateFile) { + idpCert = ( + + } + helpText={ + + } + removeButtonText={Utils.localizeMessage('admin.saml.remove.idp_certificate', 'Remove Identity Provider Certificate')} + removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')} + fileName={this.state.idpCertificateFile} + onSubmit={this.removeCertificate} + disabled={!this.state.enable} + /> + ); + } else { + idpCert = ( + + } + helpText={ + + } + uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')} + disabled={!this.state.enable} + fileType='.crt' + onSubmit={this.uploadCertificate} + /> + ); + } + + if (this.state.privateKeyFile) { + privKey = ( + + } + helpText={ + + } + removeButtonText={Utils.localizeMessage('admin.saml.remove.privKey', 'Remove Service Provider Private Key')} + removingText={Utils.localizeMessage('admin.saml.removing.privKey', 'Removing Private Key...')} + fileName={this.state.privateKeyFile} + onSubmit={this.removeCertificate} + disabled={!this.state.enable || !this.state.encrypt} + /> + ); + } else { + privKey = ( + + } + helpText={ + + } + uploadingText={Utils.localizeMessage('admin.saml.uploading.privateKey', 'Uploading Private Key...')} + disabled={!this.state.enable || !this.state.encrypt} + fileType='.key' + onSubmit={this.uploadCertificate} + /> + ); + } + + if (this.state.publicCertificateFile) { + pubCert = ( + + } + helpText={ + + } + removeButtonText={Utils.localizeMessage('admin.saml.remove.sp_certificate', 'Remove Service Provider Certificate')} + removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')} + fileName={this.state.publicCertificateFile} + onSubmit={this.removeCertificate} + disabled={!this.state.enable || !this.state.encrypt} + /> + ); + } else { + pubCert = ( + + } + helpText={ + + } + uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')} + disabled={!this.state.enable || !this.state.encrypt} + fileType='.crt' + onSubmit={this.uploadCertificate} + /> + ); + } + + return ( + + + } + helpText={ + + } + value={this.state.enable} + onChange={this.handleChange} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.idpUrlEx', 'Ex "https://idp.example.org/SAML2/SSO/Login"')} + helpText={ + + } + value={this.state.idpUrl} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.idpDescriptorUrlEx', 'Ex "https://idp.example.org/SAML2/issuer"')} + helpText={ + + } + value={this.state.idpDescriptorUrl} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + {idpCert} + + } + helpText={ + + } + value={this.state.verify} + disabled={!this.state.enable} + onChange={this.handleChange} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.assertionConsumerServiceURLEx', 'Ex "https:///login/sso/saml"')} + helpText={ + + } + value={this.state.assertionConsumerServiceURL} + onChange={this.handleChange} + disabled={!this.state.enable || !this.state.verify} + /> + + } + helpText={ + + } + value={this.state.encrypt} + disabled={!this.state.enable} + onChange={this.handleChange} + /> + {privKey} + {pubCert} + + } + placeholder={Utils.localizeMessage('admin.saml.emailAttrEx', 'Ex "Email" or "PrimaryEmail"')} + helpText={ + + } + value={this.state.emailAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.usernameAttrEx', 'Ex "Username"')} + helpText={ + + } + value={this.state.usernameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.firstnameAttrEx', 'Ex "FirstName"')} + helpText={ + + } + value={this.state.firstNameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.lastnameAttrEx', 'Ex "LastName"')} + helpText={ + + } + value={this.state.lastNameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.nicknameAttrEx', 'Ex "Nickname"')} + helpText={ + + } + value={this.state.nicknameAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.localeAttrEx', 'Ex "Locale" or "PrimaryLanguage"')} + helpText={ + + } + value={this.state.localeAttribute} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + } + placeholder={Utils.localizeMessage('admin.saml.loginButtonTextEx', 'Ex "With OKTA"')} + helpText={ + + } + value={this.state.loginButtonText} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + + ); + } +} \ No newline at end of file diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx index 62de50f0f..edded5aab 100644 --- a/webapp/components/admin_console/user_item.jsx +++ b/webapp/components/admin_console/user_item.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import Client from 'utils/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import * as Utils from 'utils/utils.jsx'; import UserStore from 'stores/user_store.jsx'; import ConfirmModal from '../confirm_modal.jsx'; @@ -374,12 +375,13 @@ export default class UserItem extends React.Component { let authServiceText; let passwordReset; if (user.auth_service) { + const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service); authServiceText = ( ); diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx index 6b0a90e8e..422b31a3a 100644 --- a/webapp/components/claim/components/email_to_oauth.jsx +++ b/webapp/components/claim/components/email_to_oauth.jsx @@ -3,6 +3,7 @@ import * as Utils from 'utils/utils.jsx'; import Client from 'utils/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -55,7 +56,8 @@ export default class EmailToOAuth extends React.Component { formClass += ' has-error'; } - const uiType = Utils.toTitleCase(this.props.newType) + ' SSO'; + const type = (this.props.newType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.newType)); + const uiType = `${type} SSO`; return (
@@ -74,7 +76,7 @@ export default class EmailToOAuth extends React.Component { id='claim.email_to_oauth.ssoType' defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO' values={{ - type: Utils.toTitleCase(this.props.newType) + type }} />

@@ -83,7 +85,7 @@ export default class EmailToOAuth extends React.Component { id='claim.email_to_oauth.ssoNote' defaultMessage='You must already have a valid {type} account' values={{ - type: Utils.toTitleCase(this.props.newType) + type }} />

diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx index 17ca12264..6a0f6431b 100644 --- a/webapp/components/claim/components/oauth_to_email.jsx +++ b/webapp/components/claim/components/oauth_to_email.jsx @@ -3,6 +3,7 @@ import * as Utils from 'utils/utils.jsx'; import Client from 'utils/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -62,7 +63,7 @@ export default class OAuthToEmail extends React.Component { formClass += ' has-error'; } - const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO'; + const uiType = `${(this.props.currentType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.currentType))} SSO`; return (
@@ -85,7 +86,7 @@ export default class OAuthToEmail extends React.Component {

+ + {window.mm_config.SamlLoginButtonText} + + + ); + } + return (

{extraBox} diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx index c7ddfc91b..fa5e9268e 100644 --- a/webapp/components/signup_user_complete.jsx +++ b/webapp/components/signup_user_complete.jsx @@ -588,6 +588,20 @@ export default class SignupUserComplete extends React.Component { ); } + if (global.window.mm_config.EnableSaml === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true') { + signupMessage.push( + + + {global.window.mm_config.SamlLoginButtonText} + + + ); + } + let ldapSignup; if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) { ldapSignup = ( diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx index f8910b9bc..5e821a26a 100644 --- a/webapp/components/user_settings/user_settings_general.jsx +++ b/webapp/components/user_settings/user_settings_general.jsx @@ -412,6 +412,24 @@ class UserSettingsGeneralTab extends React.Component { {helpText}
); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + inputs.push( +
+
+ +
+ {helpText} +
+ ); } emailSection = ( @@ -478,6 +496,16 @@ class UserSettingsGeneralTab extends React.Component { }} /> ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + + ); } emailSection = ( diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index af7aeb3c6..247dc0f81 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -620,6 +620,24 @@ class SecurityTab extends React.Component { ); } + let samlOption; + if (global.window.mm_config.EnableSaml === 'true' && user.auth_service === '') { + samlOption = ( +
+ + +
+
+ ); + } + const inputs = []; inputs.push(
@@ -627,6 +645,7 @@ class SecurityTab extends React.Component { {gitlabOption}
{ldapOption} + {samlOption} {googleOption}
); @@ -681,6 +700,13 @@ class SecurityTab extends React.Component { defaultMessage='LDAP' /> ); + } else if (this.props.user.auth_service === Constants.SAML_SERVICE) { + describe = ( + + ); } return ( @@ -701,6 +727,7 @@ class SecurityTab extends React.Component { numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods; numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods; numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods; + numMethods = global.window.mm_config.EnableSaml === 'true' ? numMethods + 1 : numMethods; let signInSection; if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) { -- cgit v1.2.3-1-g7c22