Skip to content

Commit 770787d

Browse files
author
Zach McElrath
committed
Add option to back up the issue instant to account for clock skew
1 parent 16c4491 commit 770787d

5 files changed

Lines changed: 90 additions & 13 deletions

File tree

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
Create SAML assertions.
22

3-
NOTE: currently supports SAML 1.1 tokens
3+
NOTE: currently supports SAML 1.1 and SAML 2.0 tokens
44

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

77
### Usage
88

99
```js
10-
var saml11 = require('saml').Saml11;
1110

1211
var options = {
12+
// Required
1313
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
1414
key: fs.readFileSync(__dirname + '/test-auth0.key'),
15+
// Optional
1516
issuer: 'urn:issuer',
17+
issueInstantSkewInSeconds: 60,
1618
lifetimeInSeconds: 600,
1719
audiences: 'urn:myapp',
1820
attributes: {
@@ -23,10 +25,15 @@ var options = {
2325
sessionIndex: '_faed468a-15a0-4668-aed6-3d9c478cc8fa'
2426
};
2527

28+
// SAML 1.1
29+
var saml11 = require('saml').Saml11;
2630
var signedAssertion = saml11.create(options);
27-
```
2831

29-
Everything except the cert and key is optional.
32+
// SAML 2.0
33+
var saml20 = require('saml').Saml20;
34+
var signedAssertion = saml20.create(options);
35+
36+
```
3037

3138
## Issue Reporting
3239

lib/saml11.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var path = require('path');
1212
var saml11 = fs.readFileSync(path.join(__dirname, 'saml11.template')).toString();
1313

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

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

6364
var now = moment.utc();
64-
doc.documentElement.setAttribute('IssueInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
65+
66+
// Optionally, back up the issue instant to accommodate for clock skew in the assertion consumer
67+
if (!isNaN(options.issueInstantSkewInSeconds)) {
68+
now.subtract(options.issueInstantSkewInSeconds, 'seconds');
69+
}
70+
71+
doc.documentElement.setAttribute('IssueInstant', now.format(TIME_FORMAT));
6572
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions');
6673

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

7279
if (options.audiences) {
@@ -107,7 +114,7 @@ exports.create = function(options, callback) {
107114
}
108115

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

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

lib/saml20.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var path = require('path');
1212
var saml20 = fs.readFileSync(path.join(__dirname, 'saml20.template')).toString();
1313

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

1617
var algorithms = {
1718
signature: {
@@ -102,15 +103,21 @@ exports.create = function(options, callback) {
102103
}
103104

104105
var now = moment.utc();
105-
doc.documentElement.setAttribute('IssueInstant', now.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
106+
107+
// Optionally, back up the issue instant to accommodate for clock skew in the assertion consumer
108+
if (!isNaN(options.issueInstantSkewInSeconds)) {
109+
now.subtract(options.issueInstantSkewInSeconds, 'seconds');
110+
}
111+
112+
doc.documentElement.setAttribute('IssueInstant', now.format(TIME_FORMAT));
106113
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions');
107114
var confirmationData = doc.documentElement.getElementsByTagName('saml:SubjectConfirmationData');
108115

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

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

116123
if (options.audiences) {
@@ -168,7 +175,7 @@ exports.create = function(options, callback) {
168175
}
169176

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

173180
if (options.sessionIndex) {
174181
doc.getElementsByTagName('saml:AuthnStatement')[0]

test/saml11.tests.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,34 @@ describe('saml 1.1', function () {
8181
assert.equal(600, lifetime);
8282
});
8383

84+
it('should skew the issue instant if requested', function () {
85+
86+
var options = {
87+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
88+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
89+
lifetimeInSeconds: 600,
90+
issueInstantSkewInSeconds: 60,
91+
};
92+
93+
var signedAssertion = saml11.create(options);
94+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
95+
assert.equal(true, isValid);
96+
97+
var conditions = utils.getConditions(signedAssertion);
98+
assert.equal(1, conditions.length);
99+
var notBefore = conditions[0].getAttribute('NotBefore');
100+
var notOnOrAfter = conditions[0].getAttribute('NotOnOrAfter');
101+
102+
should.ok(notBefore);
103+
should.ok(notOnOrAfter);
104+
105+
var skew = Math.round((moment.utc() - moment(notBefore).utc()) / 1000);
106+
assert.equal(60, skew);
107+
108+
var lifetime = Math.round((moment(notOnOrAfter).utc() - moment(notBefore).utc()) / 1000);
109+
assert.equal(600, lifetime);
110+
});
111+
84112
it('should set audience restriction', function () {
85113
var options = {
86114
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),

test/saml20.tests.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,34 @@ describe('saml 2.0', function () {
340340
assert.equal('specific', authnContextClassRef.textContent);
341341
});
342342

343+
it('should skew the issue instant if requested', function () {
344+
345+
var options = {
346+
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),
347+
key: fs.readFileSync(__dirname + '/test-auth0.key'),
348+
lifetimeInSeconds: 600,
349+
issueInstantSkewInSeconds: 60,
350+
};
351+
352+
var signedAssertion = saml.create(options);
353+
var isValid = utils.isValidSignature(signedAssertion, options.cert);
354+
assert.equal(true, isValid);
355+
356+
var conditions = utils.getConditions(signedAssertion);
357+
assert.equal(1, conditions.length);
358+
var notBefore = conditions[0].getAttribute('NotBefore');
359+
var notOnOrAfter = conditions[0].getAttribute('NotOnOrAfter');
360+
361+
should.ok(notBefore);
362+
should.ok(notOnOrAfter);
363+
364+
var skew = Math.round((moment.utc() - moment(notBefore).utc()) / 1000);
365+
assert.equal(60, skew);
366+
367+
var lifetime = Math.round((moment(notOnOrAfter).utc() - moment(notBefore).utc()) / 1000);
368+
assert.equal(600, lifetime);
369+
});
370+
343371
it('should place signature where specified', function () {
344372
var options = {
345373
cert: fs.readFileSync(__dirname + '/test-auth0.pem'),

0 commit comments

Comments
 (0)