mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-05 02:36:52 +00:00
Merge pull request #19443 from overleaf/ab-overleaf-integration-refacto-move-institutions
[web] Move onboarding related code to onboarding module GitOrigin-RevId: 405d4c3588f3911867fecd02b36e55fcd7633615
This commit is contained in:
parent
5f2718cf29
commit
dcf6e502b9
5 changed files with 321 additions and 1 deletions
|
@ -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(() => {
|
||||
|
|
|
@ -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))
|
||||
|
|
20
services/web/test/acceptance/files/saml-cert.crt
Normal file
20
services/web/test/acceptance/files/saml-cert.crt
Normal file
|
@ -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-----
|
28
services/web/test/acceptance/files/saml-key.pem
Normal file
28
services/web/test/acceptance/files/saml-key.pem
Normal file
|
@ -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-----
|
249
services/web/test/acceptance/src/helpers/SAMLHelper.js
Normal file
249
services/web/test/acceptance/src/helpers/SAMLHelper.js
Normal file
|
@ -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 =>
|
||||
`<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">${v}</saml:AttributeValue>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
function makeAttribute(attribute, value) {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `<saml:AttributeStatement>
|
||||
<saml:Attribute Name="${attribute}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
|
||||
${samlValue(value)}
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>`
|
||||
}
|
||||
|
||||
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 = `<saml:AttributeStatement>
|
||||
<saml:Attribute Name="${userIdAttributeName}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
|
||||
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">${uniqueId}</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>`
|
||||
|
||||
const userIdAttributeLegacy =
|
||||
samlData.userIdAttributeLegacy && samlData.uniqueIdLegacy
|
||||
? `<saml:AttributeStatement><saml:Attribute Name="${samlData.userIdAttributeLegacy}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">${samlData.uniqueIdLegacy}</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>`
|
||||
: ''
|
||||
|
||||
const nameId =
|
||||
userIdAttributeName && userIdAttributeName !== 'nameID'
|
||||
? `<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">mock@email.com</saml:NameID>`
|
||||
: ''
|
||||
|
||||
const samlAssertion = `<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="t835VaiI1fph1yk8yhdD4OtyBQ4" IssueInstant="2018-08-09T08:56:30.126Z" Version="2.0">
|
||||
<saml:Issuer>${issuer}</saml:Issuer>
|
||||
<saml:Subject>
|
||||
${nameId}
|
||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
||||
<saml:SubjectConfirmationData Recipient="*******" NotOnOrAfter="2028-08-09T09:01:30.126Z" InResponseTo="${requestId}" />
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
<saml:Conditions NotBefore="2008-08-09T08:51:30.126Z" NotOnOrAfter="2028-08-09T09:01:30.126Z">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>${issuer}</saml:Audience>
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement SessionIndex="t835VaiI1fph1yk8yhdD4OtyBQ4" AuthnInstant="2018-08-09T08:56:30.118Z">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
${makeAttribute('email', email)}
|
||||
${makeAttribute('firstName', firstName)}
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="hasEntitlement" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
|
||||
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">${hasEntitlement}</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="issuer" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
|
||||
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">${issuer}</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
${makeAttribute('lastName', lastName)}
|
||||
${userIdAttribute}
|
||||
${userIdAttributeLegacy}
|
||||
</saml:Assertion>`
|
||||
|
||||
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 = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0" ID="WQMXUw8BBp4_XWzcuKgaN5tmxpT" IssueInstant="2018-08-09T08:56:30.106Z" InResponseTo="${requestId}">
|
||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${issuer}</saml:Issuer>
|
||||
<samlp:Status>
|
||||
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
|
||||
</samlp:Status>
|
||||
${samlAssertion}
|
||||
</samlp:Response>
|
||||
`
|
||||
|
||||
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<Object>} - A promise that resolves to the parsed SAML request object.
|
||||
*/
|
||||
async function parseSamlRequest(redirectUri) {
|
||||
const decoded = redirectUri.searchParams.get('SAMLRequest')
|
||||
const base64Decoded = Buffer.from(decoded, 'base64')
|
||||
const inflated = zlib.inflateRawSync(base64Decoded)
|
||||
return xml2js.parseStringPromise(inflated.toString('utf8'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SAML request from the given redirect URI and returns the request ID.
|
||||
* @param {URL} redirectUri - The redirect URI containing the SAML request.
|
||||
* @returns {Promise<string>} - A Promise that resolves to the request ID.
|
||||
*/
|
||||
async function getRequestId(redirectUri) {
|
||||
const samlRequest = await parseSamlRequest(redirectUri)
|
||||
return samlRequest['samlp:AuthnRequest'].$.ID
|
||||
}
|
||||
|
||||
const SAMLHelper = {
|
||||
createMockSamlResponse,
|
||||
samlUniversity,
|
||||
getParseAndDoChecksForSamlLogs,
|
||||
parseSamlRequest,
|
||||
getRequestId,
|
||||
}
|
||||
|
||||
module.exports = SAMLHelper
|
Loading…
Reference in a new issue