You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1032 lines
37 KiB
1032 lines
37 KiB
/* eslint-env mocha */
|
|
import 'babel-polyfill';
|
|
|
|
import chai from 'chai';
|
|
|
|
import '../../lib/tests/server.mocks.js';
|
|
import { AuthorizeRequest } from '../server/lib/generators/AuthorizeRequest';
|
|
import { LogoutRequest } from '../server/lib/generators/LogoutRequest';
|
|
import { LogoutResponse } from '../server/lib/generators/LogoutResponse';
|
|
import { ServiceProviderMetadata } from '../server/lib/generators/ServiceProviderMetadata';
|
|
import { LogoutRequestParser } from '../server/lib/parsers/LogoutRequest';
|
|
import { LogoutResponseParser } from '../server/lib/parsers/LogoutResponse';
|
|
import { ResponseParser } from '../server/lib/parsers/Response';
|
|
import { SAMLUtils } from '../server/lib/Utils';
|
|
import {
|
|
serviceProviderOptions,
|
|
simpleMetadata,
|
|
metadataWithCertificate,
|
|
simpleLogoutRequest,
|
|
invalidXml,
|
|
randomXml,
|
|
invalidLogoutRequest,
|
|
simpleLogoutResponse,
|
|
invalidLogoutResponse,
|
|
simpleSamlResponse,
|
|
samlResponse,
|
|
duplicatedSamlResponse,
|
|
samlResponseMissingStatus,
|
|
samlResponseFailedStatus,
|
|
samlResponseMultipleAssertions,
|
|
samlResponseMissingAssertion,
|
|
samlResponseMultipleIssuers,
|
|
samlResponseValidSignatures,
|
|
samlResponseValidAssertionSignature,
|
|
encryptedResponse,
|
|
profile,
|
|
certificate,
|
|
privateKeyCert,
|
|
privateKey,
|
|
} from './data';
|
|
import '../../../definition/xml-encryption';
|
|
|
|
const { expect } = chai;
|
|
|
|
describe('SAML', () => {
|
|
describe('[AuthorizeRequest]', () => {
|
|
describe('AuthorizeRequest.generate', () => {
|
|
it('should use the custom templates to generate the request', () => {
|
|
const authorizeRequest = AuthorizeRequest.generate(serviceProviderOptions);
|
|
expect(authorizeRequest.request).to.be.equal('<authRequest><NameID IdentifierFormat="email"/> <authnContext Comparison="Whatever">Password</authnContext> </authRequest>');
|
|
});
|
|
|
|
it('should include the unique ID on the request', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
authRequestTemplate: '__newId__',
|
|
};
|
|
|
|
const authorizeRequest = AuthorizeRequest.generate(customOptions);
|
|
expect(authorizeRequest.request).to.be.equal(authorizeRequest.id);
|
|
});
|
|
|
|
it('should include the custom options on the request', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
authRequestTemplate: '__callbackUrl__ __entryPoint__ __issuer__',
|
|
};
|
|
|
|
const authorizeRequest = AuthorizeRequest.generate(customOptions);
|
|
expect(authorizeRequest.request).to.be.equal('[callback-url] [entry-point] [issuer]');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('[LogoutRequest]', () => {
|
|
describe('LogoutRequest.generate', () => {
|
|
it('should use the custom template to generate the request', () => {
|
|
const logoutRequest = LogoutRequest.generate(serviceProviderOptions, 'NameID', 'sessionIndex');
|
|
expect(logoutRequest.request).to.be.equal('[logout-request-template]');
|
|
});
|
|
|
|
it('should include the unique ID on the request', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
logoutRequestTemplate: '__newId__',
|
|
};
|
|
|
|
const logoutRequest = LogoutRequest.generate(customOptions, 'NameID', 'sessionIndex');
|
|
expect(logoutRequest.request).to.be.equal(logoutRequest.id);
|
|
});
|
|
|
|
it('should include the custom options on the request', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
logoutRequestTemplate: '__idpSLORedirectURL__ __issuer__ __identifierFormat__ __nameID__ __sessionIndex__',
|
|
};
|
|
|
|
const logoutRequest = LogoutRequest.generate(customOptions, 'NameID', 'sessionIndex');
|
|
expect(logoutRequest.request).to.be.equal('[idpSLORedirectURL] [issuer] email NameID sessionIndex');
|
|
});
|
|
});
|
|
|
|
describe('LogoutRequest.validate', () => {
|
|
it('should extract the idpSession and nameID from the request', () => {
|
|
const parser = new LogoutRequestParser(serviceProviderOptions);
|
|
|
|
parser.validate(simpleLogoutRequest, (err, data) => {
|
|
expect(err).to.be.null;
|
|
expect(data).to.be.an('object');
|
|
expect(data).to.have.property('idpSession');
|
|
expect(data).to.have.property('nameID');
|
|
// @ts-ignore -- chai already ensured the object exists
|
|
expect(data.idpSession).to.be.equal('_d6ad0e25459aaddd0433a81e159aa79e55dc52c280');
|
|
// @ts-ignore -- chai already ensured the object exists
|
|
expect(data.nameID).to.be.equal('_ab7e1d9a603473e92148d569d50176bafa60bcb2e9');
|
|
});
|
|
});
|
|
|
|
it('should fail to parse an invalid xml', () => {
|
|
const parser = new LogoutRequestParser(serviceProviderOptions);
|
|
parser.validate(invalidXml, (err, data) => {
|
|
expect(err).to.exist;
|
|
expect(data).to.not.exist;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a xml without any LogoutRequest tag', () => {
|
|
const parser = new LogoutRequestParser(serviceProviderOptions);
|
|
parser.validate(randomXml, (err, data) => {
|
|
expect(err).to.be.equal('No Request Found');
|
|
expect(data).to.not.exist;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a request with no NameId', () => {
|
|
const parser = new LogoutRequestParser(serviceProviderOptions);
|
|
|
|
parser.validate(invalidLogoutRequest, (err, data) => {
|
|
expect(err).to.be.an('error').that.has.property('message').equal('SAML Logout Request: No NameID node found');
|
|
expect(data).to.not.exist;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('[LogoutResponse]', () => {
|
|
describe('LogoutResponse.generate', () => {
|
|
it('should use the custom template to generate the response', () => {
|
|
const logoutResponse = LogoutResponse.generate(serviceProviderOptions, 'NameID', 'sessionIndex', 'inResponseToId');
|
|
expect(logoutResponse.response).to.be.equal('[logout-response-template]');
|
|
});
|
|
|
|
it('should include the unique ID on the response', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
logoutResponseTemplate: '__newId__',
|
|
};
|
|
|
|
const logoutResponse = LogoutResponse.generate(customOptions, 'NameID', 'sessionIndex', 'inResponseToId');
|
|
expect(logoutResponse.response).to.be.equal(logoutResponse.id);
|
|
});
|
|
|
|
it('should include the custom options on the response', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
logoutResponseTemplate: '__idpSLORedirectURL__ __issuer__',
|
|
};
|
|
|
|
const logoutResponse = LogoutResponse.generate(customOptions, 'NameID', 'sessionIndex', 'inResponseToId');
|
|
expect(logoutResponse.response).to.be.equal('[idpSLORedirectURL] [issuer]');
|
|
});
|
|
});
|
|
|
|
describe('LogoutResponse.validate', () => {
|
|
it('should extract the inResponseTo from the response', () => {
|
|
const logoutResponse = simpleLogoutResponse
|
|
.replace('[STATUSCODE]', 'urn:oasis:names:tc:SAML:2.0:status:Success');
|
|
const parser = new LogoutResponseParser(serviceProviderOptions);
|
|
|
|
parser.validate(logoutResponse, (err, inResponseTo) => {
|
|
expect(err).to.be.null;
|
|
expect(inResponseTo).to.be.equal('_id-6530db3fcd23dc42a31c');
|
|
});
|
|
});
|
|
|
|
it('should reject a response with a non-success StatusCode', () => {
|
|
const logoutResponse = simpleLogoutResponse
|
|
.replace('[STATUSCODE]', 'Anything');
|
|
const parser = new LogoutResponseParser(serviceProviderOptions);
|
|
|
|
parser.validate(logoutResponse, (err, inResponseTo) => {
|
|
expect(err).to.be.equal('Error. Logout not confirmed by IDP');
|
|
expect(inResponseTo).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse an invalid xml', () => {
|
|
const parser = new LogoutResponseParser(serviceProviderOptions);
|
|
parser.validate(invalidXml, (err, inResponseTo) => {
|
|
expect(err).to.exist;
|
|
expect(inResponseTo).to.not.exist;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a xml without any LogoutResponse tag', () => {
|
|
const parser = new LogoutResponseParser(serviceProviderOptions);
|
|
parser.validate(randomXml, (err, inResponseTo) => {
|
|
expect(err).to.be.equal('No Response Found');
|
|
expect(inResponseTo).to.not.exist;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a xml without an inResponseTo attribute', () => {
|
|
const instant = new Date().toISOString();
|
|
const logoutResponse = simpleLogoutResponse
|
|
.replace('[INSTANT]', instant)
|
|
.replace('[STATUSCODE]', 'urn:oasis:names:tc:SAML:2.0:status:Success')
|
|
.replace('InResponseTo=', 'SomethingElse=');
|
|
|
|
const parser = new LogoutResponseParser(serviceProviderOptions);
|
|
parser.validate(logoutResponse, (err, inResponseTo) => {
|
|
expect(err).to.be.equal('Unexpected Response from IDP');
|
|
expect(inResponseTo).to.not.exist;
|
|
});
|
|
});
|
|
|
|
it('should reject a response with no status tag', () => {
|
|
const parser = new LogoutResponseParser(serviceProviderOptions);
|
|
|
|
parser.validate(invalidLogoutResponse, (err, inResponseTo) => {
|
|
expect(err).to.be.equal('Error. Logout not confirmed by IDP');
|
|
expect(inResponseTo).to.be.null;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('[Metadata]', () => {
|
|
describe('[Metadata.generate]', () => {
|
|
it('should generate a simple metadata file when no certificate info is included', () => {
|
|
const metadata = ServiceProviderMetadata.generate(serviceProviderOptions);
|
|
expect(metadata).to.be.equal(simpleMetadata);
|
|
});
|
|
|
|
it('should include additional information when a certificate is provided', () => {
|
|
const customOptions = {
|
|
...serviceProviderOptions,
|
|
privateCert: '[CERTIFICATE_CONTENT]',
|
|
privateKey: '[PRIVATE_KEY]',
|
|
};
|
|
|
|
const metadata = ServiceProviderMetadata.generate(customOptions);
|
|
expect(metadata).to.be.equal(metadataWithCertificate);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('[Response]', () => {
|
|
describe('[Response.validate]', () => {
|
|
it('should extract a profile from the response', () => {
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = simpleSamlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(response, (err, profile, loggedOut) => {
|
|
expect(err).to.be.null;
|
|
expect(profile).to.be.an('object');
|
|
expect(profile).to.have.property('inResponseToId').equal('[INRESPONSETO]');
|
|
expect(profile).to.have.property('issuer').equal('[ISSUER]');
|
|
expect(profile).to.have.property('nameID').equal('[NAMEID]');
|
|
expect(profile).to.have.property('sessionIndex').equal('[SESSIONINDEX]');
|
|
expect(profile).to.have.property('uid').equal('1');
|
|
expect(profile).to.have.property('eduPersonAffiliation').equal('group1');
|
|
expect(profile).to.have.property('email').equal('user1@example.com');
|
|
expect(profile).to.have.property('channels').that.is.an('array').that.is.deep.equal(['channel1', 'pets', 'random']);
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should respect NotOnOrAfter conditions', () => {
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const response = samlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', new Date().toISOString());
|
|
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(response, (err, profile, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(profile).to.be.null;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should respect NotBefore conditions', () => {
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() + 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = samlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(response, (err, profile, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(profile).to.be.null;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
|
|
it('should fail to parse an invalid xml', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(invalidXml, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Unknown SAML response message');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a xml without any Response tag', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(randomXml, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Unknown SAML response message');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject a xml with multiple responses', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(duplicatedSamlResponse, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Too many SAML responses');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a reponse with no Status tag', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(samlResponseMissingStatus, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Missing StatusCode');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should fail to parse a reponse with a failed status', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(samlResponseFailedStatus, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Status is: Failed');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject a response with multiple assertions', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(samlResponseMultipleAssertions, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Too many SAML assertions');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject a response with no assertions', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(samlResponseMissingAssertion, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Missing SAML assertion');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject a document without signatures if the setting requires at least one', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Either',
|
|
cert: 'invalidCert',
|
|
};
|
|
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = simpleSamlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(response, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('No valid SAML Signature found');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject a document with multiple issuers', () => {
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(samlResponseMultipleIssuers, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Too many Issuers');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should decrypt an encrypted response', () => {
|
|
const options = {
|
|
...serviceProviderOptions,
|
|
privateCert: privateKeyCert,
|
|
privateKey,
|
|
};
|
|
|
|
const parser = new ResponseParser(options);
|
|
parser.validate(encryptedResponse, (err, profile, loggedOut) => {
|
|
// No way to change the assertion conditions on an encrypted response ¯\_(ツ)_/¯
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should validate signatures on an encrypted response', () => {
|
|
const options = {
|
|
...serviceProviderOptions,
|
|
privateCert: privateKeyCert,
|
|
signatureValidationType: 'All',
|
|
privateKey,
|
|
};
|
|
|
|
const parser = new ResponseParser(options);
|
|
parser.validate(encryptedResponse, (err, profile, loggedOut) => {
|
|
// No way to change the assertion conditions on an encrypted response ¯\_(ツ)_/¯
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('[Validate Signatures]', () => {
|
|
it('should reject an unsigned assertion if the setting says so', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Assertion',
|
|
cert: 'invalidCert',
|
|
};
|
|
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = simpleSamlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(response, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Invalid Assertion signature');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject an unsigned response if the setting says so', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Response',
|
|
cert: 'invalidCert',
|
|
};
|
|
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = simpleSamlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(response, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Invalid Signature');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should reject an assertion signed with an invalid signature', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Assertion',
|
|
cert: certificate,
|
|
};
|
|
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = samlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(response, (err, data, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Invalid Assertion signature');
|
|
expect(data).to.not.exist;
|
|
expect(loggedOut).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should accept an assertion with a valid signature', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Assertion',
|
|
cert: certificate,
|
|
};
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(samlResponseValidAssertionSignature, (err, profile, loggedOut) => {
|
|
// To have a valid signature, we can't change the assertion conditions ¯\_(ツ)_/¯
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should accept a document with a valid response signature', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Response',
|
|
cert: certificate,
|
|
};
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(samlResponseValidSignatures, (err, profile, loggedOut) => {
|
|
// To have a valid signature, we can't change the assertion conditions ¯\_(ツ)_/¯
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should reject a document with a valid signature of the wrong type', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Response',
|
|
cert: certificate,
|
|
};
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(samlResponseValidAssertionSignature, (err, profile, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Invalid Signature');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should accept a document with both valid signatures', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'All',
|
|
cert: certificate,
|
|
};
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(samlResponseValidSignatures, (err, profile, loggedOut) => {
|
|
// To have a valid signature, we can't change the assertion conditions ¯\_(ツ)_/¯
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should reject a document with a single signature when both are expected', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'All',
|
|
cert: certificate,
|
|
};
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(samlResponseValidAssertionSignature, (err, profile, loggedOut) => {
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('Invalid Signature');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should accept a document with either valid signature', () => {
|
|
const providerOptions = {
|
|
...serviceProviderOptions,
|
|
signatureValidationType: 'Either',
|
|
cert: certificate,
|
|
};
|
|
|
|
const parser = new ResponseParser(providerOptions);
|
|
parser.validate(samlResponseValidAssertionSignature, (err, profile, loggedOut) => {
|
|
// To have a valid signature, we can't change the assertion conditions ¯\_(ツ)_/¯
|
|
expect(err).to.be.an('error').that.has.property('message').that.is.equal('NotBefore / NotOnOrAfter assertion failed');
|
|
expect(loggedOut).to.be.false;
|
|
expect(profile).to.be.null;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('[Login]', () => {
|
|
describe('UserMapping', () => {
|
|
it('should collect all appropriate data from the profile, respecting the fieldMap', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: 'anotherUsername',
|
|
email: 'singleEmail',
|
|
name: 'anotherName',
|
|
customField1: 'customField1',
|
|
customField2: 'customField2',
|
|
customField3: 'customField3',
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
globalSettings.roleAttributeName = 'roles';
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
SAMLUtils.relayState = '[RelayState]';
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('samlLogin').that.is.an('object');
|
|
expect(userObject).to.have.nested.property('samlLogin.provider').that.is.equal('[RelayState]');
|
|
expect(userObject).to.have.nested.property('samlLogin.idp').that.is.equal('[IssuerName]');
|
|
expect(userObject).to.have.nested.property('samlLogin.idpSession').that.is.equal('[SessionIndex]');
|
|
expect(userObject).to.have.nested.property('samlLogin.nameID').that.is.equal('[nameID]');
|
|
expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('testing@server.com');
|
|
expect(userObject).to.have.property('fullName').that.is.equal('[AnotherName]');
|
|
expect(userObject).to.have.property('username').that.is.equal('[AnotherUserName]');
|
|
expect(userObject).to.have.property('roles').that.is.an('array').with.members(['user', 'ruler', 'admin', 'king', 'president', 'governor', 'mayor']);
|
|
expect(userObject).to.have.property('channels').that.is.an('array').with.members(['pets', 'pics', 'funny', 'random', 'babies']);
|
|
|
|
const map = new Map();
|
|
map.set('customField1', 'value1');
|
|
map.set('customField2', 'value2');
|
|
map.set('customField3', 'value3');
|
|
|
|
expect(userObject).to.have.property('customFields').that.is.a('Map').and.is.deep.equal(map);
|
|
});
|
|
|
|
it('should join array values if username receives an array of values', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const multipleUsernames = {
|
|
...profile,
|
|
anotherUsername: ['user1', 'user2'],
|
|
};
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
const userObject = SAMLUtils.mapProfileToUserObject(multipleUsernames);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('samlLogin').that.is.an('object');
|
|
expect(userObject).to.have.property('username').that.is.equal('user1 user2');
|
|
});
|
|
|
|
// Channels support both a comma separated single value and an array of values
|
|
it('should support `channels` attribute with multiple values', () => {
|
|
const channelsProfile = {
|
|
...profile,
|
|
channels: [
|
|
'pets',
|
|
'pics',
|
|
'funny',
|
|
],
|
|
};
|
|
|
|
const userObject = SAMLUtils.mapProfileToUserObject(channelsProfile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('channels').that.is.an('array').with.members(['pets', 'pics', 'funny']);
|
|
});
|
|
|
|
it('should reject an userDataFieldMap without an email field', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
globalSettings.userDataFieldMap = JSON.stringify({});
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
expect(() => {
|
|
SAMLUtils.mapProfileToUserObject(profile);
|
|
}).to.throw('SAML Profile did not contain an email address');
|
|
});
|
|
|
|
it('should fail to map a profile that is missing the email field', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
const fieldMap = {
|
|
inexistentField: 'email',
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
expect(() => {
|
|
SAMLUtils.mapProfileToUserObject(profile);
|
|
}).to.throw('SAML Profile did not contain an email address');
|
|
});
|
|
|
|
it('should load data from the default fields when the field map is lacking', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
email: 'singleEmail',
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('fullName').that.is.equal('[DisplayName]');
|
|
expect(userObject).to.have.property('username').that.is.equal('[username]');
|
|
});
|
|
|
|
it('should load multiple roles from the roleAttributeName when it has multiple values', () => {
|
|
const multipleRoles = {
|
|
...profile,
|
|
roles: ['role1', 'role2'],
|
|
};
|
|
|
|
const userObject = SAMLUtils.mapProfileToUserObject(multipleRoles);
|
|
|
|
expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['role1', 'role2']);
|
|
});
|
|
|
|
it('should assign the default role when the roleAttributeName is missing', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
globalSettings.roleAttributeName = '';
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['user']);
|
|
});
|
|
|
|
it('should assign the default role when the value of the role attribute is missing', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
globalSettings.roleAttributeName = 'inexistentField';
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['user']);
|
|
});
|
|
|
|
it('should run custom regexes when one is used', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: {
|
|
fieldName: 'singleEmail',
|
|
regex: '(.*)@.+$',
|
|
},
|
|
email: 'singleEmail',
|
|
name: 'anotherName',
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
SAMLUtils.relayState = '[RelayState]';
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('username').that.is.equal('testing');
|
|
});
|
|
|
|
it('should run custom templates when one is used', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: {
|
|
fieldName: 'anotherName',
|
|
template: 'user-__anotherName__',
|
|
},
|
|
email: 'singleEmail',
|
|
name: {
|
|
fieldNames: [
|
|
'anotherName',
|
|
'displayName',
|
|
],
|
|
template: '__displayName__ (__anotherName__)',
|
|
},
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
SAMLUtils.relayState = '[RelayState]';
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('username').that.is.equal('user-[AnotherName]');
|
|
expect(userObject).to.have.property('fullName').that.is.equal('[DisplayName] ([AnotherName])');
|
|
});
|
|
|
|
it('should combine regexes and templates when both are used', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: {
|
|
fieldName: 'anotherName',
|
|
template: 'user-__anotherName__45@7',
|
|
regex: 'user-(.*)@',
|
|
},
|
|
email: 'singleEmail',
|
|
name: {
|
|
fieldNames: [
|
|
'anotherName',
|
|
'displayName',
|
|
],
|
|
regex: '\\[(.*)\\]',
|
|
template: '__displayName__ (__regex__)',
|
|
},
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
SAMLUtils.relayState = '[RelayState]';
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
// should run the template first, then the regex
|
|
expect(userObject).to.have.property('username').that.is.equal('[AnotherName]45');
|
|
// for this one, should run the regex first, then the template
|
|
expect(userObject).to.have.property('fullName').that.is.equal('[DisplayName] (AnotherName)');
|
|
});
|
|
|
|
it('should support individual array values on templates', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const multipleUsernames = {
|
|
...profile,
|
|
anotherUsername: ['1', '2'],
|
|
};
|
|
|
|
const fieldMap = {
|
|
username: {
|
|
fieldName: 'anotherUsername',
|
|
template: 'user-__anotherUsername[-1]__',
|
|
},
|
|
email: {
|
|
fieldName: 'anotherUsername',
|
|
template: 'user-__anotherUsername[0]__',
|
|
},
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
const userObject = SAMLUtils.mapProfileToUserObject(multipleUsernames);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('username').that.is.equal('user-2');
|
|
expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('user-1');
|
|
});
|
|
|
|
|
|
it('should collect the values of every attribute on the field map', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: 'anotherUsername',
|
|
email: 'singleEmail',
|
|
name: 'anotherName',
|
|
others: {
|
|
fieldNames: [
|
|
'issuer',
|
|
'sessionIndex',
|
|
'nameID',
|
|
'displayName',
|
|
'username',
|
|
'roles',
|
|
'otherRoles',
|
|
'language',
|
|
'channels',
|
|
'customField1',
|
|
],
|
|
},
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('attributeList').that.is.a('Map').that.have.keys([
|
|
'anotherUsername',
|
|
'singleEmail',
|
|
'anotherName',
|
|
'issuer',
|
|
'sessionIndex',
|
|
'nameID',
|
|
'displayName',
|
|
'username',
|
|
'roles',
|
|
'otherRoles',
|
|
'language',
|
|
'channels',
|
|
'customField1',
|
|
]);
|
|
|
|
// Workaround because chai doesn't handle Maps very well
|
|
for (const [key, value] of userObject.attributeList) {
|
|
// @ts-ignore
|
|
expect(value).to.be.equal(profile[key]);
|
|
}
|
|
});
|
|
|
|
it('should use the immutable property as default identifier', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
globalSettings.immutableProperty = 'EMail';
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('identifier').that.has.property('type').that.is.equal('email');
|
|
|
|
globalSettings.immutableProperty = 'Username';
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
const newUserObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
expect(newUserObject).to.be.an('object');
|
|
expect(newUserObject).to.have.property('identifier').that.has.property('type').that.is.equal('username');
|
|
});
|
|
|
|
it('should collect the identifier from the fieldset', () => {
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: 'anotherUsername',
|
|
email: 'singleEmail',
|
|
name: 'anotherName',
|
|
__identifier__: 'customField3',
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('identifier').that.has.property('type').that.is.equal('custom');
|
|
expect(userObject).to.have.property('identifier').that.has.property('attribute').that.is.equal('customField3');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Response Mapping', () => {
|
|
it('should extract a mapped user from the response', () => {
|
|
const notBefore = new Date();
|
|
notBefore.setMinutes(notBefore.getMinutes() - 3);
|
|
|
|
const notOnOrAfter = new Date();
|
|
notOnOrAfter.setMinutes(notOnOrAfter.getMinutes() + 3);
|
|
|
|
const response = simpleSamlResponse
|
|
.replace('[NOTBEFORE]', notBefore.toISOString())
|
|
.replace('[NOTONORAFTER]', notOnOrAfter.toISOString());
|
|
|
|
const parser = new ResponseParser(serviceProviderOptions);
|
|
parser.validate(response, (err, profile, loggedOut) => {
|
|
expect(profile).to.be.an('object');
|
|
expect(err).to.be.null;
|
|
expect(loggedOut).to.be.false;
|
|
|
|
const { globalSettings } = SAMLUtils;
|
|
|
|
const fieldMap = {
|
|
username: {
|
|
fieldName: 'uid',
|
|
template: 'user-__uid__',
|
|
},
|
|
email: 'email',
|
|
epa: 'eduPersonAffiliation',
|
|
};
|
|
|
|
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
|
|
globalSettings.roleAttributeName = 'roles';
|
|
|
|
SAMLUtils.updateGlobalSettings(globalSettings);
|
|
SAMLUtils.relayState = '[RelayState]';
|
|
|
|
// @ts-ignore
|
|
const userObject = SAMLUtils.mapProfileToUserObject(profile);
|
|
|
|
expect(userObject).to.be.an('object');
|
|
expect(userObject).to.have.property('samlLogin').that.is.an('object');
|
|
expect(userObject).to.have.nested.property('samlLogin.provider').that.is.equal('[RelayState]');
|
|
expect(userObject).to.have.nested.property('samlLogin.idp').that.is.equal('[ISSUER]');
|
|
expect(userObject).to.have.nested.property('samlLogin.idpSession').that.is.equal('[SESSIONINDEX]');
|
|
expect(userObject).to.have.nested.property('samlLogin.nameID').that.is.equal('[NAMEID]');
|
|
expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('user1@example.com');
|
|
expect(userObject).to.have.property('username').that.is.equal('user-1');
|
|
|
|
const map = new Map();
|
|
map.set('epa', 'group1');
|
|
|
|
expect(userObject).to.have.property('customFields').that.is.a('Map').and.is.deep.equal(map);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|