diff --git a/README.md b/README.md index 7916a7f4..e80ed138 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ passport.use(new SamlStrategy( * `host`: host for callback; will be combined with path and protocol to construct callback url if `callbackUrl` is not specified (default: `localhost`) * `entryPoint`: identity provider entrypoint * `issuer`: issuer string to supply to identity provider + * `audience`: expected saml response Audience (if not provided, Audience won't be verified) * `cert`: see [Security and signatures](#security-and-signatures) * `privateCert`: see [Security and signatures](#security-and-signatures) * `decryptionPvk`: optional private key that will be used to attempt to decrypt any encrypted assertions that are received diff --git a/lib/passport-saml/saml.js b/lib/passport-saml/saml.js index a0e74e02..1b7e4a80 100644 --- a/lib/passport-saml/saml.js +++ b/lib/passport-saml/saml.js @@ -802,6 +802,13 @@ SAML.prototype.processValidlySignedAssertion = function(xml, inResponseTo, callb if(conErr) throw conErr; } + + if (self.options.audience) { + var audienceErr = self.checkAudienceValidityError( + self.options.audience, conditions.AudienceRestriction); + if(audienceErr) + throw audienceErr; + } var attributeStatement = assertion.AttributeStatement; if (attributeStatement) { @@ -869,6 +876,28 @@ SAML.prototype.checkTimestampsValidityError = function(nowMs, notBefore, notOnOr return null; }; +SAML.prototype.checkAudienceValidityError = function(expectedAudience, audienceRestrictions) { + var self = this; + if (!audienceRestrictions || audienceRestrictions.length < 1) { + return new Error('SAML assertion has no AudienceRestriction'); + } + var errors = audienceRestrictions.map(function(restriction) { + if (!restriction.Audience || !restriction.Audience[0]) { + return new Error('SAML assertion AudienceRestriction has no Audience value'); + } + if (restriction.Audience[0] !== expectedAudience) { + return new Error('SAML assertion audience mismatch'); + } + return null; + }).filter(function(result) { + return result !== null; + }); + if (errors.length > 0) { + return errors[0]; + } + return null; +}; + SAML.prototype.validatePostRequest = function (container, callback) { var self = this; var xml = new Buffer(container.SAMLRequest, 'base64').toString('utf8'); diff --git a/test/tests.js b/test/tests.js index e35003b3..a4717d1b 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1505,6 +1505,70 @@ describe( 'passport-saml /', function() { done(); }); }); + + it( 'onelogin xml document with audience and no AudienceRestriction should not pass', function( done ) { + var xml = 'https://app.onelogin.com/saml/metadata/371755' + + 'https://app.onelogin.com/saml/metadata/371755DCnPTQYBb1hKspbe6fg1U3q8xn4=e0+aFomA0+JAY0f9tKqzIuqIVSSw7LiFUsneEDKPBWdiTz1sMdgr/2y1e9+rjaS2mRmCi/vSQLY3zTYz0hp6nJNU19+TWoXo9kHQyWT4KkeQL4Xs/gZ/AoKC20iHVKtpPps0IQ0Ml/qRoouSitt6Sf/WDz2LV/pWcH2hx5tv3xSw36hK2NQc7qw7r1mEXnvcjXReYo8rrVf7XHGGxNoRIEICUIi110uvsWemSXf0Z0dyb0FVYOWuSsQMDlzNpheADBifFO4UTfSEhFZvn8kVCGZUIwrbOhZ2d/+YEtgyuTg+qtslgfy4dwd4TvEcfuRzQTazeefprSFyiQckAXOjcw=='+TEST_CERT+'ploer@subspacesw.comurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + + ''; + var base64xml = new Buffer( xml ).toString('base64'); + var container = { SAMLResponse: base64xml }; + + var samlConfig = { + entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755', + audience: 'http://sp.example.com', + acceptedClockSkewMs: -1 + }; + var samlObj = new SAML( samlConfig ); + + samlObj.validatePostResponse( container, function( err, profile, logout ) { + should.exist( err ); + err.message.should.match( 'SAML assertion has no AudienceRestriction' ); + done(); + }); + }); + + it( 'onelogin xml document with audience not matching AudienceRestriction should not pass', function( done ) { + var xml = 'https://app.onelogin.com/saml/metadata/371755' + + 'https://app.onelogin.com/saml/metadata/371755DCnPTQYBb1hKspbe6fg1U3q8xn4=e0+aFomA0+JAY0f9tKqzIuqIVSSw7LiFUsneEDKPBWdiTz1sMdgr/2y1e9+rjaS2mRmCi/vSQLY3zTYz0hp6nJNU19+TWoXo9kHQyWT4KkeQL4Xs/gZ/AoKC20iHVKtpPps0IQ0Ml/qRoouSitt6Sf/WDz2LV/pWcH2hx5tv3xSw36hK2NQc7qw7r1mEXnvcjXReYo8rrVf7XHGGxNoRIEICUIi110uvsWemSXf0Z0dyb0FVYOWuSsQMDlzNpheADBifFO4UTfSEhFZvn8kVCGZUIwrbOhZ2d/+YEtgyuTg+qtslgfy4dwd4TvEcfuRzQTazeefprSFyiQckAXOjcw=='+TEST_CERT+'ploer@subspacesw.com{audience}urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + + ''; + var base64xml = new Buffer( xml ).toString('base64'); + var container = { SAMLResponse: base64xml }; + + var samlConfig = { + entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755', + audience: 'http://sp.example.com', + acceptedClockSkewMs: -1 + }; + var samlObj = new SAML( samlConfig ); + + samlObj.validatePostResponse( container, function( err, profile, logout ) { + should.exist( err ); + err.message.should.match( 'SAML assertion audience mismatch' ); + done(); + }); + }); + + it( 'onelogin xml document with audience matching AudienceRestriction should pass', function( done ) { + var xml = 'https://app.onelogin.com/saml/metadata/371755' + + 'https://app.onelogin.com/saml/metadata/371755DCnPTQYBb1hKspbe6fg1U3q8xn4=e0+aFomA0+JAY0f9tKqzIuqIVSSw7LiFUsneEDKPBWdiTz1sMdgr/2y1e9+rjaS2mRmCi/vSQLY3zTYz0hp6nJNU19+TWoXo9kHQyWT4KkeQL4Xs/gZ/AoKC20iHVKtpPps0IQ0Ml/qRoouSitt6Sf/WDz2LV/pWcH2hx5tv3xSw36hK2NQc7qw7r1mEXnvcjXReYo8rrVf7XHGGxNoRIEICUIi110uvsWemSXf0Z0dyb0FVYOWuSsQMDlzNpheADBifFO4UTfSEhFZvn8kVCGZUIwrbOhZ2d/+YEtgyuTg+qtslgfy4dwd4TvEcfuRzQTazeefprSFyiQckAXOjcw=='+TEST_CERT+'ploer@subspacesw.comhttp://sp.example.comurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + + ''; + var base64xml = new Buffer( xml ).toString('base64'); + var container = { SAMLResponse: base64xml }; + + var samlConfig = { + entryPoint: 'https://app.onelogin.com/trust/saml2/http-post/sso/371755', + audience: 'http://sp.example.com', + acceptedClockSkewMs: -1 + }; + var samlObj = new SAML( samlConfig ); + + samlObj.validatePostResponse( container, function( err, profile, logout ) { + should.not.exist( err ); + profile.nameID.should.startWith( 'ploer' ); + done(); + }); + }); + }); }); describe('validatePostRequest()', function() {