diff --git a/apps/meteor/app/meteor-accounts-saml/server/startup.ts b/apps/meteor/app/meteor-accounts-saml/server/startup.ts
index 7a2bf16d3244e..556ab7df13e7d 100644
--- a/apps/meteor/app/meteor-accounts-saml/server/startup.ts
+++ b/apps/meteor/app/meteor-accounts-saml/server/startup.ts
@@ -1,4 +1,5 @@
import { Logger } from '@rocket.chat/logger';
+import debounce from 'lodash.debounce';
import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings/server';
@@ -10,5 +11,6 @@ SAMLUtils.setLoggerInstance(logger);
Meteor.startup(async () => {
await addSettings('Default');
- settings.watchByRegex(/^SAML_.+/, loadSamlServiceProviders);
});
+
+settings.watchByRegex(/^SAML_.+/, debounce(loadSamlServiceProviders, 2000));
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index 4a7ee0f11474d..8fcc9eeac067b 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -168,6 +168,7 @@
"chai-dom": "^1.11.0",
"chai-spies": "~1.0.0",
"cross-env": "^7.0.3",
+ "docker-compose": "^0.24.3",
"emojione-assets": "^4.5.0",
"eslint": "~8.45.0",
"eslint-config-prettier": "~8.8.0",
diff --git a/apps/meteor/tests/e2e/config/constants.ts b/apps/meteor/tests/e2e/config/constants.ts
index 3e9b693cc7dcd..c938b693ff450 100644
--- a/apps/meteor/tests/e2e/config/constants.ts
+++ b/apps/meteor/tests/e2e/config/constants.ts
@@ -15,3 +15,8 @@ export const ADMIN_CREDENTIALS = {
password: 'rocketchat.internal.admin.test',
username: 'rocketchat.internal.admin.test',
} as const;
+
+export const DEFAULT_USER_CREDENTIALS = {
+ password: 'password',
+ bcrypt: '$2b$10$LNYaqDreDE7tt9EVEeaS9uw.C3hic9hcqFfIocMBPTMxJaDCC6QWW',
+} as const;
diff --git a/apps/meteor/tests/e2e/containers/saml/Dockerfile b/apps/meteor/tests/e2e/containers/saml/Dockerfile
new file mode 100644
index 0000000000000..be87fdb742b65
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/Dockerfile
@@ -0,0 +1,35 @@
+FROM php:7.1-apache
+
+# Utilities
+RUN apt-get update && \
+ apt-get -y install apt-transport-https git curl vim --no-install-recommends && \
+ rm -r /var/lib/apt/lists/*
+
+# SimpleSAMLphp
+ARG SIMPLESAMLPHP_VERSION=1.15.2
+RUN curl -s -L -o /tmp/simplesamlphp.tar.gz https://github.com/simplesamlphp/simplesamlphp/releases/download/v$SIMPLESAMLPHP_VERSION/simplesamlphp-$SIMPLESAMLPHP_VERSION.tar.gz && \
+ tar xzf /tmp/simplesamlphp.tar.gz -C /tmp && \
+ rm -f /tmp/simplesamlphp.tar.gz && \
+ mv /tmp/simplesamlphp-* /var/www/simplesamlphp && \
+ touch /var/www/simplesamlphp/modules/exampleauth/enable
+COPY config/simplesamlphp/config.php /var/www/simplesamlphp/config
+COPY config/simplesamlphp/authsources.php /var/www/simplesamlphp/config
+COPY config/simplesamlphp/saml20-sp-remote.php /var/www/simplesamlphp/metadata
+COPY config/simplesamlphp/server_crt /var/www/simplesamlphp/cert/server.crt
+COPY config/simplesamlphp/server_pem /var/www/simplesamlphp/cert/server.pem
+
+# Apache
+COPY config/apache/ports.conf /etc/apache2
+COPY config/apache/simplesamlphp.conf /etc/apache2/sites-available
+COPY config/apache/cert_crt /etc/ssl/cert/cert.crt
+COPY config/apache/private_key /etc/ssl/private/private.key
+RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf && \
+ a2enmod ssl && \
+ a2dissite 000-default.conf default-ssl.conf && \
+ a2ensite simplesamlphp.conf
+
+# Set work dir
+WORKDIR /var/www/simplesamlphp
+
+# General setup
+EXPOSE 8080 8443
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt b/apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt
new file mode 100644
index 0000000000000..4104eaf80f6e4
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJANdMEUvsTntJMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzMjIxWhcNNDgwNjI1MTQzMjIxWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAyXRKD3KzV/hOqwThFRtA+eoitJpIIEmWbugPMC1G+7bFqxHmtxiuQhOw
+yHrij35biiD8VpboY69Zep7n1QCfmIodh9uxdaNxFtzxjryRLzfP3MpPFkpBHCdV
+HZWDP2TzIvOxWcnlLmikSnBrBM1nvhKSWjaFsjDAXMLXT0mceiDpQ0QQkDA6RAyx
+JWWRJILjudBh56ukqvdz4eFWAAViZX5MUwCDxiBxtP3NIXVmODM7kDLqZ9+QcfpM
+N5QvfcUjHP9584yrYiJ9N64Fy5vU2OH1RX5EsMHTtdqR4H5K6zNfVlgRSG170Mcj
+ksTSbo1kcDCSuzTO82NrVkU+R78W/QIDAQABo1AwTjAdBgNVHQ4EFgQUkYMQMPqY
+leTTtBHS1f7yNFmY86QwHwYDVR0jBBgwFoAUkYMQMPqYleTTtBHS1f7yNFmY86Qw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGZpbEWzYLoa5keg9rQDa
+S2cf9rQFMNflwR7hQ6OtSXeP0JsQ6yFhRq3TgpBdbkmdealDJbyG8pWQxqwOBD/j
+45jr+NsHap0HvQAg9pfq/QIqjH5osCGAHmNdHfP638FOpi0s6hAX11+nCW6ClHMO
+ofn0fYXCBtM18qZvtoeB8swi6MlRXFLvvRL4tWzGugdQj+0f+ukk/GZAEitdpkuj
+qFCHqfKwg9FJ4My5M8lIyB/P4+SK/ail/BoCJ/qGBXky2bob0MQuLysd35zrTA62
+j5IvPp1XZj/2KnuPtqMuFhNtE5wCnOEC1WG02ZVIfs4DAxX78z59VSEaFlnstT9k
+aQ==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/ports.conf b/apps/meteor/tests/e2e/containers/saml/config/apache/ports.conf
new file mode 100644
index 0000000000000..286af7fd4ec36
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/apache/ports.conf
@@ -0,0 +1,9 @@
+Listen 8080
+
+
+ Listen 8443
+
+
+
+ Listen 8443
+
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/private_key b/apps/meteor/tests/e2e/containers/saml/config/apache/private_key
new file mode 100644
index 0000000000000..f511efcd9b655
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/apache/private_key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJdEoPcrNX+E6r
+BOEVG0D56iK0mkggSZZu6A8wLUb7tsWrEea3GK5CE7DIeuKPfluKIPxWluhjr1l6
+nufVAJ+Yih2H27F1o3EW3PGOvJEvN8/cyk8WSkEcJ1UdlYM/ZPMi87FZyeUuaKRK
+cGsEzWe+EpJaNoWyMMBcwtdPSZx6IOlDRBCQMDpEDLElZZEkguO50GHnq6Sq93Ph
+4VYABWJlfkxTAIPGIHG0/c0hdWY4MzuQMupn35Bx+kw3lC99xSMc/3nzjKtiIn03
+rgXLm9TY4fVFfkSwwdO12pHgfkrrM19WWBFIbXvQxyOSxNJujWRwMJK7NM7zY2tW
+RT5Hvxb9AgMBAAECggEAVT4KtHyxXJDqIL1gzICKvvUOmGMMD/VzXRx+iMEv3wTY
+oWliuakM21LfpAUzZspty4XnoHAch0nET/l7WYr4/R+8HSed8IwnJyh4YhByUouI
+PgGw81qaMGKIRotkTOfXZbu+GKMwgbGvivwEnLSZqDjNirS1X8/3JYkgeCFKv/X6
+K4Z1SKoebtA4oGDGZtxNpeGJ6TibaSO3PRW+oH3dD5j5ez1k+dlRasMnuKG76jBy
+naz052t52UZxAnGkbZUU84VljO5R8x1jip+M6+qILC/PsI1hb45Dx5VRXNUBGP2s
+/PRVFptNGmsIlXBYMSdOo9RJAIsZ1kTty34nIqeuYQKBgQD4Nv+QoeRXjG7iAmzj
+JabcA+2FiHftvjdaYjahXQ/Ma6ns/vv//tVfxJaNyCxMObjIKhidsrf/AAn03FgT
+LkL0Q2tTBNL9jejzHstnCKSSu/pSgqatsABm1cokC9qsuMjweMVy+tGjSsF6YgjI
+2K0heQpF0bMBSYmyQU4aTLbytQKBgQDPxdWVn7ReCscDwpchx+zffKh5JOshW44z
+GYBfVbugBljj9w6fDjKyQzk0jMGmuG9rvqfZzu9MpQhjBFKxL4aXPBN7bQOY3DKc
+FuGSbIbV7ldPmZhOxqLy6NLyiry6eSQGNf1Q8YCF6bsUR/rvdKDtBLrbJObgRVbu
+ChT2PXRYKQKBgFSEDZMGvMReqebE4qSZTm592+Nq60MFUL2y0V0yXc3CHxL2Y4Hw
+GGFKg+T08rhlsxhc1RLlJqdqMPmyCT9Gsj+PsTyMWPdC2b3mj2We2MKpxPtRR0W+
+tvRM+U46xxOmu6y9wqV65+TM8IImXU1eEd1i5G+Pjn7ytjL+74Qe+PA9AoGAEasM
+F5YmG10lQU+Z1HiQzwxlsy+Ngx+q/vNrNDAxLVF8253Vs3bcnsYSpkJV8Vx7tRjY
+YzAyrzzVcr4aXhDhjBjCu1sw1B3de+KCOhZafPSwngc8qW5AyxE7Zv6fP+gvRQvw
+R6LRwBF5JCde0mADk0Q0s4/2xhl/Y+ydjbb6HskCgYEAysuIUrDslGGsK57loxMO
+FVz9SmLTZIJqkSW+l3dDHMG+BvnJFP+yf0Kr2zGbXRzOvNVWAFP2aU39RFDxbUIM
+Nz7obLWrVKbQUDaU7fCbP4OBVuo9p4UM6j/PZy+3Cyps+GMTrC9wi4HbbNEWHyCW
+xaNL9LNQ3B9hJG77htK1oRw=
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/simplesamlphp.conf b/apps/meteor/tests/e2e/containers/saml/config/apache/simplesamlphp.conf
new file mode 100644
index 0000000000000..a81c39d85f8b7
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/apache/simplesamlphp.conf
@@ -0,0 +1,23 @@
+
+ ServerName localhost
+ DocumentRoot /var/www/simplesamlphp
+ Alias /simplesaml /var/www/simplesamlphp/www
+
+
+ Require all granted
+
+
+
+
+ ServerName localhost
+ DocumentRoot /var/www/simplesamlphp
+ SSLEngine on
+ SSLCertificateFile /etc/ssl/cert/cert.crt
+ SSLCertificateKeyFile /etc/ssl/private/private.key
+ Alias /simplesaml /var/www/simplesamlphp/www
+
+
+ Require all granted
+
+
+
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/authsources.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/authsources.php
new file mode 100644
index 0000000000000..867d66049b3ac
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/authsources.php
@@ -0,0 +1,34 @@
+ array(
+ 'core:AdminPassword',
+ ),
+
+ 'example-userpass' => array(
+ 'exampleauth:UserPass',
+ 'samluser1:password' => array(
+ 'uid' => array('1'),
+ 'username' => 'samluser1',
+ 'cn' => 'Saml User 1',
+ 'eduPersonAffiliation' => array('group1'),
+ 'email' => 'samluser1@example.com',
+ ),
+ 'samluser2:password' => array(
+ 'uid' => array('2'),
+ 'username' => 'samluser2',
+ 'cn' => 'Saml User 2',
+ 'eduPersonAffiliation' => array('group2'),
+ 'email' => 'user_for_saml_merge@email.com',
+ ),
+ 'samluser3:password' => array(
+ 'uid' => array('3'),
+ 'username' => 'user_for_saml_merge2',
+ 'cn' => 'Saml User 3',
+ 'eduPersonAffiliation' => array('group2'),
+ 'email' => 'samluser3@example.com',
+ ),
+ ),
+
+);
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php
new file mode 100644
index 0000000000000..79e73e6b58c43
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php
@@ -0,0 +1,111 @@
+ 'simplesaml/',
+ 'certdir' => 'cert/',
+ 'loggingdir' => 'log/',
+ 'datadir' => 'data/',
+ 'tempdir' => '/tmp/simplesaml',
+ 'debug' => true,
+ 'showerrors' => true,
+ 'errorreporting' => true,
+ 'debug.validatexml' => false,
+ 'auth.adminpassword' => 'secret',
+ 'admin.protectindexpage' => false,
+ 'admin.protectmetadata' => false,
+ 'secretsalt' => 'defaultsecretsalt',
+ 'technicalcontact_name' => 'Administrator',
+ 'technicalcontact_email' => 'na@example.org',
+ 'timezone' => null,
+ 'logging.level' => SimpleSAML_Logger::DEBUG,
+ 'logging.handler' => 'errorlog',
+ //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg',
+ 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER,
+ 'logging.processname' => 'simplesamlphp',
+ 'logging.logfile' => 'simplesamlphp.log',
+ 'statistics.out' => array(
+ ),
+ 'database.dsn' => 'mysql:host=localhost;dbname=saml',
+ 'database.username' => 'simplesamlphp',
+ 'database.password' => 'secret',
+ 'database.prefix' => '',
+ 'database.persistent' => false,
+ 'database.slaves' => array(
+ ),
+ 'enable.saml20-idp' => true,
+ 'enable.shib13-idp' => true,
+ 'enable.adfs-idp' => false,
+ 'enable.wsfed-sp' => false,
+ 'enable.authmemcookie' => false,
+ 'session.duration' => 8 * (60 * 60), // 8 hours.
+ 'session.datastore.timeout' => (4 * 60 * 60), // 4 hours
+ 'session.state.timeout' => (60 * 60), // 1 hour
+ 'session.cookie.name' => 'SimpleSAMLSessionIDIdp',
+ 'session.cookie.lifetime' => 0,
+ 'session.cookie.path' => '/',
+ 'session.cookie.domain' => null,
+ 'session.cookie.secure' => false,
+ 'enable.http_post' => false,
+ 'session.phpsession.cookiename' => 'PHPSESSIDIDP',
+ 'session.phpsession.savepath' => null,
+ 'session.phpsession.httponly' => true,
+ 'session.authtoken.cookiename' => 'SimpleSAMLAuthTokenIdp',
+ 'session.rememberme.enable' => false,
+ 'session.rememberme.checked' => false,
+ 'session.rememberme.lifetime' => (14 * 86400),
+ 'language.available' => array(
+ 'en', 'no', 'nn', 'se', 'da', 'de', 'sv', 'fi', 'es', 'fr', 'it', 'nl', 'lb', 'cs',
+ 'sl', 'lt', 'hr', 'hu', 'pl', 'pt', 'pt-br', 'tr', 'ja', 'zh', 'zh-tw', 'ru', 'et',
+ 'he', 'id', 'sr', 'lv', 'ro', 'eu'
+ ),
+ 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'),
+ 'language.default' => 'en',
+ 'language.parameter.name' => 'language',
+ 'language.parameter.setcookie' => true,
+ 'language.cookie.name' => 'language',
+ 'language.cookie.domain' => null,
+ 'language.cookie.path' => '/',
+ 'language.cookie.lifetime' => (60 * 60 * 24 * 900),
+ 'attributes.extradictionary' => null,
+ 'theme.use' => 'default',
+ 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost',
+ 'idpdisco.enableremember' => true,
+ 'idpdisco.rememberchecked' => true,
+ 'idpdisco.validate' => true,
+ 'idpdisco.extDiscoveryStorage' => null,
+ 'idpdisco.layout' => 'dropdown',
+ 'shib13.signresponse' => true,
+ 'authproc.idp' => array(
+ 30 => 'core:LanguageAdaptor',
+ 45 => array(
+ 'class' => 'core:StatisticsWithAttribute',
+ 'attributename' => 'realm',
+ 'type' => 'saml20-idp-SSO',
+ ),
+ 50 => 'core:AttributeLimit',
+ 99 => 'core:LanguageAdaptor',
+ ),
+ 'authproc.sp' => array(
+ 90 => 'core:LanguageAdaptor',
+ ),
+ 'metadata.sources' => array(
+ array('type' => 'flatfile'),
+ ),
+ 'store.type' => 'phpsession',
+ 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3',
+ 'store.sql.username' => null,
+ 'store.sql.password' => null,
+ 'store.sql.prefix' => 'SimpleSAMLphp',
+ 'memcache_store.servers' => array(
+ array(
+ array('hostname' => 'localhost'),
+ ),
+ ),
+ 'memcache_store.prefix' => null,
+ 'memcache_store.expires' => 36 * (60 * 60), // 36 hours.
+ 'metadata.sign.enable' => false,
+ 'metadata.sign.privatekey' => null,
+ 'metadata.sign.privatekey_pass' => null,
+ 'metadata.sign.certificate' => null,
+ 'proxy' => null,
+ 'trusted.url.domains' => array(),
+);
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/saml20-sp-remote.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/saml20-sp-remote.php
new file mode 100644
index 0000000000000..79591af7dfa3d
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/saml20-sp-remote.php
@@ -0,0 +1,24 @@
+ 'http://localhost:3000/_saml/metadata/test-sp',
+ 'contacts' => array (
+ ),
+ 'metadata-set' => 'saml20-sp-remote',
+ 'AssertionConsumerService' => array (
+ 0 => array (
+ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ 'Location' => 'http://localhost:3000/_saml/validate/test-sp',
+ 'index' => 1,
+ 'isDefault' => true,
+ ),
+ ),
+ 'SingleLogoutService' => array (
+ 0 => array (
+ 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ 'Location' => 'http://localhost:3000/_saml/logout/test-sp/',
+ 'ResponseLocation' => 'http://localhost:3000/_saml/logout/test-sp/',
+ ),
+ ),
+ 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+);
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_crt b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_crt
new file mode 100644
index 0000000000000..f0623b9305e07
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav
+Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+
+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc
++TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix
+YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8
+jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C
+YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b
+lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs
+X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7
+yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7
+NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
+99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n
+aQ==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_pem b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_pem
new file mode 100644
index 0000000000000..ba2bac5fe6308
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz
+SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR
+50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq
+qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r
+DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa
+ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa
+CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY
+y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP
+rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5
+PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp
+TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1
+1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR
+TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX
+SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq
+V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L
+0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz
+lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY
+n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN
+LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9
+gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09
+oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw
+n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4
+2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o
+CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF
+fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp
+Sm75WXsflOxuTn08LbgGc4s=
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/apps/meteor/tests/e2e/containers/saml/docker-compose.yml b/apps/meteor/tests/e2e/containers/saml/docker-compose.yml
new file mode 100644
index 0000000000000..6d8a00f8eba9c
--- /dev/null
+++ b/apps/meteor/tests/e2e/containers/saml/docker-compose.yml
@@ -0,0 +1,7 @@
+version: '3'
+services:
+ testsamlidp_idp:
+ build: .
+ ports:
+ - "8080:8080"
+ - "8443:8443"
diff --git a/apps/meteor/tests/e2e/fixtures/collections/users.ts b/apps/meteor/tests/e2e/fixtures/collections/users.ts
index cc437597a5e01..5b85974fb57c2 100644
--- a/apps/meteor/tests/e2e/fixtures/collections/users.ts
+++ b/apps/meteor/tests/e2e/fixtures/collections/users.ts
@@ -1,7 +1,8 @@
import { faker } from '@faker-js/faker';
import type { IUser } from '@rocket.chat/core-typings';
-import type { IUserState } from '../userStates';
+import { DEFAULT_USER_CREDENTIALS } from '../../config/constants';
+import { type IUserState } from '../userStates';
type UserFixture = IUser & {
username: string;
@@ -23,7 +24,7 @@ export function createUserFixture(user: IUserState): UserFixture {
utcOffset: -3,
username,
services: {
- password: { bcrypt: '$2b$10$EMxaeQQbSw9JLL.YvOVPaOW8MKta6pgmp2BcN5Op4cC9bJiOqmUS.' },
+ password: { bcrypt: DEFAULT_USER_CREDENTIALS.bcrypt },
email2fa: { enabled: true, changedAt: new Date() },
email: {
verificationTokens: [
diff --git a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts
index 38835db4aaa6b..e7e68790cf3dd 100644
--- a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts
+++ b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts
@@ -57,6 +57,22 @@ export default async function injectInitialData() {
_id: 'API_Enable_Rate_Limiter_Dev',
value: false,
},
+ {
+ _id: 'SAML_Custom_Default_provider',
+ value: 'test-sp',
+ },
+ {
+ _id: 'SAML_Custom_Default_issuer',
+ value: 'http://localhost:3000/_saml/metadata/test-sp',
+ },
+ {
+ _id: 'SAML_Custom_Default_entry_point',
+ value: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',
+ },
+ {
+ _id: 'SAML_Custom_Default_idp_slo_redirect_url',
+ value: 'http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php',
+ },
{
_id: 'Accounts_OAuth_Google',
value: false,
diff --git a/apps/meteor/tests/e2e/fixtures/userStates.ts b/apps/meteor/tests/e2e/fixtures/userStates.ts
index 7bcab213f8fce..f21405a94f02e 100644
--- a/apps/meteor/tests/e2e/fixtures/userStates.ts
+++ b/apps/meteor/tests/e2e/fixtures/userStates.ts
@@ -86,6 +86,10 @@ export const Users = {
user1: generateContext('user1'),
user2: generateContext('user2'),
user3: generateContext('user3'),
+ samluser1: generateContext('samluser1'),
+ samluser2: generateContext('samluser2'),
+ userForSamlMerge: generateContext('user_for_saml_merge'),
+ userForSamlMerge2: generateContext('user_for_saml_merge2'),
admin: generateContext('rocketchat.internal.admin.test'),
};
diff --git a/apps/meteor/tests/e2e/login.spec.ts b/apps/meteor/tests/e2e/login.spec.ts
index 958f5120f1422..41710fffa203c 100644
--- a/apps/meteor/tests/e2e/login.spec.ts
+++ b/apps/meteor/tests/e2e/login.spec.ts
@@ -1,13 +1,16 @@
import { faker } from '@faker-js/faker';
-import { Registration } from './page-objects';
+import { DEFAULT_USER_CREDENTIALS } from './config/constants';
+import { Utils, Registration } from './page-objects';
import { test, expect } from './utils/test';
test.describe.parallel('Login', () => {
let poRegistration: Registration;
+ let poUtils: Utils;
test.beforeEach(async ({ page }) => {
poRegistration = new Registration(page);
+ poUtils = new Utils(page);
await page.goto('/home');
});
@@ -27,4 +30,24 @@ test.describe.parallel('Login', () => {
await expect(poRegistration.inputPassword).toBeInvalid();
});
});
+
+ test('Login with valid username and password', async () => {
+ await test.step('expect successful login', async () => {
+ await poRegistration.username.type('user1');
+ await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password);
+ await poRegistration.btnLogin.click();
+
+ await expect(poUtils.mainContent).toBeVisible();
+ });
+ });
+
+ test('Login with valid email and password', async () => {
+ await test.step('expect successful login', async () => {
+ await poRegistration.username.type('user1@email.com');
+ await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password);
+ await poRegistration.btnLogin.click();
+
+ await expect(poUtils.mainContent).toBeVisible();
+ });
+ });
});
diff --git a/apps/meteor/tests/e2e/page-objects/auth.ts b/apps/meteor/tests/e2e/page-objects/auth.ts
index d0a7e13d65055..98421f6461ab7 100644
--- a/apps/meteor/tests/e2e/page-objects/auth.ts
+++ b/apps/meteor/tests/e2e/page-objects/auth.ts
@@ -20,6 +20,10 @@ export class Registration {
return this.page.locator('role=button[name="Login"]');
}
+ get btnLoginWithSaml(): Locator {
+ return this.page.locator('role=button[name="SAML"]');
+ }
+
get btnLoginWithGoogle(): Locator {
return this.page.locator('role=button[name="Sign in with Google"]');
}
diff --git a/apps/meteor/tests/e2e/saml.spec.ts b/apps/meteor/tests/e2e/saml.spec.ts
new file mode 100644
index 0000000000000..faa6b25710c28
--- /dev/null
+++ b/apps/meteor/tests/e2e/saml.spec.ts
@@ -0,0 +1,290 @@
+import child_process from 'child_process';
+import path from 'path';
+
+import { Page } from '@playwright/test';
+import { v2 as compose } from 'docker-compose'
+import { MongoClient } from 'mongodb';
+
+import * as constants from './config/constants';
+import { createUserFixture } from './fixtures/collections/users';
+import { Users } from './fixtures/userStates';
+import { Registration } from './page-objects';
+import { getUserInfo } from './utils/getUserInfo';
+import { setSettingValueById } from './utils/setSettingValueById';
+import { test, expect } from './utils/test';
+
+const resetTestData = async (cleanupOnly = false) => {
+ // Reset saml users' data on mongo in the beforeAll hook to allow re-running the tests within the same playwright session
+ // This is needed because those tests will modify this data and running them a second time would trigger different code paths
+ const connection = await MongoClient.connect(constants.URL_MONGODB);
+
+ const usernamesToDelete = [Users.userForSamlMerge, Users.userForSamlMerge2, Users.samluser1, Users.samluser2].map(({ data: { username }}) => username);
+ await connection
+ .db()
+ .collection('users')
+ .deleteMany({
+ username: {
+ $in: usernamesToDelete,
+ }
+ });
+
+ if (cleanupOnly) {
+ return;
+ }
+
+ const usersFixtures = [Users.userForSamlMerge, Users.userForSamlMerge2].map((user) => createUserFixture(user));
+ await Promise.all(
+ usersFixtures.map((user) =>
+ connection.db().collection('users').updateOne({ username: user.username }, { $set: user }, { upsert: true }),
+ ),
+ );
+
+ await Promise.all(
+ [
+ {
+ _id: 'SAML_Custom_Default_logout_behaviour',
+ value: 'SAML',
+ },
+ {
+ _id: 'SAML_Custom_Default_immutable_property',
+ value: 'EMail',
+ },
+ {
+ _id: 'SAML_Custom_Default_mail_overwrite',
+ value: false,
+ },
+ {
+ _id: 'SAML_Custom_Default',
+ value: false,
+ },
+ ].map((setting) =>
+ connection
+ .db()
+ .collection('rocketchat_settings')
+ .updateOne({ _id: setting._id }, { $set: { value: setting.value } }),
+ ),
+ );
+};
+
+test.describe('SAML', () => {
+ let poRegistration: Registration;
+
+ const containerPath = path.join(__dirname, 'containers', 'saml');
+
+ test.beforeAll(async ({ api }) => {
+ await resetTestData();
+
+ // Only one setting updated through the API to avoid refreshing the service configurations several times
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default', true)).status()).toBe(200);
+
+ await compose.buildOne('testsamlidp_idp', {
+ cwd: containerPath,
+ });
+
+ await compose.upOne('testsamlidp_idp', {
+ cwd: containerPath,
+ });
+ });
+
+ test.afterAll(async () => {
+ await compose.down({
+ cwd: containerPath,
+ });
+
+ // the compose CLI doesn't have any way to remove images, so try to remove it with a direct call to the docker cli, but ignore errors if it fails.
+ try {
+ child_process.spawn('docker', ['rmi', 'saml-testsamlidp_idp'], {
+ cwd: containerPath,
+ });
+ } catch {
+ // ignore errors here
+ }
+
+ // Remove saml test users so they don't interfere with other tests
+ await resetTestData(true);
+ });
+
+ test.beforeEach(async ({ page }) => {
+ poRegistration = new Registration(page);
+
+ await page.goto('/home');
+ });
+
+ test('Login', async ({ page, api }) => {
+ await test.step('expect to have SAML login button available', async () => {
+ await expect(poRegistration.btnLoginWithSaml).toBeVisible({ timeout: 10000 });
+ });
+
+ await test.step('expect to be redirected to the IdP for login', async () => {
+ await poRegistration.btnLoginWithSaml.click();
+
+ await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
+ });
+
+ await test.step('expect to be redirected back on successful login', async () => {
+ await page.getByLabel('Username').fill('samluser1');
+ await page.getByLabel('Password').fill('password');
+ await page.locator('role=button[name="Login"]').click();
+
+ await expect(page).toHaveURL('/home');
+ });
+
+ await test.step('expect user data to have been mapped to the correct fields', async () => {
+ const user = await getUserInfo(api, 'samluser1');
+
+ expect(user).toBeDefined();
+ expect(user?.username).toBe('samluser1');
+ expect(user?.name).toBe('Saml User 1');
+ expect(user?.emails).toBeDefined();
+ expect(user?.emails?.[0].address).toBe('samluser1@example.com');
+ });
+ });
+
+ const doLoginStep = async (page: Page, username: string) => {
+ await test.step('expect successful login', async () => {
+ await poRegistration.btnLoginWithSaml.click();
+ // Redirect to Idp
+ await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
+
+ // Fill username and password
+ await page.getByLabel('Username').fill(username);
+ await page.getByLabel('Password').fill('password');
+ await page.locator('role=button[name="Login"]').click();
+
+ // Redirect back to rocket.chat
+ await expect(page).toHaveURL('/home');
+
+ await expect(page.getByLabel('User Menu')).toBeVisible();
+ });
+ };
+
+ const doLogoutStep = async (page: Page) => {
+ await test.step('logout', async () => {
+ await page.getByLabel('User Menu').click();
+ await page.locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Logout")]').click();
+
+ await expect(page).toHaveURL('/home');
+ await expect(page.getByLabel('User Menu')).not.toBeVisible();
+ });
+ };
+
+ test('Logout - Rocket.Chat only', async ({ page, api }) => {
+ await test.step('Configure logout to only logout from Rocket.Chat', async () => {
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'Local')).status()).toBe(200);
+ });
+
+ await doLoginStep(page, 'samluser1');
+ await doLogoutStep(page);
+
+ await test.step('expect IdP to redirect back automatically on new login request', async () => {
+ await poRegistration.btnLoginWithSaml.click();
+
+ await expect(page).toHaveURL('/home');
+ });
+ });
+
+ test('Logout - Single Sign Out', async ({ page, api }) => {
+ await test.step('Configure logout to terminate SAML session', async () => {
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'SAML')).status()).toBe(200);
+ })
+
+ await doLoginStep(page, 'samluser1');
+ await doLogoutStep(page);
+
+ await test.step('expect IdP to show login form on new login request', async () => {
+ await poRegistration.btnLoginWithSaml.click();
+
+ await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/);
+ await expect(page.getByLabel('Username')).toBeVisible();
+ });
+ });
+
+ test('User Merge - By Email', async ({ page, api }) => {
+ await test.step('Configure SAML to identify users by email', async () => {
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'EMail')).status()).toBe(200);
+ });
+
+ await doLoginStep(page, 'samluser2');
+
+ await test.step('expect user data to have been mapped to the correct fields', async () => {
+ const user = await getUserInfo(api, 'samluser2');
+
+ expect(user).toBeDefined();
+ expect(user?._id).toBe('user_for_saml_merge');
+ expect(user?.username).toBe('samluser2');
+ expect(user?.name).toBe('Saml User 2');
+ expect(user?.emails).toBeDefined();
+ expect(user?.emails?.[0].address).toBe('user_for_saml_merge@email.com');
+ });
+ });
+
+ test('User Merge - By Username', async ({ page, api }) => {
+ await test.step('Configure SAML to identify users by username', async () => {
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', false)).status()).toBe(200);
+ });
+
+ await doLoginStep(page, 'samluser3');
+
+ await test.step('expect user data to have been mapped to the correct fields', async () => {
+ const user = await getUserInfo(api, 'user_for_saml_merge2');
+
+ expect(user).toBeDefined();
+ expect(user?._id).toBe('user_for_saml_merge2');
+ expect(user?.username).toBe('user_for_saml_merge2');
+ expect(user?.name).toBe('Saml User 3');
+ expect(user?.emails).toBeDefined();
+ expect(user?.emails?.[0].address).toBe('user_for_saml_merge2@email.com');
+ });
+ });
+
+ test('User Merge - By Username with Email Override', async ({ page, api }) => {
+ await test.step('Configure SAML to identify users by username', async () => {
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200);
+ await expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', true)).status()).toBe(200);
+ });
+
+ await doLoginStep(page, 'samluser3');
+
+ await test.step('expect user data to have been mapped to the correct fields', async () => {
+ const user = await getUserInfo(api, 'user_for_saml_merge2');
+
+ expect(user).toBeDefined();
+ expect(user?._id).toBe('user_for_saml_merge2');
+ expect(user?.username).toBe('user_for_saml_merge2');
+ expect(user?.name).toBe('Saml User 3');
+ expect(user?.emails).toBeDefined();
+ expect(user?.emails?.[0].address).toBe('samluser3@example.com');
+ });
+ });
+
+ test.fixme('User Merge - By Custom Identifier', async () => {
+ // Test user merge with a custom identifier configured in the fieldmap
+ });
+
+ test.fixme('Signature Validation', async () => {
+ // Test login with signed responses
+ });
+
+ test.fixme('Login - User without username', async () => {
+ // Test login with a SAML user with no username
+ // Test different variations of the Immutable Property setting
+ });
+
+ test.fixme('Login - User without email', async () => {
+ // Test login with a SAML user with no email
+ // Test different variations of the Immutable Property setting
+ });
+
+ test.fixme('Login - User without name', async () => {
+ // Test login with a SAML user with no name
+ });
+
+ test.fixme('Login - User with channels attribute', async () => {
+ // Test login with a SAML user with a "channels" attribute
+ });
+
+ test.fixme('Data Sync - Custom Field Map', async () => {
+ // Test the data sync using a custom fieldmap setting
+ });
+});
diff --git a/apps/meteor/tests/e2e/utils/getUserInfo.ts b/apps/meteor/tests/e2e/utils/getUserInfo.ts
new file mode 100644
index 0000000000000..13c592a7244b7
--- /dev/null
+++ b/apps/meteor/tests/e2e/utils/getUserInfo.ts
@@ -0,0 +1,15 @@
+import { IUser } from '@rocket.chat/core-typings';
+
+import type { BaseTest } from './test';
+
+export const getUserInfo = async (api: BaseTest['api'], username: string): Promise => {
+ const response = await api.get(`/users.info?username=${username}`);
+
+ if (response.status() !== 200) {
+ throw new Error('Failed to get user info.');
+ }
+
+ const data = await response.json();
+
+ return data.user;
+}
diff --git a/yarn.lock b/yarn.lock
index 645a843be1e01..7565bb974e76d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9889,6 +9889,7 @@ __metadata:
date-fns: ^2.28.0
date.js: ~0.3.3
debug: ~4.1.1
+ docker-compose: ^0.24.3
dompurify: ^2.3.8
ejson: ^2.2.3
emailreplyparser: ^0.0.5
@@ -21185,6 +21186,15 @@ __metadata:
languageName: node
linkType: hard
+"docker-compose@npm:^0.24.3":
+ version: 0.24.3
+ resolution: "docker-compose@npm:0.24.3"
+ dependencies:
+ yaml: ^2.2.2
+ checksum: b2149eafb6e0a37ff4595044fe63d2fac23483afab06bca71cece78df4bae6f796b0a123854957addda77cc0559f205bc03cf3984ced816161e30e7f247d88e7
+ languageName: node
+ linkType: hard
+
"doctrine@npm:^2.1.0":
version: 2.1.0
resolution: "doctrine@npm:2.1.0"
@@ -42556,6 +42566,13 @@ __metadata:
languageName: node
linkType: hard
+"yaml@npm:^2.2.2":
+ version: 2.3.4
+ resolution: "yaml@npm:2.3.4"
+ checksum: e6d1dae1c6383bcc8ba11796eef3b8c02d5082911c6723efeeb5ba50fc8e881df18d645e64de68e421b577296000bea9c75d6d9097c2f6699da3ae0406c030d8
+ languageName: node
+ linkType: hard
+
"yamljs@npm:0.3.0":
version: 0.3.0
resolution: "yamljs@npm:0.3.0"