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() {