UNPKG

saml2-js

Version:
1,103 lines (941 loc) 59.8 kB
_ = 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