diff --git a/services/web/scripts/process_lapsed_reconfirmations.js b/services/web/scripts/process_lapsed_reconfirmations.js
index 7bd7c8c4d1..a38af158ad 100644
--- a/services/web/scripts/process_lapsed_reconfirmations.js
+++ b/services/web/scripts/process_lapsed_reconfirmations.js
@@ -1,4 +1,4 @@
-const InstitutionsReconfirmationHandler = require('../modules/overleaf-integration/app/src/Institutions/InstitutionsReconfirmationHandler')
+const InstitutionsReconfirmationHandler = require('../modules/institutions/app/src/InstitutionsReconfirmationHandler')
InstitutionsReconfirmationHandler.processLapsed()
.then(() => {
diff --git a/services/web/test/acceptance/config/settings.test.saas.js b/services/web/test/acceptance/config/settings.test.saas.js
index e47a039e86..0ffa1feb96 100644
--- a/services/web/test/acceptance/config/settings.test.saas.js
+++ b/services/web/test/acceptance/config/settings.test.saas.js
@@ -7,12 +7,20 @@ const httpAuthPass = 'password'
const httpAuthUsers = {}
httpAuthUsers[httpAuthUser] = httpAuthPass
+const overleafHost =
+ process.env.V2_URL ||
+ `http://${process.env.HTTP_TEST_HOST || '127.0.0.1'}:23000`
+
const overrides = {
+ appName: 'Overleaf',
+ siteUrl: overleafHost,
+
enableSubscriptions: true,
apis: {
thirdPartyDataStore: {
url: `http://127.0.0.1:23002`,
+ dropboxApp: 'Overleaf',
},
analytics: {
url: `http://127.0.0.1:23050`,
@@ -35,6 +43,9 @@ const overrides = {
user: 'overleaf',
pass: 'password',
},
+ tags: {
+ url: 'http://127.0.0.1:25000',
+ },
},
oauthProviders: {
@@ -66,6 +77,18 @@ const overrides = {
},
},
},
+
+ overleaf: {
+ host: 'http://127.0.0.1:25000',
+ oauth: {
+ clientID: 'mock-oauth-client-id',
+ clientSecret: 'mock-oauth-client-secret',
+ },
+ },
+
+ analytics: {
+ enabled: true,
+ },
}
module.exports = baseApp.mergeWith(baseTest.mergeWith(overrides))
diff --git a/services/web/test/acceptance/files/saml-cert.crt b/services/web/test/acceptance/files/saml-cert.crt
new file mode 100644
index 0000000000..f4be3fea10
--- /dev/null
+++ b/services/web/test/acceptance/files/saml-cert.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDWzCCAkOgAwIBAgIUMYF933mpvZjZwxuweemukpsawsEwDQYJKoZIhvcNAQEF
+BQAwPTELMAkGA1UEBhMCVUsxCzAJBgNVBAgMAlVLMSEwHwYDVQQKDBhJbnRlcm5l
+dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjAwMzA1MTU0NjQ0WhcNMzAwMzAzMTU0NjQ0
+WjA9MQswCQYDVQQGEwJVSzELMAkGA1UECAwCVUsxITAfBgNVBAoMGEludGVybmV0
+IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANgiv2BzCV/xAN3U0miBRnUEg/vwTxqL4Ibzuf4H1X9Kael8jsM2GVEo0D4ot+RK
+nwDjx22hJaNF7uA8WG+tuVUA7m6JGYw1jB/3Pa6MxUDYl2dgQQetnIKHWX+Gq2GE
+aY734P+kopc0FEUVRp/ZWfEEI74r0rpT2mdW/pLFAJKc+zK+vIBgU40WEdeT0/Vo
+0x2J0sqAT56td4XHYlg29Y7eARTq2+Z00eM8lJC4KzD9LM6Ut3Ea4mg1juaIAXKy
+kcmJ+PbO0tzZPf7V+ZY66lrU4vye6oig23D5A0uC9LkwtDPEW5vCmvFnEwBHo/cZ
+TXldG5Pw9+Ja8o4W+vs7O9sCAwEAAaNTMFEwHQYDVR0OBBYEFPS6b8k/u0hA4woS
+kHF8Wc6AW1qkMB8GA1UdIwQYMBaAFPS6b8k/u0hA4woSkHF8Wc6AW1qkMA8GA1Ud
+EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADWigiC7md2smgPH2Wo4W0Aw
+ON+gMfF3hsn1K6M3ce4ou61gcwGOKWsvxdCAvArhGVY0ijw0Tq47K/zqrmWoWute
+LtKPweVgAgvcOCro3NPVdXb46k/u8305es7LWNksJ3PaMw35GU6bhBrsUfGPeM6n
+J9QDUJiFCxwQTATfOwlNZucfTmdBLspajhNsjuKb3TqqKzy5a5nHkEWjEJrpcSg+
+P5HoTVQIzedaY2J2D8peE0V9zFDPhq3SsVxAXdyoGSNAXa9unGZyGbfH1/GeHwOn
+UTMZerv5c5Nv1MtOgwEWi7NkqWhAIf6rZDpXWLwZ1V258yhpwQ371MqklzJbyaY=
+-----END CERTIFICATE-----
diff --git a/services/web/test/acceptance/files/saml-key.pem b/services/web/test/acceptance/files/saml-key.pem
new file mode 100644
index 0000000000..b7782aabc4
--- /dev/null
+++ b/services/web/test/acceptance/files/saml-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDYIr9gcwlf8QDd
+1NJogUZ1BIP78E8ai+CG87n+B9V/SmnpfI7DNhlRKNA+KLfkSp8A48dtoSWjRe7g
+PFhvrblVAO5uiRmMNYwf9z2ujMVA2JdnYEEHrZyCh1l/hqthhGmO9+D/pKKXNBRF
+FUaf2VnxBCO+K9K6U9pnVv6SxQCSnPsyvryAYFONFhHXk9P1aNMdidLKgE+erXeF
+x2JYNvWO3gEU6tvmdNHjPJSQuCsw/SzOlLdxGuJoNY7miAFyspHJifj2ztLc2T3+
+1fmWOupa1OL8nuqIoNtw+QNLgvS5MLQzxFubwprxZxMAR6P3GU15XRuT8PfiWvKO
+Fvr7OzvbAgMBAAECggEBAMaFhArvHtlE4GrhJDJhK3ooH6K1Y7Mab60FCP1P7MXy
+b73KbsbXVgG53yx48g96ivmiPndv4MZLYdING53Yj7aIGHjm7NRgCskBq2I8YqHh
+T4/gVVrcGDm8YHRGGfyERwDOpZeqfL0tVMDvfeMtHPPHvZzbW79RbfYlbccZtCD0
+54PPnXqcKBx/gsWi6RrRFWYChBpmMjPrxz/SOSIYVvxBk3rY7aikckP/XT6dR71K
+1Ifqqa6ihi8bx9NLEJPCWmMCNMmwDG5iJXr9gtMJAUnr+K8yD4vkDDwgDilU8mTC
+UtfcbYyl1lOnPGUjLx9xKuk9bsk4k/uDsBfUWK/lEgECgYEA78dlfsfQArOuAO8M
+GdU1R+OtLAofl1wyhcpDDcQZq9MMFvBCBSKkbw+Rg1IY/4WApKvPY9RbiVSFU8Cp
+d31JgW9wFADOhXtVlpRDZPJXXdS2zJaJatD3PrqIgKnNxKyqzz9gjuGMCiwBMujm
+FqrmkOREAN+1jqcSgsVuDQld2hsCgYEA5sHjQ37JftyJxKSHmKhZTr0I7BoRStox
+nOq3aSBqaWfihMFY1WW3eCyCoG+EltiJtScLRPor6MebPm9cqOPKnQcnC3l59OVW
+3UTC3g7JjflyiViminXMHDFtsZfpxSkzYA3+oPQJVl2bSj0ud21eXC/y2KN+8KO3
+Kd7KGVJyQUECgYEAsNiPswIMGPIM1AN7GVJ3CZ6Sinis9CW73ZFgAzcu99ugfwqU
+ptT2EjOZTxGt/keoqctOGoL1QERmUW83jjmJjT1znE08BJcCeRzA2CMk7L+GUz5z
++6RDtrA9HSgf636uPEyyGq+faaErATFlAjLp+tNglIRqk9wFew3CLTtLTSECgYEA
+mzpWXPMPLK3CZ2ueY4zr9tGnDNxEQawhr8Mc+jT6IEnn0RIXZgX0s3yNqssZ0Dd9
++0R2ikIYA5Ey138mP95sT9Gd7FQdPCaClnpI9APShhUFfWsLLR0s3tJJTiw4745V
+pwoC/dbr6RMzAW/CsEf8L9t5a04geFRJRHtATGRvw4ECgYEAwZ2PUuNZsmtGj0/o
+VneVCKNrUBMNDR5fOcMHKsNmowgDUxW0hEwa2JI1Zj5lLnqPbsgAQqP+j8AyfPAB
+5wCQb+fV5NmZW15GB/7dMkISaLvwBoA9qKK2MO2szWDRpMG6rkF6dzWhLOKgqaL0
+vsQx+F6ymMvXw0pnQf9/Qqxkp7k=
+-----END PRIVATE KEY-----
diff --git a/services/web/test/acceptance/src/helpers/SAMLHelper.js b/services/web/test/acceptance/src/helpers/SAMLHelper.js
new file mode 100644
index 0000000000..d18342f206
--- /dev/null
+++ b/services/web/test/acceptance/src/helpers/SAMLHelper.js
@@ -0,0 +1,249 @@
+const fs = require('fs')
+const path = require('path')
+const SignedXml = require('xml-crypto').SignedXml
+const { SamlLog } = require('../../../../app/src/models/SamlLog')
+const { expect } = require('chai')
+const zlib = require('zlib')
+const xml2js = require('xml2js')
+
+const samlDataDefaults = {
+ firstName: 'first-name',
+ hasEntitlement: 'Y',
+ issuer: 'Overleaf',
+ lastName: 'last-name',
+ requestId: 'dummy-request-id',
+}
+
+function samlValue(val) {
+ if (!Array.isArray(val)) {
+ val = [val]
+ }
+ return val
+ .map(
+ v =>
+ `${v}`
+ )
+ .join('')
+}
+
+function makeAttribute(attribute, value) {
+ if (!value) {
+ return ''
+ }
+
+ return `
+
+ ${samlValue(value)}
+
+`
+}
+
+function createMockSamlAssertion(samlData = {}, opts = {}) {
+ const {
+ email,
+ firstName,
+ hasEntitlement,
+ issuer,
+ lastName,
+ uniqueId,
+ requestId,
+ } = {
+ ...samlDataDefaults,
+ ...samlData,
+ }
+ const { signedAssertion = true } = opts
+
+ const userIdAttributeName = samlData.userIdAttribute || 'uniqueId'
+ const userIdAttribute = `
+
+ ${uniqueId}
+
+ `
+
+ const userIdAttributeLegacy =
+ samlData.userIdAttributeLegacy && samlData.uniqueIdLegacy
+ ? `${samlData.uniqueIdLegacy}`
+ : ''
+
+ const nameId =
+ userIdAttributeName && userIdAttributeName !== 'nameID'
+ ? `mock@email.com`
+ : ''
+
+ const samlAssertion = `
+ ${issuer}
+
+ ${nameId}
+
+
+
+
+
+
+ ${issuer}
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
+
+
+ ${makeAttribute('email', email)}
+ ${makeAttribute('firstName', firstName)}
+
+
+ ${hasEntitlement}
+
+
+
+
+ ${issuer}
+
+
+ ${makeAttribute('lastName', lastName)}
+ ${userIdAttribute}
+ ${userIdAttributeLegacy}
+ `
+
+ if (!signedAssertion) {
+ return samlAssertion
+ }
+
+ const sig = new SignedXml()
+ sig.addReference(
+ "//*[local-name(.)='Assertion']",
+ [
+ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
+ 'http://www.w3.org/2001/10/xml-exc-c14n#',
+ ],
+ 'http://www.w3.org/2000/09/xmldsig#sha1'
+ )
+
+ sig.signingKey = fs.readFileSync(
+ path.resolve(__dirname, '../../files/saml-key.pem'),
+ 'utf8'
+ )
+ sig.computeSignature(samlAssertion)
+ return sig.getSignedXml()
+}
+
+function createMockSamlResponse(samlData = {}, opts = {}) {
+ const { issuer, requestId } = {
+ ...samlDataDefaults,
+ ...samlData,
+ }
+ const { signedResponse = true } = opts
+
+ const samlAssertion = createMockSamlAssertion(samlData, opts)
+
+ let samlResponse = `
+
+
+ ${issuer}
+
+
+
+ ${samlAssertion}
+
+ `
+
+ if (signedResponse) {
+ const sig = new SignedXml()
+ sig.addReference(
+ "//*[local-name(.)='Response']",
+ [
+ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
+ 'http://www.w3.org/2001/10/xml-exc-c14n#',
+ ],
+ 'http://www.w3.org/2000/09/xmldsig#sha1'
+ )
+
+ sig.signingKey = fs.readFileSync(
+ path.resolve(__dirname, '../../files/saml-key.pem'),
+ 'utf8'
+ )
+ sig.computeSignature(samlResponse)
+ samlResponse = sig.getSignedXml()
+ }
+
+ return Buffer.from(samlResponse).toString('base64')
+}
+
+function samlUniversity(config = {}) {
+ return {
+ hostname: 'example-sso.com',
+ sso_cert: fs
+ .readFileSync(
+ path.resolve(__dirname, '../../files/saml-cert.crt'),
+ 'utf8'
+ )
+ .replace(/-----BEGIN CERTIFICATE-----/, '')
+ .replace(/-----END CERTIFICATE-----/, '')
+ .replace(/\n/g, ''),
+ sso_enabled: true,
+ sso_entry_point: 'http://example-sso.com/saml',
+ sso_entity_id: 'http://example-sso.com/saml/idp',
+ university_id: 9999,
+ university_name: 'Example University',
+ sso_user_email_attribute: 'email',
+ sso_user_first_name_attribute: 'firstName',
+ sso_user_id_attribute: 'uniqueId',
+ sso_user_last_name_attribute: 'lastName',
+ sso_license_entitlement_attribute: 'hasEntitlement',
+ sso_license_entitlement_matcher: 'Y',
+ sso_signature_algorithm: 'sha256',
+ ...config,
+ }
+}
+
+async function getParseAndDoChecksForSamlLogs(numberOfLog) {
+ const logs = await SamlLog.find({}, {})
+ .sort({ $natural: -1 })
+ .limit(numberOfLog || 1)
+ .exec()
+ logs.forEach(log => {
+ expect(log.sessionId).to.exist
+ expect(log.sessionId.length).to.equal(8) // not full session ID
+ expect(log.createdAt).to.exist
+ expect(log.jsonData).to.exist
+ log.parsedJsonData = JSON.parse(log.jsonData)
+ if (log.samlAssertion) {
+ log.parsedSamlAssertion = JSON.parse(log.samlAssertion)
+ }
+ })
+
+ return logs
+}
+
+/**
+ * Parses a SAML request from a redirect URI.
+ *
+ * @param {URL} redirectUri - The redirect URI containing the SAML request.
+ * @returns {Promise