Skip to content

Commit

Permalink
Add option to back up the issue instant to account for clock skew
Browse files Browse the repository at this point in the history
  • Loading branch information
Zach McElrath committed Mar 6, 2018
1 parent 16c4491 commit 0e2c0f8
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 13 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
Create SAML assertions.

NOTE: currently supports SAML 1.1 tokens
NOTE: currently supports SAML 1.1 and SAML 2.0 tokens

[![Build Status](https://travis-ci.org/auth0/node-saml.png)](https://travis-ci.org/auth0/node-saml)

### Usage

```js
var saml11 = require('saml').Saml11;

var options = {
// Required
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key'),
// Optional
issuer: 'urn:issuer',
issueInstantSkewInSeconds: 60,
lifetimeInSeconds: 600,
audiences: 'urn:myapp',
attributes: {
Expand All @@ -23,10 +25,15 @@ var options = {
sessionIndex: '_faed468a-15a0-4668-aed6-3d9c478cc8fa'
};

// SAML 1.1
var saml11 = require('saml').Saml11;
var signedAssertion = saml11.create(options);
```

Everything except the cert and key is optional.
// SAML 2.0
var saml20 = require('saml').Saml20;
var signedAssertion = saml20.create(options);

```

## Issue Reporting

Expand Down
15 changes: 11 additions & 4 deletions lib/saml11.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var path = require('path');
var saml11 = fs.readFileSync(path.join(__dirname, 'saml11.template')).toString();

var NAMESPACE = 'urn:oasis:names:tc:SAML:1.0:assertion';
var TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSS[Z]';

var algorithms = {
signature: {
Expand Down Expand Up @@ -61,12 +62,18 @@ exports.create = function(options, callback) {
doc.documentElement.setAttribute('Issuer', options.issuer);

var now = moment.utc();
doc.documentElement.setAttribute('IssueInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));

// Optionally, back up the issue instant to accommodate for clock skew in the assertion consumer
if (!isNaN(options.issueInstantSkewInSeconds)) {
now.subtract(options.issueInstantSkewInSeconds, 'seconds');
}

doc.documentElement.setAttribute('IssueInstant', now.format(TIME_FORMAT));
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions');

if (options.lifetimeInSeconds) {
conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
conditions[0].setAttribute('NotOnOrAfter', now.add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
conditions[0].setAttribute('NotBefore', now.format(TIME_FORMAT));
conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format(TIME_FORMAT));
}

if (options.audiences) {
Expand Down Expand Up @@ -107,7 +114,7 @@ exports.create = function(options, callback) {
}

doc.getElementsByTagName('saml:AuthenticationStatement')[0]
.setAttribute('AuthenticationInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
.setAttribute('AuthenticationInstant', now.format(TIME_FORMAT));

var nameID = doc.documentElement.getElementsByTagNameNS(NAMESPACE, 'NameIdentifier')[0];

Expand Down
17 changes: 12 additions & 5 deletions lib/saml20.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var path = require('path');
var saml20 = fs.readFileSync(path.join(__dirname, 'saml20.template')).toString();

var NAMESPACE = 'urn:oasis:names:tc:SAML:2.0:assertion';
var TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSS[Z]';

var algorithms = {
signature: {
Expand Down Expand Up @@ -102,15 +103,21 @@ exports.create = function(options, callback) {
}

var now = moment.utc();
doc.documentElement.setAttribute('IssueInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));

// Optionally, back up the issue instant to accommodate for clock skew in the assertion consumer
if (!isNaN(options.issueInstantSkewInSeconds)) {
now.subtract(options.issueInstantSkewInSeconds, 'seconds');
}

doc.documentElement.setAttribute('IssueInstant', now.format(TIME_FORMAT));
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions');
var confirmationData = doc.documentElement.getElementsByTagName('saml:SubjectConfirmationData');

if (options.lifetimeInSeconds) {
conditions[0].setAttribute('NotBefore', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
conditions[0].setAttribute('NotBefore', now.format(TIME_FORMAT));
conditions[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format(TIME_FORMAT));

confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
confirmationData[0].setAttribute('NotOnOrAfter', now.clone().add(options.lifetimeInSeconds, 'seconds').format(TIME_FORMAT));
}

if (options.audiences) {
Expand Down Expand Up @@ -168,7 +175,7 @@ exports.create = function(options, callback) {
}

doc.getElementsByTagName('saml:AuthnStatement')[0]
.setAttribute('AuthnInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
.setAttribute('AuthnInstant', now.format(TIME_FORMAT));

if (options.sessionIndex) {
doc.getElementsByTagName('saml:AuthnStatement')[0]
Expand Down
28 changes: 28 additions & 0 deletions test/saml11.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ describe('saml 1.1', function () {
assert.equal(600, lifetime);
});

it('should skew the issue instant if requested', function () {

var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key'),
lifetimeInSeconds: 600,
issueInstantSkewInSeconds: 60,
};

var signedAssertion = saml.create(options);
var isValid = utils.isValidSignature(signedAssertion, options.cert);
assert.equal(true, isValid);

var conditions = utils.getConditions(signedAssertion);
assert.equal(1, conditions.length);
var notBefore = conditions[0].getAttribute('NotBefore');
var notOnOrAfter = conditions[0].getAttribute('NotOnOrAfter');

should.ok(notBefore);
should.ok(notOnOrAfter);

var skew = Math.round((moment.utc() - moment(notBefore).utc()) / 1000);
assert.equal(60, skew);

var lifetime = Math.round((moment(notOnOrAfter).utc() - moment(notBefore).utc()) / 1000);
assert.equal(600, lifetime);
});

it('should set audience restriction', function () {
var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
Expand Down
28 changes: 28 additions & 0 deletions test/saml20.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,34 @@ describe('saml 2.0', function () {
assert.equal('specific', authnContextClassRef.textContent);
});

it('should skew the issue instant if requested', function () {

var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
key: fs.readFileSync(__dirname + '/test-auth0.key'),
lifetimeInSeconds: 600,
issueInstantSkewInSeconds: 60,
};

var signedAssertion = saml.create(options);
var isValid = utils.isValidSignature(signedAssertion, options.cert);
assert.equal(true, isValid);

var conditions = utils.getConditions(signedAssertion);
assert.equal(1, conditions.length);
var notBefore = conditions[0].getAttribute('NotBefore');
var notOnOrAfter = conditions[0].getAttribute('NotOnOrAfter');

should.ok(notBefore);
should.ok(notOnOrAfter);

var skew = Math.round((moment.utc() - moment(notBefore).utc()) / 1000);
assert.equal(60, skew);

var lifetime = Math.round((moment(notOnOrAfter).utc() - moment(notBefore).utc()) / 1000);
assert.equal(600, lifetime);
});

it('should place signature where specified', function () {
var options = {
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
Expand Down

0 comments on commit 0e2c0f8

Please sign in to comment.