diff --git a/server/controllers/auth.js b/server/controllers/auth.js index a3d62e2b..93d8b25a 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -14,14 +14,16 @@ router.get('/login', (req, res, next) => { }) router.get('/login/:strategy', async (req, res, next) => { try { - const authResult = await WIKI.models.users.login({ + await WIKI.models.users.login({ strategy: req.params.strategy }, { req, res }) } catch (err) { next(err) } }) -router.get('/login/:strategy/callback', async (req, res, next) => { +router.all('/login/:strategy/callback', async (req, res, next) => { + if (req.method !== 'GET' && req.method !== 'POST') { return next() } + try { const authResult = await WIKI.models.users.login({ strategy: req.params.strategy diff --git a/server/modules/authentication/saml/authentication.js b/server/modules/authentication/saml/authentication.js index 87b7aabb..ee37773b 100644 --- a/server/modules/authentication/saml/authentication.js +++ b/server/modules/authentication/saml/authentication.js @@ -10,30 +10,53 @@ const SAMLStrategy = require('passport-saml').Strategy module.exports = { init (passport, conf) { + let samlConfig = { + callbackUrl: conf.callbackURL, + entryPoint: conf.entryPoint, + issuer: conf.issuer, + signatureAlgorithm: conf.signatureAlgorithm, + identifierFormat: conf.identifierFormat, + acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs), + disableRequestedAuthnContext: conf.disableRequestedAuthnContext, + authnContext: conf.authnContext, + forceAuthn: conf.forceAuthn, + providerName: conf.providerName, + skipRequestCompression: conf.skipRequestCompression, + authnRequestBinding: conf.authnRequestBinding + } + if (!_.isEmpty(conf.audience)) { + samlConfig.audience = conf.audience + } + if (!_.isEmpty(conf.cert)) { + samlConfig.cert = _.split(conf.cert, '|') + } + if (!_.isEmpty(conf.privateCert)) { + samlConfig.privateCert = conf.privateCert + } + if (!_.isEmpty(conf.decryptionPvk)) { + samlConfig.decryptionPvk = conf.decryptionPvk + } passport.use('saml', - new SAMLStrategy({ - callbackURL: conf.callbackURL, - entryPoint: conf.entryPoint, - issuer: conf.issuer, - audience: conf.audience, - cert: _.split(conf.cert, '|'), - privateCert: conf.privateCert, - decryptionPvk: conf.decryptionPvk, - signatureAlgorithm: conf.signatureAlgorithm, - identifierFormat: conf.identifierFormat, - acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs), - disableRequestedAuthnContext: conf.disableRequestedAuthnContext, - authnContext: conf.authnContext, - forceAuthn: conf.forceAuthn, - providerName: conf.providerName, - skipRequestCompression: conf.skipRequestCompression, - authnRequestBinding: conf.authnRequestBinding - }, (profile, cb) => { - WIKI.models.users.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) + new SAMLStrategy(samlConfig, async (profile, cb) => { + try { + const userId = _.get(profile, [conf.mappingUID], null) || _.get(profile, 'nameID', null) + if (!userId) { + throw new Error('Invalid or Missing Unique ID field!') + } + + const user = await WIKI.models.users.processProfile({ + profile: { + id: userId, + email: _.get(profile, conf.mappingEmail, ''), + displayName: _.get(profile, conf.mappingDisplayName, '???'), + picture: _.get(profile, conf.mappingPicture, '') + }, + providerKey: 'saml' + }) + cb(null, user) + } catch (err) { + cb(err, null) + } }) ) } diff --git a/server/modules/authentication/saml/definition.yml b/server/modules/authentication/saml/definition.yml index 189723e6..c0ef9054 100644 --- a/server/modules/authentication/saml/definition.yml +++ b/server/modules/authentication/saml/definition.yml @@ -5,36 +5,44 @@ author: requarks.io logo: https://static.requarks.io/logo/saml.svg color: red darken-3 website: https://wiki.oasis-open.org/security/FrontPage +isAvailable: true useForm: false props: entryPoint: type: String title: Entry Point hint: Identity provider entrypoint (URL) + order: 1 issuer: type: String title: Issuer hint: Issuer string to supply to Identity Provider + order: 2 audience: type: String title: Audience - hint: Expected SAML response Audience (if not provided, Audience won't be verified) + hint: (Optional) - Expected SAML response Audience (if not provided, Audience won't be verified) + order: 3 cert: type: String title: Certificate - hint: Public PEM-encoded X.509 signing certificate contents in base64 (e.g. 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W=='). If the provider has multiple certificates that are valid, join them together using the | pipe symbol. + hint: (Optional) - Public PEM-encoded X.509 signing certificate. If the provider has multiple certificates that are valid, join them together using the | pipe symbol. + order: 4 privateCert: type: String title: Private Certificate - hint: PEM formatted key used to sign the certificate. + hint: (Optional) - PEM formatted key used to sign the certificate. + order: 5 decryptionPvk: type: String title: Decryption Private Key - hint: (optional) Private key that will be used to attempt to decrypt any encrypted assertions that are received. + hint: (Optional) - Private key that will be used to attempt to decrypt any encrypted assertions that are received. + order: 6 signatureAlgorithm: type: String title: Signature Algorithm hint: Signature algorithm used for signing requests + order: 7 default: sha1 enum: - sha1 @@ -44,41 +52,73 @@ props: type: String title: Name Identifier format default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + order: 8 acceptedClockSkewMs: type: Number title: Accepted Clock Skew Milleseconds hint: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely. - default: 0 + default: -1 + order: 9 disableRequestedAuthnContext: type: Boolean title: Disable Requested Auth Context hint: If enabled, do not request a specific authentication context. This is known to help when authenticating against Active Directory (AD FS) servers. default: false + order: 10 authnContext: type: String title: Auth Context hint: Name identifier format to request auth context. default: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + order: 11 forceAuthn: type: Boolean title: Force Initial Re-authentication hint: If enabled, the initial SAML request from the service provider specifies that the IdP should force re-authentication of the user, even if they possess a valid session. default: false + order: 12 providerName: type: String title: Provider Name hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider. default: wiki.js + order: 13 skipRequestCompression: type: Boolean title: Skip Request Compression hint: If enabled, the SAML request from the service provider won't be compressed. default: false + order: 14 authnRequestBinding: type: String title: Request Binding hint: Binding used for request authentication from IDP. - default: 'HTTP-Redirect' + order: 15 + default: 'HTTP-POST' enum: - HTTP-Redirect - HTTP-POST + mappingUID: + title: Unique ID Field Mapping + type: String + default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' + hint: The field storing the user unique identifier. Can be a variable name or a URI-formatted string. + order: 16 + mappingEmail: + title: Email Field Mapping + type: String + default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' + hint: The field storing the user email. Can be a variable name or a URI-formatted string. + order: 17 + mappingDisplayName: + title: Display Name Field Mapping + type: String + default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' + hint: The field storing the user display name. Can be a variable name or a URI-formatted string. + order: 18 + mappingPicture: + title: Avatar Picture Field Mapping + type: String + default: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/picture' + hint: The field storing the user avatar picture. Can be a variable name or a URI-formatted string. + order: 19 diff --git a/server/modules/storage/git/storage.js b/server/modules/storage/git/storage.js index db1380ae..a8090672 100644 --- a/server/modules/storage/git/storage.js +++ b/server/modules/storage/git/storage.js @@ -116,7 +116,7 @@ module.exports = { } else { originUrl = `https://${this.config.basicUsername}:${this.config.basicPassword}@${this.config.repoUrl}` } - await this.git.addRemote('origin', ) + await this.git.addRemote('origin', originUrl) break }