2015-08-03 16:11:23 +02:00
if ( ! Accounts . saml ) {
2015-08-04 21:02:09 +02:00
Accounts . saml = { } ;
2015-08-03 16:11:23 +02:00
}
var Fiber = Npm . require ( 'fibers' ) ;
2017-04-12 11:37:21 +02:00
//var connect = Npm.require('connect');
var bodyParser = Npm . require ( 'body-parser' )
2015-08-03 16:11:23 +02:00
RoutePolicy . declare ( '/_saml/' , 'network' ) ;
2015-08-05 18:08:36 +02:00
Meteor . methods ( {
2017-05-17 17:56:38 +02:00
samlLogout : function ( provider ) {
2015-08-06 22:42:26 +02:00
// Make sure the user is logged in before initiate SAML SLO
if ( ! Meteor . userId ( ) ) {
throw new Meteor . Error ( "not-authorized" ) ;
2015-08-07 17:57:51 +02:00
}
2017-05-17 17:56:38 +02:00
var samlProvider = function ( element ) {
2015-08-06 22:42:26 +02:00
return ( element . provider == provider )
}
providerConfig = Meteor . settings . saml . filter ( samlProvider ) [ 0 ] ;
2015-08-07 17:57:51 +02:00
if ( Meteor . settings . debug ) {
2015-08-06 22:42:26 +02:00
console . log ( "Logout request from " + JSON . stringify ( providerConfig ) ) ;
}
// This query should respect upcoming array of SAML logins
2015-08-07 17:57:51 +02:00
var user = Meteor . users . findOne ( {
_id : Meteor . userId ( ) ,
"services.saml.provider" : provider
} , {
"services.saml" : 1
} ) ;
var nameID = user . services . saml . nameID ;
var sessionIndex = nameID = user . services . saml . idpSession ;
if ( Meteor . settings . debug ) {
2015-08-06 22:42:26 +02:00
console . log ( "NameID for user " + Meteor . userId ( ) + " found: " + JSON . stringify ( nameID ) ) ;
}
2015-08-07 17:57:51 +02:00
2015-08-06 22:42:26 +02:00
_saml = new SAML ( providerConfig ) ;
2015-08-07 17:57:51 +02:00
var request = _saml . generateLogoutRequest ( {
nameID : nameID ,
sessionIndex : 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
}
} ) ;
var _syncRequestToUrl = Meteor . wrapAsync ( _saml . requestToUrl , _saml ) ;
var result = _syncRequestToUrl ( request . request , "logout" ) ;
if ( Meteor . settings . debug ) {
console . log ( "SAML Logout Request " + result ) ;
2015-08-06 22:42:26 +02:00
}
2015-08-07 17:57:51 +02:00
return result ;
2015-08-05 18:08:36 +02:00
}
} )
2017-05-17 17:56:38 +02:00
Accounts . registerLoginHandler ( function ( loginRequest ) {
2015-08-04 21:02:09 +02:00
if ( ! loginRequest . saml || ! loginRequest . credentialToken ) {
return undefined ;
}
var loginResult = Accounts . saml . retrieveCredential ( loginRequest . credentialToken ) ;
2015-08-10 10:38:00 +02:00
if ( Meteor . settings . debug ) {
2017-05-17 17:56:38 +02:00
console . log ( "RESULT :" + JSON . stringify ( loginResult ) ) ;
2015-08-10 10:38:00 +02:00
}
2017-05-19 16:26:22 +02:00
if ( loginResult && loginResult . profile && loginResult . profile . nameID ) {
console . log ( "Profile: " + JSON . stringify ( loginResult . profile . nameID ) ) ;
var localProfileMatchAttribute ;
var localFindStructure ;
var nameIDFormat ;
// Default nameIDFormat is emailAddress
if ( ! ( Meteor . settings . saml [ 0 ] . identifierFormat ) || ( Meteor . settings . saml [ 0 ] . identifierFormat == "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" ) ) {
nameIDFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" ;
} else {
nameIDFormat = Meteor . settings . saml [ 0 ] . identifierFormat ;
}
if ( nameIDFormat == "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" ) {
// If nameID Format is emailAdress, we should not force 'email' as localProfileMatchAttribute
localProfileMatchAttribute = "email" ;
localFindStructure = "emails.address" ;
profileOrEmail = "email" ;
profileOrEmailValue = loginResult . profile . nameID ;
} else // any other nameID format
// Check if Meteor.settings.saml[0].localProfileMatchAttribute has value
// These values will be stored in profile substructure. They're NOT security relevant because profile isn't a safe place
if ( Meteor . settings . saml [ 0 ] . localProfileMatchAttribute ) {
profileOrEmail = "profile" ;
profileOrEmailValue = {
[ Meteor . settings . saml [ 0 ] . localProfileMatchAttribute ] : loginResult . profile . nameID
} ;
localFindStructure = 'profile.' + Meteor . settings . saml [ 0 ] . localProfileMatchAttribute ;
}
2018-02-14 21:19:23 +01:00
if ( Meteor . settings . debug ) {
console . log ( "Looking for user with " + localFindStructure + "=" + loginResult . profile . nameID ) ;
}
2015-08-04 21:02:09 +02:00
var user = Meteor . users . findOne ( {
2017-05-19 16:26:22 +02:00
//profile[Meteor.settings.saml[0].localProfileMatchAttribute]: loginResult.profile.nameID
[ localFindStructure ] : loginResult . profile . nameID
2015-08-04 21:02:09 +02:00
} ) ;
2017-05-12 22:38:33 +02:00
if ( ! user ) {
2017-05-17 17:56:38 +02:00
if ( Meteor . settings . saml [ 0 ] . dynamicProfile ) {
2017-05-19 16:26:22 +02:00
if ( Meteor . settings . debug ) {
2018-02-12 23:02:21 +01:00
console . log ( "User not found. Will dynamically create one with '" + Meteor . settings . saml [ 0 ] . localProfileMatchAttribute + "' = " + loginResult . profile [ Meteor . settings . saml [ 0 ] . localProfileMatchAttribute ] ) ;
2018-02-14 21:19:23 +01:00
console . log ( "Identity handle: " + profileOrEmail + " = " + JSON . stringify ( profileOrEmailValue ) + " || username = " + loginResult . profile . nameID ) ;
2017-05-19 16:26:22 +02:00
}
2017-05-17 17:56:38 +02:00
Accounts . createUser ( {
2017-05-19 16:26:22 +02:00
//email: loginResult.profile.email,
2017-05-17 17:56:38 +02:00
password : "" ,
username : loginResult . profile . nameID ,
2017-05-19 16:26:22 +02:00
[ profileOrEmail ] : profileOrEmailValue
//[Meteor.settings.saml[0].localProfileMatchAttribute]: loginResult.profile[Meteor.settings.saml[0].localProfileMatchAttribute]
2017-05-17 17:56:38 +02:00
} ) ;
2018-02-14 21:19:23 +01:00
if ( Meteor . settings . debug ) {
console . log ( "Trying to find user" ) ;
}
2017-05-17 17:56:38 +02:00
user = Meteor . users . findOne ( {
2017-05-19 16:26:22 +02:00
"username" : loginResult . profile . nameID
2017-05-17 17:56:38 +02:00
} ) ;
2017-07-20 17:44:14 +02:00
// update user profile w attrs from SAML Attr Satement
//Meteor.user.update(user, )
if ( Meteor . settings . debug ) {
console . log ( "Profile for attributes: " + JSON . stringify ( loginResult . profile ) ) ;
}
var attributeNames = Meteor . settings . saml [ 0 ] . attributesSAML ;
var meteorProfile = { } ;
if ( attributeNames ) {
attributeNames . forEach ( function ( attribute ) {
meteorProfile [ attribute ] = loginResult . profile [ attribute ] ;
} ) ;
}
if ( Meteor . settings . debug ) {
console . log ( "Profile for Meteor: " + JSON . stringify ( meteorProfile ) ) ;
}
Meteor . users . update ( user , {
$set : {
'profile' : meteorProfile
}
} ) ;
2017-05-17 17:56:38 +02:00
if ( Meteor . settings . debug ) {
console . log ( "Created new user" ) ;
}
} else {
2017-05-19 16:26:22 +02:00
throw new Error ( "Could not find an existing user with supplied attribute '" + Meteor . settings . saml [ 0 ] . localProfileMatchAttribute + "' and value:" + loginResult . profile [ Meteor . settings . saml [ 0 ] . localProfileMatchAttribute ] ) ;
2017-05-12 22:38:33 +02:00
}
}
2015-08-04 21:02:09 +02:00
//creating the token and adding to the user
var stampedToken = Accounts . _generateStampedLoginToken ( ) ;
Meteor . users . update ( user , {
$push : {
'services.resume.loginTokens' : stampedToken
}
} ) ;
var samlLogin = {
2015-08-06 22:42:26 +02:00
provider : Accounts . saml . RelayState ,
2015-08-04 21:02:09 +02:00
idp : loginResult . profile . issuer ,
2015-08-07 17:57:51 +02:00
idpSession : loginResult . profile . sessionIndex ,
2015-08-04 21:02:09 +02:00
nameID : loginResult . profile . nameID
} ;
Meteor . users . update ( {
_id : user . _id
} , {
$set : {
2015-08-06 22:42:26 +02:00
// TBD this should be pushed, otherwise we're only able to SSO into a single IDP at a time
2015-08-04 21:02:09 +02:00
'services.saml' : samlLogin
}
} ) ;
2017-05-17 17:56:38 +02:00
if ( loginResult . profile . uid ) {
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
'uid' : loginResult . profile . uid
}
} ) ;
}
2017-07-20 20:25:00 +02:00
var attributeNames = Meteor . settings . saml [ 0 ] . attributesSAML ;
var meteorProfile = { } ;
if ( attributeNames ) {
attributeNames . forEach ( function ( attribute ) {
meteorProfile [ attribute ] = loginResult . profile [ attribute ] ;
} ) ;
}
if ( Meteor . settings . debug ) {
console . log ( "Profile Update for Meteor: " + JSON . stringify ( meteorProfile ) ) ;
}
Meteor . users . update ( {
_id : user . _id
} , {
$set : {
'profile' : meteorProfile
}
} ) ;
2015-08-04 21:02:09 +02:00
//sending token along with the userId
var result = {
userId : user . _id ,
token : stampedToken . token
} ;
return result
} else {
2017-05-19 16:26:22 +02:00
throw new Error ( "SAML Assertion did not contain a proper SAML subject value" ) ;
2015-08-04 21:02:09 +02:00
}
2015-08-03 16:11:23 +02:00
} ) ;
Accounts . saml . _loginResultForCredentialToken = { } ;
2017-05-17 17:56:38 +02:00
Accounts . saml . hasCredential = function ( credentialToken ) {
2015-08-04 21:02:09 +02:00
return _ . has ( Accounts . saml . _loginResultForCredentialToken , credentialToken ) ;
2015-08-03 16:11:23 +02:00
}
2017-05-17 17:56:38 +02:00
Accounts . saml . retrieveCredential = function ( credentialToken ) {
2015-08-04 21:02:09 +02:00
// The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check.
var result = Accounts . saml . _loginResultForCredentialToken [ credentialToken ] ;
delete Accounts . saml . _loginResultForCredentialToken [ credentialToken ] ;
return result ;
2015-08-03 16:11:23 +02:00
}
2015-08-04 21:02:09 +02:00
2015-08-03 16:11:23 +02:00
// Listen to incoming SAML http requests
2017-05-17 17:56:38 +02:00
WebApp . connectHandlers . use ( bodyParser . urlencoded ( {
extended : true
} ) ) . use ( function ( req , res , next ) {
2015-08-04 21:02:09 +02:00
// Need to create a Fiber since we're using synchronous http calls and nothing
// else is wrapping this in a fiber automatically
2017-05-17 17:56:38 +02:00
Fiber ( function ( ) {
2015-08-04 21:02:09 +02:00
middleware ( req , res , next ) ;
} ) . run ( ) ;
2015-08-03 16:11:23 +02:00
} ) ;
2017-05-17 17:56:38 +02:00
middleware = function ( req , res , next ) {
2015-08-04 21:02:09 +02:00
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
try {
var samlObject = samlUrlToObject ( req . url ) ;
if ( ! samlObject || ! samlObject . serviceName ) {
next ( ) ;
return ;
}
if ( ! samlObject . actionName )
throw new Error ( "Missing SAML action" ) ;
2017-05-17 17:56:38 +02:00
var service = _ . find ( Meteor . settings . saml , function ( samlSetting ) {
2015-08-04 21:02:09 +02:00
return samlSetting . provider === samlObject . serviceName ;
} ) ;
// Skip everything if there's no service set by the saml middleware
if ( ! service )
throw new Error ( "Unexpected SAML service " + samlObject . serviceName ) ;
switch ( samlObject . actionName ) {
2017-05-17 17:56:38 +02:00
case "metadata" :
_saml = new SAML ( service ) ;
service . callbackUrl = Meteor . absoluteUrl ( "_saml/validate/" + service . provider ) ;
res . writeHead ( 200 ) ;
res . write ( _saml . generateServiceProviderMetadata ( service . callbackUrl ) ) ;
res . end ( ) ;
//closePopup(res);
break ;
case "logout" :
// This is where we receive SAML LogoutResponse
2018-02-14 21:19:23 +01:00
if ( Meteor . settings . debug ) {
console . log ( "Handling call to 'logout' endpoint." + req . query . SAMLResponse ) ;
}
2017-05-17 17:56:38 +02:00
_saml = new SAML ( service ) ;
_saml . validateLogoutResponse ( req . query . SAMLResponse , function ( err , result ) {
if ( ! err ) {
var logOutUser = function ( inResponseTo ) {
2015-08-10 10:38:00 +02:00
if ( Meteor . settings . debug ) {
2017-05-17 17:56:38 +02:00
console . log ( "Logging Out user via inResponseTo " + inResponseTo ) ;
2015-08-10 10:38:00 +02:00
}
2017-05-17 17:56:38 +02:00
var loggedOutUser = Meteor . users . find ( {
'services.saml.inResponseTo' : inResponseTo
} ) . fetch ( ) ;
if ( loggedOutUser . length == 1 ) {
if ( Meteor . settings . debug ) {
console . log ( "Found user " + loggedOutUser [ 0 ] . _id ) ;
2015-08-07 17:57:51 +02:00
}
2017-05-17 17:56:38 +02:00
Meteor . users . update ( {
_id : loggedOutUser [ 0 ] . _id
} , {
$set : {
"services.resume.loginTokens" : [ ]
}
} ) ;
Meteor . users . update ( {
_id : loggedOutUser [ 0 ] . _id
} , {
$unset : {
"services.saml" : ""
}
} ) ;
} else {
throw new Meteor . error ( "Found multiple users matching SAML inResponseTo fields" ) ;
}
2015-08-07 17:57:51 +02:00
}
2017-05-17 17:56:38 +02:00
Fiber ( function ( ) {
logOutUser ( result ) ;
} ) . run ( ) ;
2015-08-07 17:57:51 +02:00
2017-05-17 17:56:38 +02:00
res . writeHead ( 302 , {
'Location' : req . query . RelayState
} ) ;
res . end ( ) ;
} else {
2018-02-14 21:19:23 +01:00
if ( Meteor . settings . debug ) {
console . log ( "Couldn't validate SAML Logout Response.." ) ;
}
2017-05-17 17:56:38 +02:00
}
} )
break ;
case "sloRedirect" :
var idpLogout = req . query . redirect
res . writeHead ( 302 , {
// credentialToken here is the SAML LogOut Request that we'll send back to IDP
'Location' : idpLogout
} ) ;
res . end ( ) ;
break ;
case "authorize" :
service . callbackUrl = Meteor . absoluteUrl ( "_saml/validate/" + service . provider ) ;
service . id = samlObject . credentialToken ;
_saml = new SAML ( service ) ;
_saml . getAuthorizeUrl ( req , function ( err , url ) {
if ( err )
throw new Error ( "Unable to generate authorize url" ) ;
2015-08-07 17:57:51 +02:00
res . writeHead ( 302 , {
2017-05-17 17:56:38 +02:00
'Location' : url
2015-08-07 17:57:51 +02:00
} ) ;
res . end ( ) ;
2015-08-04 21:02:09 +02:00
} ) ;
2017-05-17 17:56:38 +02:00
break ;
case "validate" :
_saml = new SAML ( service ) ;
2017-05-19 16:26:22 +02:00
if ( Meteor . settings . debug ) {
console . log ( "Service: " + JSON . stringify ( service ) ) ;
} ;
2017-05-17 17:56:38 +02:00
Accounts . saml . RelayState = req . body . RelayState ;
_saml . validateResponse ( req . body . SAMLResponse , req . body . RelayState , function ( err , profile , loggedOut ) {
if ( err )
throw new Error ( "Unable to validate response url: " + err ) ;
var credentialToken = profile . inResponseToId || profile . InResponseTo || samlObject . credentialToken ;
if ( ! credentialToken )
throw new Error ( "Unable to determine credentialToken" ) ;
Accounts . saml . _loginResultForCredentialToken [ credentialToken ] = {
profile : profile
} ;
closePopup ( res ) ;
} ) ;
break ;
default :
throw new Error ( "Unexpected SAML action " + samlObject . actionName ) ;
2015-08-06 22:42:26 +02:00
2015-08-04 21:02:09 +02:00
}
} catch ( err ) {
closePopup ( res , err ) ;
2015-08-03 16:11:23 +02:00
}
2015-08-04 21:02:09 +02:00
} ;
2015-08-03 16:11:23 +02:00
2017-05-17 17:56:38 +02:00
var samlUrlToObject = function ( url ) {
2015-08-04 21:02:09 +02:00
// req.url will be "/_saml/<action>/<service name>/<credentialToken>"
if ( ! url )
return null ;
2015-08-03 16:11:23 +02:00
2015-08-04 21:02:09 +02:00
var splitPath = url . split ( '/' ) ;
2015-08-03 16:11:23 +02:00
2015-08-04 21:02:09 +02:00
// Any non-saml request will continue down the default
// middlewares.
if ( splitPath [ 1 ] !== '_saml' )
return null ;
2015-08-03 16:11:23 +02:00
2015-08-04 21:02:09 +02:00
var result = {
actionName : splitPath [ 2 ] ,
serviceName : splitPath [ 3 ] ,
credentialToken : splitPath [ 4 ]
} ;
2015-08-10 10:38:00 +02:00
if ( Meteor . settings . debug ) {
2017-05-17 17:56:38 +02:00
console . log ( result ) ;
2015-08-10 10:38:00 +02:00
}
2015-08-03 16:11:23 +02:00
return result ;
} ;
2017-05-17 17:56:38 +02:00
var closePopup = function ( res , err ) {
2015-08-04 21:02:09 +02:00
res . writeHead ( 200 , {
'Content-Type' : 'text/html'
} ) ;
var content =
2015-08-03 16:11:23 +02:00
'<html><head><script>window.close()</script></head><body><H1>Verified</H1></body></html>' ;
2015-08-04 21:02:09 +02:00
if ( err )
content = '<html><body><h2>Sorry, an annoying error occured</h2><div>' + err + '</div><a onclick="window.close();">Close Window</a></body></html>' ;
res . end ( content , 'utf-8' ) ;
2017-05-12 22:38:33 +02:00
} ;