Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = saml11.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