saml2-js
Version:
SAML 2.0 node helpers
1,103 lines (941 loc) • 59.8 kB
text/coffeescript
_ = require 'underscore'
assert = require 'assert'
async = require 'async'
zlib = require 'zlib'
crypto = require 'crypto'
fs = require 'fs'
saml2 = require "#{__dirname}/../lib/saml2"
url = require 'url'
util = require 'util'
xmldom = require '@xmldom/xmldom'
xmlcrypto = require 'xml-crypto'
describe 'saml2', ->
get_test_file = (filename) ->
fs.readFileSync("#{__dirname}/data/#{filename}").toString()
has_attribute = (node, attr_name, attr_value) ->
_(node.attributes).some (attr) -> attr.name is attr_name and attr.value is attr_value
describe 'private helpers', ->
dom_from_test_file = (filename) ->
(new xmldom.DOMParser()).parseFromString get_test_file filename
before =>
@good_response_dom = dom_from_test_file "good_response.xml"
# Auth Request, before it is compressed and base-64 encoded
describe 'create_authn_request', ->
it 'contains expected fields', ->
{ id, xml } = saml2.create_authn_request 'https://sp.example.com/metadata.xml', 'https://sp.example.com/assert', 'https://idp.example.com/login'
dom = (new xmldom.DOMParser()).parseFromString xml
authn_request = dom.getElementsByTagName('AuthnRequest')[0]
required_attributes =
Version: '2.0'
Destination: 'https://idp.example.com/login'
AssertionConsumerServiceURL: 'https://sp.example.com/assert'
ProtocolBinding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
_(required_attributes).each (req_value, req_name) ->
assert _(authn_request.attributes).some((attr) -> attr.name is req_name and attr.value is req_value)
, "Expected to find attribute '#{req_name}' with value '#{req_value}'!"
assert _(authn_request.attributes).some((attr) -> attr.name is "ID"), "Missing required attribute 'ID'"
assert.equal dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer')[0].firstChild.data, 'https://sp.example.com/metadata.xml'
it 'contains an AuthnContext if requested', ->
{ id, xml } = saml2.create_authn_request 'a', 'b', 'c', true, { comparison: 'exact', class_refs: ['context:class']}
dom = (new xmldom.DOMParser()).parseFromString xml
authn_request = dom.getElementsByTagName('AuthnRequest')[0]
requested_authn_context = authn_request.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'RequestedAuthnContext')[0]
assert _(requested_authn_context.attributes).some((attr) -> attr.name is 'Comparison' and attr.value is 'exact'), "Could not determine if specified attribute had proper value (Comparison=exact)"
assert.equal requested_authn_context.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnContextClassRef')[0].firstChild.data, 'context:class'
describe 'create_metadata', ->
CERT_1 = get_test_file 'test.crt'
CERT_2 = get_test_file 'test2.crt'
CERT_1_DATA = saml2.extract_certificate_data CERT_1
CERT_2_DATA = saml2.extract_certificate_data CERT_2
METADATA =
saml2.create_metadata(
'https://sp.example.com/metadata.xml',
'https://sp.example.com/assert',
[CERT_1],
[CERT_1, CERT_2])
dom = (new xmldom.DOMParser()).parseFromString METADATA
entity_descriptor =
dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:metadata', 'EntityDescriptor')[0]
it 'contains expected entity id', ->
assert(
has_attribute entity_descriptor, 'entityID', 'https://sp.example.com/metadata.xml',
"Expected to find attribute 'entityID' with value 'https://sp.example.com/metadata.xml'.")
it 'contains expected key descriptors', ->
key_descriptors = entity_descriptor.getElementsByTagNameNS(
'urn:oasis:names:tc:SAML:2.0:metadata', 'KeyDescriptor')
assert.equal(
key_descriptors.length, 3, "Expected 3 key descriptors; found #{key_descriptors.length}")
assert(
has_attribute key_descriptors[0], 'use', 'signing',
"Expected 1st key descriptor to have attribute 'use' with value 'signing'.")
assert(
has_attribute key_descriptors[1], 'use', 'encryption',
"Expected 2nd key descriptor to have attribute 'use' with value 'encryption'.")
assert(
has_attribute key_descriptors[2], 'use', 'encryption',
"Expected 3rd key descriptor to have attribute 'use' with value 'encryption'.")
signing_cert = key_descriptors[0].getElementsByTagNameNS(
'http://www.w3.org/2000/09/xmldsig#', 'X509Certificate')[0].firstChild
assert.equal signing_cert, CERT_1_DATA, 'Unexpected value for signing cert.'
encryption_cert_1 = key_descriptors[1].getElementsByTagNameNS(
'http://www.w3.org/2000/09/xmldsig#', 'X509Certificate')[0].firstChild
assert.equal encryption_cert_1, CERT_1_DATA, 'Unexpected value for 1st encryption cert.'
encryption_cert_2 = key_descriptors[2].getElementsByTagNameNS(
'http://www.w3.org/2000/09/xmldsig#', 'X509Certificate')[0].firstChild
assert.equal encryption_cert_2, CERT_2_DATA, 'Unexpected value for 2nd encryption cert.'
it 'contains expected service URLs', ->
consumer_service = entity_descriptor.getElementsByTagNameNS(
'urn:oasis:names:tc:SAML:2.0:metadata', 'AssertionConsumerService')[0]
assert(
has_attribute(
consumer_service, 'Binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'),
"Expected to find an AssertionConsumerService with POST binding.")
assert(
has_attribute consumer_service, 'Location', 'https://sp.example.com/assert',
"Expected to find an AssertionConsumerService with location
'htps://sp.example.com/assert'")
logout_service = entity_descriptor.getElementsByTagNameNS(
'urn:oasis:names:tc:SAML:2.0:metadata', 'SingleLogoutService')[0]
assert(
has_attribute(
logout_service, 'Binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
"Expected to find an SingleLogoutService with redirect binding.")
assert(
has_attribute logout_service, 'Location', 'https://sp.example.com/assert',
"Expected to find an SingleLogoutService with location 'htps://sp.example.com/assert'")
it 'contains only one SPSSODescriptor', ->
sp_sso_descriptor = entity_descriptor.getElementsByTagNameNS(
'urn:oasis:names:tc:SAML:2.0:metadata', 'SPSSODescriptor')
assert.equal(
sp_sso_descriptor.length, 1, "Expected 1 SP SSO descriptor; found #{sp_sso_descriptor.length}")
describe 'format_pem', ->
it 'formats an unformatted private key', ->
raw_private_key = (/-----BEGIN PRIVATE KEY-----([^-]*)-----END PRIVATE KEY-----/g.exec get_test_file("test.pem"))[1]
formatted_key = saml2.format_pem raw_private_key, 'PRIVATE KEY'
assert.equal formatted_key.trim(), get_test_file("test.pem").trim()
it 'does not change an already formatted private key', ->
formatted_key = saml2.format_pem get_test_file("test.pem"), 'PRIVATE KEY'
assert.equal formatted_key, get_test_file("test.pem")
describe 'sign_get_request', ->
it 'correctly signs a get request', ->
signed = saml2.sign_request 'TESTMESSAGE', get_test_file("test.pem")
verifier = crypto.createVerify 'RSA-SHA256'
verifier.update 'SAMLRequest=TESTMESSAGE&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256'
assert verifier.verify(get_test_file("test.crt"), signed.Signature, 'base64'), "Signature is not valid"
assert.equal signed.SigAlg, 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
assert.equal signed.SAMLRequest, 'TESTMESSAGE'
it 'correctly signs a get response with RelayState', ->
signed = saml2.sign_request 'TESTMESSAGE', get_test_file("test.pem"), 'TESTSTATE', true
verifier = crypto.createVerify 'RSA-SHA256'
verifier.update 'SAMLResponse=TESTMESSAGE&RelayState=TESTSTATE&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256'
assert verifier.verify(get_test_file("test.crt"), signed.Signature, 'base64'), "Signature is not valid"
assert signed.SigAlg, 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
assert.equal signed.RelayState, 'TESTSTATE'
assert.equal signed.SAMLResponse, 'TESTMESSAGE'
describe 'sign_authn_request_with_embedded_signature', ->
it 'correctly embeds the signature', ->
{ id, xml } = saml2.create_authn_request 'https://sp.example.com/metadata.xml', 'https://sp.example.com/assert', 'https://idp.example.com/login'
signed = saml2.sign_authn_request xml, get_test_file("test.pem")
result = saml2.check_saml_signature signed, get_test_file("test.crt")
assert result, 'validation result should not be null'
assert.equal result.length, 1, 'validation result should only have 1 item'
describe 'check_saml_signature', ->
it 'accepts signed xml', ->
result = saml2.check_saml_signature(get_test_file("good_assertion.xml"), get_test_file("test.crt"))
assert.deepEqual result, [get_test_file("good_assertion_signed_data.xml")]
it 'rejects xml without a signature', ->
assert.equal null, saml2.check_saml_signature(get_test_file("unsigned_assertion.xml"), get_test_file("test.crt"))
it 'rejects xml with an invalid signature', ->
assert.equal null, saml2.check_saml_signature(get_test_file("good_assertion.xml"), get_test_file("test2.crt"))
it 'validates a Response signature when a signature also exists within the Assertion', ->
assert.notEqual null, saml2.check_saml_signature(get_test_file("good_response_twice_signed.xml"), get_test_file("test.crt"))
it 'validates a Response signature when the dsig namespace is declared at the root level', ->
result = saml2.check_saml_signature(get_test_file("good_response_twice_signed_dsig_ns_at_top.xml"), get_test_file("test.crt"))
assert.notEqual null, result
it 'correctly ignores commented-out digest', ->
result = saml2.check_saml_signature(get_test_file("good_assertion_commented_out_digest.xml"), get_test_file("test.crt"))
assert.deepEqual result, [get_test_file("good_assertion_signed_data.xml")]
describe 'check_status_success', =>
it 'accepts a valid success status', =>
assert saml2.check_status_success(@good_response_dom), "Did not get 'true' for valid response."
it 'rejects a missing success status', ->
assert not saml2.check_status_success(dom_from_test_file("response_error_status.xml")), "Did not get 'false' for invalid response."
it 'rejects a missing status', ->
assert not saml2.check_status_success(dom_from_test_file("response_no_status.xml")), "Did not get 'false' for invalid response."
describe 'pretty_assertion_attributes', ->
it 'creates a correct user object', ->
test_attributes =
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": [ "tuser@example.com" ]
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": [ "Test User" ]
"http://schemas.xmlsoap.org/claims/Group": [ "Test Group" ]
expected =
email: "tuser@example.com"
name: "Test User"
group: "Test Group"
assert.deepEqual saml2.pretty_assertion_attributes(test_attributes), expected
describe 'decrypt_assertion', =>
KEY_1 = get_test_file("test.pem")
KEY_2 = get_test_file("test2.pem")
it 'decrypts and extracts an assertion with all availble keys', (done) =>
saml2.decrypt_assertion @good_response_dom, [KEY_2, KEY_1], (err, result) ->
assert not err?, "Got error: #{err}"
assert.equal result, get_test_file("good_response_decrypted.xml")
done()
it 'errors if an incorrect key is used', (done) =>
saml2.decrypt_assertion @good_response_dom, [KEY_2], (err, result) ->
assert (err instanceof Error), "Did not get expected error."
done()
describe 'parse_response_header', =>
it 'correctly parses a response header', =>
response = saml2.parse_response_header @good_response_dom
assert.equal response.destination, 'https://sp.example.com/assert'
assert.equal response.in_response_to, '_1'
it 'errors if there is no response', ->
# An assertion is not a response, so this should fail.
assert.throws -> saml2.parse_response_header dom_from_test_file("good_assertion.xml")
it 'errors if given a response with the wrong version', ->
assert.throws -> saml2.parse_response_header dom_from_test_file("response_bad_version.xml")
describe 'parse_logout_request', =>
it 'correctly parses a logout request', =>
request = saml2.parse_logout_request dom_from_test_file('logout_request.xml')
assert.equal request.issuer, 'http://idp.example.com/metadata.xml'
assert.equal request.name_id, 'tstudent'
assert.equal request.session_index, '_2'
describe 'get_name_id', ->
it 'gets the correct NameID', ->
name_id = saml2.get_name_id dom_from_test_file('good_assertion.xml')
assert.equal name_id, 'tstudent'
it 'parses assertions with explicit namespaces', ->
name_id = saml2.get_name_id dom_from_test_file('good_assertion_explicit_namespaces.xml')
assert.equal name_id, 'tstudent'
describe 'get_session_info', ->
it 'gets the correct session index', ->
info = saml2.get_session_info dom_from_test_file('good_assertion.xml')
assert.equal info.index, '_3'
it 'returns null for no session_index', ->
info = saml2.get_session_info dom_from_test_file('good_assertion_no_session_index.xml'), false
assert.equal info.index, null
it 'gets the correct session not on or after', ->
info = saml2.get_session_info dom_from_test_file('good_assertion.xml')
assert.equal info.not_on_or_after, '2014-03-13T22:35:05.387Z'
describe 'parse_assertion_attributes', ->
it 'correctly parses assertion attributes', ->
expected_attributes =
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname': [ 'Test' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': [ 'tstudent@example.com' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier': [ 'tstudent' ]
'http://schemas.xmlsoap.org/claims/Group': [ 'CN=Students,CN=Users,DC=idp,DC=example,DC=com' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname': [ 'Student' ]
'http://schemas.xmlsoap.org/claims/CommonName': [ 'Test Student' ]
attributes = saml2.parse_assertion_attributes dom_from_test_file('good_assertion.xml')
assert.deepEqual attributes, expected_attributes
it 'correctly parses assertion attributes', ->
expected_attributes =
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname': [ '' ]
attributes = saml2.parse_assertion_attributes dom_from_test_file('empty_attribute_value.xml')
assert.deepEqual attributes, expected_attributes
it 'correctly parses no assertion attributes', ->
attributes = saml2.parse_assertion_attributes dom_from_test_file('blank_assertion.xml')
assert.deepEqual attributes, {}
describe 'add_namespaces_to_child_assertions', ->
it 'adds namespaces defined by InclusiveNamespaces', ->
response = saml2.add_namespaces_to_child_assertions get_test_file('namespaced_assertion_with_inclusivenamespaces.xml')
dom = (new xmldom.DOMParser()).parseFromString response
assertion = dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')
attributes = (attr.name for attr in assertion[0].attributes)
assert.deepEqual attributes, [
'xmlns:xsi'
'xmlns:xs'
'xmlns:samlp'
'xmlns:saml'
]
# shouldn't have copied the namespace from the Response element
assert.equal attributes.indexOf('xmlns:foo'), -1
it 'copies namespaces from Response to Assertion if there is no InclusiveNamespaces', ->
response = saml2.add_namespaces_to_child_assertions get_test_file('namespaced_assertion.xml')
dom = (new xmldom.DOMParser()).parseFromString response
assertion = dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')
attributes = (attr.name for attr in assertion[0].attributes)
assert.deepEqual attributes, [
'xmlns:xsi'
'xmlns:xs'
'xmlns:samlp'
'xmlns:saml'
]
it 'copies namespaces from Response to Assertion if there is an empty InclusiveNamespaces', ->
response = saml2.add_namespaces_to_child_assertions get_test_file('namespaced_assertion_with_empty_inclusivenamespaces.xml')
dom = (new xmldom.DOMParser()).parseFromString response
assertion = dom.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')
attributes = (attr.name for attr in assertion[0].attributes)
assert.deepEqual attributes, [
'xmlns:xsi'
'xmlns:xs'
'xmlns:samlp'
'xmlns:saml'
'xmlns:foo'
]
describe 'set option defaults', ->
it 'sets defaults in the correct order', ->
options_top =
option1: "top"
option4: "top"
options_middle =
option1: "middle"
option2: "middle"
option5: "middle"
options_bottom =
option1: "bottom"
option2: "bottom"
option3: "bottom"
option6: "bottom"
expected_options =
option1: "top"
option2: "middle"
option3: "bottom"
option4: "top"
option5: "middle"
option6: "bottom"
actual_options = saml2.set_option_defaults options_top, options_middle, options_bottom
assert.deepEqual actual_options, expected_options
describe 'post_assert', ->
it 'returns a user object when passed a valid AuthnResponse', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
ignore_timing: true
request_body:
SAMLResponse: get_test_file("post_response.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert not err?, "Got error: #{err}"
expected_response =
response_header:
version: '2.0'
id: '_2'
in_response_to: '_1'
destination: 'https://sp.example.com/assert'
type: 'authn_response'
user:
name_id: 'tstudent'
session_index: '_3'
given_name: 'Test',
email: 'tstudent@example.com',
ppid: 'tstudent',
group: 'CN=Students,CN=Users,DC=idp,DC=example,DC=com',
surname: 'Student',
common_name: 'Test Student',
attributes:
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname': [ 'Test' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': [ 'tstudent@example.com' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier': [ 'tstudent' ]
'http://schemas.xmlsoap.org/claims/Group': [ 'CN=Students,CN=Users,DC=idp,DC=example,DC=com' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname': [ 'Student' ]
'http://schemas.xmlsoap.org/claims/CommonName': [ 'Test Student' ]
assert.deepEqual response, expected_response
done()
it 'allows the signature to be embedded outside of the assertion', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: get_test_file('test.crt')
request_options =
allow_unencrypted_assertion: true
ignore_timing: true
request_body:
SAMLResponse: get_test_file('response_external_signed_assertion.xml')
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert not err?, "Got error: #{err}"
expected_response =
response_header:
version: '2.0'
id: '_2'
in_response_to: '_1'
destination: 'https://sp.example.com/assert'
type: 'authn_response'
user:
name_id: 'tstudent',
session_index: '_3'
given_name: 'Test'
attributes:
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname': [ 'Test' ]
assert.deepEqual response, expected_response
done()
it 'errors if passed invalid data', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: get_test_file('test.crt')
resquest_options =
request_body:
SAMLResponse: 'FAIL'
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, resquest_options, (err, user) ->
assert (err instanceof Error), "Did not get expected error."
done()
it "rejects a signed response if the assertion isn't signed", (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt') ]
allow_unencrypted_assertion: true
request_options =
request_body:
SAMLResponse: get_test_file("response_unsigned_assertion.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
done()
it "rejects a signed response if the idp certificate is invalid", (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ 'INVALIDCERTIFICATEDATA', get_test_file('test.crt') ]
allow_unencrypted_assertion: true
request_options =
ignore_timing: true
request_body:
SAMLResponse: get_test_file("response_unsigned_assertion.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/may be invalid/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'correctly parses an empty NameID', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
ignore_signature: true
allow_unencrypted_assertion: true
ignore_timing: true
request_body:
SAMLResponse: get_test_file("empty_nameid.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert not err?, "Got error: #{err}"
expected_response =
response_header:
version: '2.0'
id: '_2'
in_response_to: '_1'
destination: 'https://sp.example.com/assert'
type: 'authn_response'
user:
name_id: undefined
session_index: '_4'
session_not_on_or_after: '2016-02-11T21:12:09Z'
attributes: {}
assert.deepEqual response, expected_response
done()
it 'returns an error when there is no Subject', (done) ->
# This test is validating existing behavior from v2.0.3 and prior. Per
# the SAML2 spec, Subject is actually optional
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
ignore_signature: true
allow_unencrypted_assertion: true
ignore_timing: true
request_body:
SAMLResponse: new Buffer(get_test_file("no_subject.xml")).toString('base64')
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert.equal("Expected 1 Subject; found 0", err.message, "Unexpected error message:" + err.message)
done()
describe 'when AuthnStatement has no session_index', ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
ignore_timing: true
request_body:
SAMLResponse: get_test_file("empty_session_index.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
it 'correctly parses an AuthnStatement when require_session_index is false', (done) ->
sp.post_assert idp, request_options, (err, response) ->
assert not err?, "Got error: #{err}"
expected_response =
response_header:
version: '2.0'
id: '_2'
in_response_to: '_1'
destination: 'https://sp.example.com/assert'
type: 'authn_response'
user:
name_id: undefined
session_index: null
session_not_on_or_after: '2016-02-11T21:12:09Z'
attributes: {}
assert.deepEqual response, expected_response
done()
it 'returns an error when require_session_index is true', (done) ->
sp.post_assert idp, _.extend({}, request_options, {require_session_index: true}), (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert.equal("SessionIndex not an attribute of AuthnStatement.", err.message, "Unexpected error message:" + err.message)
done()
it 'rejects an assertion with an NotBefore condition in the future', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: get_test_file("response_notbefore_future.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not yet valid/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'rejects an encrypted assertion with an NotBefore condition in the future', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: get_test_file("response_notbefore_future_encrypted.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not yet valid/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'rejects a signed-then-encrypted assertion with a NotBefore condition in the future', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
request_body:
SAMLResponse: get_test_file("response_notbefore_future_signed_then_encrypted_base64.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not yet valid/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'rejects an encrypted-then-signed assertion with a NotBefore condition in the future', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
request_body:
SAMLResponse: get_test_file("response_notbefore_future_encrypted_then_signed_base64.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not yet valid/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'throws if options.notbefore_skew is not a number', (done) ->
sp_options = { notbefore_skew: 'carrot_cake' }
request_options = { request_body: { SAMLResponse: '…' } }
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider {}
sp.post_assert idp, request_options, (err, data) ->
assert (err instanceof Error), "Did not get expected error."
assert (/notbefore_skew/.test(err.message)), "Unexpected error message: " + err.message
done()
it 'accepts an assertion with an NotBefore condition in the future but within the specified skew tolerance', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
# 5 second grace period:
notbefore_skew: 5
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
saml_response = get_test_file("response_notbefore_future_decoded.xml")\
.replace 'NotBefore="2054-03-12T21:35:05.387Z"',
# mimicking an IdP with a clock 3 seconds ahead of ours
"NotBefore=\"#{new Date(Date.now()+3000).toISOString()}\""
saml_response_base64 = Buffer.from(saml_response, 'utf8').toString('base64')
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: saml_response_base64
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
console.error err if err?
assert !err?, 'Response was wrongly rejected'
done()
it 'rejects an assertion with an NotOnOrAfter condition in the past', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: get_test_file("response_unsigned_assertion.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is no longer valid/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'rejects an encrypted assertion with an NotOnOrAfter condition in the past', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: get_test_file("post_response.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.post_assert idp, request_options, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is no longer valid/.test(err.message)), "Unexpected error message:" + err.message
done()
context 'when response contains AudienceRestriction', ->
sp_options = (properties = {}) ->
_.extend
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
, properties
idp_options = (properties = {}) ->
_.extend
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
, properties
request_options = (properties = {}) ->
_.extend
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: get_test_file("response_audience_no_timing.xml")
, properties
it 'rejects an empty audience', (done) ->
sp = new saml2.ServiceProvider sp_options
audience: 'https://another-sp.example.com/metadata.xml'
idp = new saml2.IdentityProvider idp_options()
request = request_options
request_body:
SAMLResponse: get_test_file("response_empty_audience_no_timing.xml")
sp.post_assert idp, request, (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not valid for this audience/.test(err.message)), "Unexpected error message:" + err.message
done()
context 'and `audience` option is set to a string', ->
it 'rejects non-matching audience', (done) ->
sp = new saml2.ServiceProvider sp_options
audience: 'https://another-sp.example.com/metadata.xml'
idp = new saml2.IdentityProvider idp_options()
sp.post_assert idp, request_options(), (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not valid for this audience/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'accepts a matching audience', (done) ->
sp = new saml2.ServiceProvider sp_options
audience: 'https://sp.example.com/metadata.xml'
idp = new saml2.IdentityProvider idp_options()
sp.post_assert idp, request_options(), (err, response) ->
assert not err?, "Got error: #{err}"
done()
context 'and `audience` option is set to a regex', ->
it 'rejects non-matching audience', (done) ->
sp = new saml2.ServiceProvider sp_options
audience: /^https:\/\/another-sp\./
idp = new saml2.IdentityProvider idp_options()
sp.post_assert idp, request_options(), (err, response) ->
assert (err instanceof Error), "Did not get expected error."
assert (/SAML Response is not valid for this audience/.test(err.message)), "Unexpected error message:" + err.message
done()
it 'accepts a matching audience', (done) ->
sp = new saml2.ServiceProvider sp_options
audience: /^https:\/\/sp\.example\.com/
idp = new saml2.IdentityProvider idp_options()
sp.post_assert idp, request_options(), (err, response) ->
assert not err?, "Got error: #{err}"
done()
context 'and `audience` option is not set', ->
it 'accepts any audience', (done) ->
sp = new saml2.ServiceProvider sp_options()
idp = new saml2.IdentityProvider idp_options()
sp.post_assert idp, request_options(), (err, response) ->
assert not err?, "Got error: #{err}"
done()
context 'when response does not contain AudienceRestriction', ->
idp = new saml2.IdentityProvider
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
require_session_index: false
ignore_signature: true
allow_unencrypted_assertion: true
request_body:
SAMLResponse: get_test_file("response_no_audience_no_timing.xml")
sp_options = (audience) ->
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test2.pem')
alt_private_keys: get_test_file('test.pem')
certificate: get_test_file('test2.crt')
alt_certs: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
audience: audience
it 'accepts a response with a matching string `audience` option', (done) ->
sp = new saml2.ServiceProvider sp_options('https://sp.example.com/metadata.xml')
sp.post_assert idp, request_options, (err, response) ->
assert !err?, 'Response was wrongly rejected'
done()
it 'accepts a response with a non-matching string `audience` option', (done) ->
sp = new saml2.ServiceProvider sp_options('https://whatever.com')
sp.post_assert idp, request_options, (err, response) ->
assert !err?, 'Response was wrongly rejected'
done()
it 'accepts a response with a non-matching regex `audience` option', (done) ->
sp = new saml2.ServiceProvider sp_options(/whatever/)
sp.post_assert idp, request_options, (err, response) ->
assert !err?, 'Response was wrongly rejected'
done()
describe 'redirect assert', ->
it 'returns a user object with passed a valid AuthnResponse', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: [ get_test_file('test.crt'), get_test_file('test2.crt') ]
request_options =
ignore_timing: true
request_body:
SAMLResponse: get_test_file("redirect_response.xml")
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.redirect_assert idp, request_options, (err, response) ->
assert not err?, "Got error: #{err}"
expected_response =
response_header:
version: '2.0'
id: '_2'
in_response_to: '_1'
destination: 'https://sp.example.com/assert'
type: 'authn_response'
user:
name_id: 'tstudent'
session_index: '_3'
given_name: 'Test',
email: 'tstudent@example.com',
ppid: 'tstudent',
group: 'CN=Students,CN=Users,DC=idp,DC=example,DC=com',
surname: 'Student',
common_name: 'Test Student',
attributes:
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname': [ 'Test' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': [ 'tstudent@example.com' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier': [ 'tstudent' ]
'http://schemas.xmlsoap.org/claims/Group': [ 'CN=Students,CN=Users,DC=idp,DC=example,DC=com' ]
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname': [ 'Student' ]
'http://schemas.xmlsoap.org/claims/CommonName': [ 'Test Student' ]
assert.deepEqual response, expected_response
done()
describe 'ServiceProvider', ->
it 'can be constructed', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
sp = new saml2.ServiceProvider sp_options
done()
it 'can create login request url', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: 'other_service_cert'
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
async.waterfall [
(cb_wf) -> sp.create_login_request_url idp, {assert_endpoint:'https://sp.example.com/assert'}, cb_wf
], (err, login_url, id) ->
assert not err?, "Error creating login URL: #{err}"
parsed_url = url.parse login_url, true
saml_request = parsed_url.query?.SAMLRequest?
assert saml_request, 'Could not find SAMLRequest in url query parameters'
done()
it 'can create login request url with query paramters', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login?partnerid=abcdef1234&random_param=foo'
sso_logout_url: 'https://idp.example.com/logout'
certificates: 'other_service_cert'
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
async.waterfall [
(cb_wf) -> sp.create_login_request_url idp, {assert_endpoint:'https://sp.example.com/assert'}, cb_wf
], (err, login_url, id) ->
assert not err?, "Error creating login URL: #{err}"
parsed_url = url.parse login_url, true
saml_request = parsed_url.query?.SAMLRequest?
assert.deepEqual _(parsed_url.query).omit("SAMLRequest"), {
partnerid: "abcdef1234"
random_param: "foo"
}
assert saml_request, 'Could not find SAMLRequest in url query parameters'
done()
it 'passes through RelayState in create login request url', (done) ->
sp_options =
entity_id: 'https://sp.example.com/metadata.xml'
private_key: get_test_file('test.pem')
certificate: get_test_file('test.crt')
assert_endpoint: 'https://sp.example.com/assert'
idp_options =
sso_login_url: 'https://idp.example.com/login'
sso_logout_url: 'https://idp.example.com/logout'
certificates: 'other_service_cert'
sp = new saml2.ServiceProvider sp_options
idp = new saml2.IdentityProvider idp_options
sp.create_login_request_url idp, {assert_endpoint: 'https://sp.example.com/assert', relay_state: 'Some Relay State!'}, (err, login_url, id) ->
assert not err?, "Error creating login URL: #{err}"
parsed_url = url.parse login_url, true
assert.equal parsed_url.query?.RelayState, 'Some Relay State!'
done()
it 'can specify a nameid format in cre