/* globals RoutePolicy, SAML */ /* jshint newcap: false */ if (!Accounts.saml) { Accounts.saml = { settings: { debug: true, generateUsername: false, providers: [] } }; } const fiber = Npm.require('fibers'); const connect = Npm.require('connect'); RoutePolicy.declare('/_saml/', 'network'); /** * Fetch SAML provider configs for given 'provider'. */ function getSamlProviderConfig(provider) { if (! provider) { throw new Meteor.Error('no-saml-provider', 'SAML internal error', { method: 'getSamlProviderConfig' }); } const samlProvider = function(element) { return (element.provider === provider); }; return Accounts.saml.settings.providers.filter(samlProvider)[0]; } Meteor.methods({ samlLogout(provider) { // Make sure the user is logged in before initiate SAML SLO if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'samlLogout' }); } const providerConfig = getSamlProviderConfig(provider); if (Accounts.saml.settings.debug) { console.log(`Logout request from ${ JSON.stringify(providerConfig) }`); } // This query should respect upcoming array of SAML logins const user = Meteor.users.findOne({ _id: Meteor.userId(), 'services.saml.provider': provider }, { 'services.saml': 1 }); let nameID = user.services.saml.nameID; const sessionIndex = user.services.saml.idpSession; nameID = sessionIndex; if (Accounts.saml.settings.debug) { console.log(`NameID for user ${ Meteor.userId() } found: ${ JSON.stringify(nameID) }`); } const _saml = new SAML(providerConfig); const request = _saml.generateLogoutRequest({ nameID, sessionIndex }); // request.request: actual XML SAML Request // request.id: comminucation id which will be mentioned in the ResponseTo field of SAMLResponse Meteor.users.update({ _id: Meteor.userId() }, { $set: { 'services.saml.inResponseTo': request.id } }); const _syncRequestToUrl = Meteor.wrapAsync(_saml.requestToUrl, _saml); const result = _syncRequestToUrl(request.request, 'logout'); if (Accounts.saml.settings.debug) { console.log(`SAML Logout Request ${ result }`); } return result; } }); Accounts.registerLoginHandler(function(loginRequest) { if (!loginRequest.saml || !loginRequest.credentialToken) { return undefined; } const loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken); if (Accounts.saml.settings.debug) { console.log(`RESULT :${ JSON.stringify(loginResult) }`); } if (loginResult === undefined) { return { type: 'saml', error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found') }; } if (loginResult && loginResult.profile && loginResult.profile.email) { let user = Meteor.users.findOne({ 'emails.address': loginResult.profile.email }); if (!user) { const newUser = { name: loginResult.profile.cn || loginResult.profile.username, active: true, globalRoles: ['user'], emails: [{ address: loginResult.profile.email, verified: true }] }; if (Accounts.saml.settings.generateUsername === true) { const username = RocketChat.generateUsernameSuggestion(newUser); if (username) { newUser.username = username; } } else if (loginResult.profile.username) { newUser.username = loginResult.profile.username; } const userId = Accounts.insertUserDoc({}, newUser); user = Meteor.users.findOne(userId); } //creating the token and adding to the user const stampedToken = Accounts._generateStampedLoginToken(); Meteor.users.update(user, { $push: { 'services.resume.loginTokens': stampedToken } }); const samlLogin = { provider: Accounts.saml.RelayState, idp: loginResult.profile.issuer, idpSession: loginResult.profile.sessionIndex, nameID: loginResult.profile.nameID }; Meteor.users.update({ _id: user._id }, { $set: { // TBD this should be pushed, otherwise we're only able to SSO into a single IDP at a time 'services.saml': samlLogin } }); //sending token along with the userId const result = { userId: user._id, token: stampedToken.token }; return result; } else { throw new Error('SAML Profile did not contain an email address'); } }); Accounts.saml._loginResultForCredentialToken = {}; Accounts.saml.hasCredential = function(credentialToken) { return _.has(Accounts.saml._loginResultForCredentialToken, credentialToken); }; Accounts.saml.retrieveCredential = function(credentialToken) { // The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check. const result = Accounts.saml._loginResultForCredentialToken[credentialToken]; delete Accounts.saml._loginResultForCredentialToken[credentialToken]; return result; }; const closePopup = function(res, err) { res.writeHead(200, { 'Content-Type': 'text/html' }); let content = '