Skip to content

Allow for use of privateKey instead of privateCert #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ type Profile = {
* `issuer`: issuer string to supply to identity provider
* `audience`: expected saml response Audience (if not provided, Audience won't be verified)
* `cert`: the IDP's public signing certificate used to validate the signatures of the incoming SAML Responses, see [Security and signatures](#security-and-signatures)
* `privateCert`: see [Security and signatures](#security-and-signatures)
* `privateKey`: see [Security and signatures](#security-and-signatures). Old name of `privateCert` is accepted alternative.
* `decryptionPvk`: optional private key that will be used to attempt to decrypt any encrypted assertions that are received
* `signatureAlgorithm`: optionally set the signature algorithm for signing requests, valid values are 'sha1' (default), 'sha256', or 'sha512'
* `digestAlgorithm`: optionally set the digest algorithm used to provide a digest for the signed data object, valid values are 'sha1' (default), 'sha256', or 'sha512'
Expand Down Expand Up @@ -238,9 +238,9 @@ To select hashing algorithm, use:
...
```

To sign them you need to provide a private key in the PEM format via the `privateCert` configuration key.
To sign them you need to provide a private key in the PEM format via the `privateKey` configuration key.

Formats supported for `privateCert` field are,
Formats supported for `privateKey` field are,

1. Well formatted PEM:

Expand Down
7 changes: 4 additions & 3 deletions src/passport-saml/saml-post-signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const issuerXPath = '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:n
const defaultTransforms = [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ];

interface SignSamlPostOptions {
privateCert: string;
privateCert?: string;
privateKey?: string;
signatureAlgorithm?: string;
xmlSignatureTransforms?: string[];
digestAlgorithm: string;
Expand All @@ -15,15 +16,15 @@ interface SignSamlPostOptions {
export function signSamlPost(samlMessage: string, xpath: string, options: SignSamlPostOptions) {
if (!samlMessage) throw new Error('samlMessage is required');
if (!xpath) throw new Error('xpath is required');
if (!options || !options.privateCert) throw new Error('options.privateCert is required');
if (!options || (!options.privateCert && !options.privateKey)) throw new Error('options.privateCert or options.privateKey is required');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of issuing a deprecation warning if someone is using privateCert? This way, we can remove this extra code with a future semver-major release.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cjbarth That's OK if you want to add the warning.


const transforms = options.xmlSignatureTransforms || defaultTransforms;
const sig = new SignedXml();
if (options.signatureAlgorithm) {
sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm);
}
sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm));
sig.signingKey = options.privateCert;
sig.signingKey = options.privateCert || options.privateKey;
sig.computeSignature(samlMessage, { location: { reference: xpath + issuerXPath, action: 'after' }});
return sig.getSignedXml();
}
Expand Down
18 changes: 10 additions & 8 deletions src/passport-saml/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ interface SAMLOptions {
signatureAlgorithm: string;
path: string;
privateCert: string;
privateKey: string;
logoutUrl: string;
entryPoint: string;
skipRequestCompression: boolean;
Expand Down Expand Up @@ -232,7 +233,7 @@ class SAML {
samlMessageToSign.SigAlg = samlMessage.SigAlg;
}
signer.update(querystring.stringify(samlMessageToSign));
samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateCert), 'base64');
samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateCert) || this.options.privateKey, 'base64');
}

generateAuthorizeRequest = function (req, isPassive, isHttpPostBinding, callback) {
Expand Down Expand Up @@ -357,7 +358,8 @@ class SAML {
}

let stringRequest = xmlbuilder.create(request).end();
if (isHttpPostBinding && this.options.privateCert) {
const privateKey = this.options.privateCert || this.options.privateKey;
if (isHttpPostBinding && privateKey) {
stringRequest = signAuthnRequestPost(stringRequest, this.options);
}
callback(null, stringRequest);
Expand Down Expand Up @@ -464,8 +466,8 @@ class SAML {
Object.keys(additionalParameters).forEach(k => {
samlMessage[k] = additionalParameters[k];
});

if (this.options.privateCert) {
const privateKey = this.options.privateCert || this.options.privateKey;
if (privateKey) {
try {
if (!this.options.entryPoint) {
throw new Error('"entryPoint" config parameter is required for signed messages');
Expand Down Expand Up @@ -1303,17 +1305,17 @@ class SAML {
"Missing decryptionCert while generating metadata for decrypting service provider");
}
}

if(this.options.privateCert){
const privateKey = this.options.privateCert || this.options.privateKey;
if(privateKey){
if(!signingCert){
throw new Error(
"Missing signingCert while generating metadata for signing service provider messages");
}
}

if(this.options.decryptionPvk || this.options.privateCert){
if(this.options.decryptionPvk || privateKey){
metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor=[];
if (this.options.privateCert) {
if (privateKey) {

signingCert = signingCert.replace( /-+BEGIN CERTIFICATE-+\r?\n?/, '' );
signingCert = signingCert.replace( /-+END CERTIFICATE-+\r?\n?/, '' );
Expand Down
38 changes: 37 additions & 1 deletion test/saml-post-signing-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,48 @@ describe('SAML POST Signing', function () {
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

it('should sign a simple saml request when using a privateKey', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></SAMLRequest>';
var result = signSamlPost(xml, '/SAMLRequest', { privateKey: signingKey });
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

it('should place the Signature element after the Issuer element', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer><SomeOtherElement /></SAMLRequest>';
var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey });
result.should.match(/<\/saml2:Issuer><Signature/);
result.should.match(/<\/Signature><SomeOtherElement/);
});

it('should place the Signature element after the Issuer element when using a privateKey', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer><SomeOtherElement /></SAMLRequest>';
var result = signSamlPost(xml, '/SAMLRequest', { privateKey: signingKey });
result.should.match(/<\/saml2:Issuer><Signature/);
result.should.match(/<\/Signature><SomeOtherElement/);
});

it('should sign and digest with SHA256 when specified', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></SAMLRequest>';
var options = {
signatureAlgorithm: 'sha256',
digestAlgorithm: 'sha256',
privateCert: signingKey
}
};
var result = signSamlPost(xml, '/SAMLRequest', options);
result.should.match(/<SignatureMethod Algorithm="http:\/\/www.w3.org\/2001\/04\/xmldsig-more#rsa-sha256"/);
result.should.match(/<Transform Algorithm="http:\/\/www.w3.org\/2001\/10\/xml-exc-c14n#"\/>/);
result.should.match(/<Transform Algorithm="http:\/\/www.w3.org\/2000\/09\/xmldsig#enveloped-signature"\/>/);
result.should.match(/<DigestMethod Algorithm="http:\/\/www.w3.org\/2001\/04\/xmlenc#sha256"\/>/);
});

it('should sign and digest with SHA256 when specified and using privateKey', function () {
var xml = '<SAMLRequest><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></SAMLRequest>';
var options = {
signatureAlgorithm: 'sha256',
digestAlgorithm: 'sha256',
privateKey: signingKey
};
var result = signSamlPost(xml, '/SAMLRequest', options);
result.should.match(/<SignatureMethod Algorithm="http:\/\/www.w3.org\/2001\/04\/xmldsig-more#rsa-sha256"/);
result.should.match(/<Transform Algorithm="http:\/\/www.w3.org\/2001\/10\/xml-exc-c14n#"\/>/);
Expand All @@ -41,4 +69,12 @@ describe('SAML POST Signing', function () {
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

it('should sign an AuthnRequest when using a privateKey', function () {
var xml = '<AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:protocol"><saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://example.com</saml2:Issuer></AuthnRequest>';
var result = signAuthnRequestPost(xml, { privateKey: signingKey });
result.should.match(/<DigestValue>[A-Za-z0-9\/\+\=]+<\/DigestValue>/);
result.should.match(/<SignatureValue>[A-Za-z0-9\/\+\=]+<\/SignatureValue>/);
});

});
137 changes: 136 additions & 1 deletion test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,22 @@ describe( 'passport-saml /', function() {
testMetadata( samlConfig, expectedMetadata, signingCert );
});

it( 'config with protocol, path, host, decryptionPvk and privateKey should pass', function() {
var samlConfig = {
issuer: 'http://example.serviceprovider.com',
protocol: 'http://',
host: 'example.serviceprovider.com',
path: '/saml/callback',
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
decryptionPvk: fs.readFileSync(__dirname + '/static/testshib encryption pvk.pem'),
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key')
};
var expectedMetadata = fs.readFileSync(__dirname + '/static/expectedMetadataWithBothKeys.xml', 'utf-8');
var signingCert = fs.readFileSync(__dirname + '/static/acme_tools_com.cert').toString();

testMetadata( samlConfig, expectedMetadata, signingCert );
});

});

it('generateServiceProviderMetadata contains logout callback url', function () {
Expand Down Expand Up @@ -1895,7 +1911,33 @@ describe( 'passport-saml /', function() {
}
});
});

it( 'acme_tools request signed with sha256 when using privateKey', function( done ) {
var samlConfig = {
entryPoint: 'https://adfs.acme_tools.com/adfs/ls/',
issuer: 'acme_tools_com',
callbackUrl: 'https://relyingparty/adfs/postResponse',
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'),
authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
identifierFormat: null,
signatureAlgorithm: 'sha256',
additionalParams: {
customQueryStringParam: 'CustomQueryStringParamValue'
}
};
var samlObj = new SAML( samlConfig );
samlObj.generateUniqueID = function () { return '12345678901234567890' };
samlObj.getAuthorizeUrl({}, {}, function(err, url) {
try {
var qry = require('querystring').parse(require('url').parse(url).query);
qry.SigAlg.should.match('http://www.w3.org/2001/04/xmldsig-more#rsa-sha256');
qry.Signature.should.match('hel9NaoLU0brY/VhrQsY+lTtuAbTsT/ul6nZ/eVlSMXQRaKn5LTbKadzxmPghX7s4xoHwdah+yZHK/0u4StYSj4b5MKcqbsJapVr2R7H90z8YfGfR2C/G0Gng721YV9Da6VBzKg8Was91zQotgsMpZ9pGX1kPKi6cgFwPwM4NEFugn8AYgXEriNvO5+Q23K/MdBT2bgwRTj2FQCWTuQcgwbyWHXoquHztZ0lbh8UhY5BfQRv7c6D9XPkQEMMQFQeME4PIEg3JnynwFZk5wwhkphMd5nXxau+zt7Nfp4fRm0G8WYnxV1etBnWimwSglZVaSHFYeQBRsC2wvKSiVS8JA==');
qry.customQueryStringParam.should.match('CustomQueryStringParamValue');
done();
} catch (err2) {
done(err2);
}
});
});
it( 'acme_tools request not signed if missing entry point', function( done ) {
var samlConfig = {
entryPoint: '',
Expand All @@ -1922,7 +1964,32 @@ describe( 'passport-saml /', function() {
}
});
});
it( 'acme_tools request not signed if missing entry point when using privateKey', function( done ) {
var samlConfig = {
entryPoint: '',
issuer: 'acme_tools_com',
callbackUrl: 'https://relyingparty/adfs/postResponse',
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'),
authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
signatureAlgorithm: 'sha256',
additionalParams: {
customQueryStringParam: 'CustomQueryStringParamValue'
}
};
var samlObj = new SAML( samlConfig );
samlObj.generateUniqueID = function () { return '12345678901234567890' };

var request = '<?xml version=\\"1.0\\"?><samlp:AuthnRequest xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" ID=\\"_ea40a8ab177df048d645\\" Version=\\"2.0\\" IssueInstant=\\"2017-08-22T19:30:01.363Z\\" ProtocolBinding=\\"urn:oasis:names$tc:SAML:2.0:bindings:HTTP-POST\\" AssertionConsumerServiceURL=\\"https://example.com/login/callback\\" Destination=\\"https://www.example.com\\"><saml:Issuer xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">onelogin_saml</saml:Issuer><s$mlp:NameIDPolicy xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" Format=\\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\\" AllowCreate=\\"true\\"/><samlp:RequestedAuthnContext xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protoc$l\\" Comparison=\\"exact\\"><saml:AuthnContextClassRef xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp$AuthnRequest>';
samlObj.requestToUrl(request, null, 'authorize', {}, function(err) {
try {
should.exist(err);
err.message.should.eql('"entryPoint" config parameter is required for signed messages');
done();
} catch (err2) {
done(err2);
}
});
});
it( 'acme_tools request signed with sha1', function( done ) {
var samlConfig = {
entryPoint: 'https://adfs.acme_tools.com/adfs/ls/',
Expand Down Expand Up @@ -1950,6 +2017,33 @@ describe( 'passport-saml /', function() {
}
});
});
it( 'acme_tools request signed with sha1 when using privateKey', function( done ) {
var samlConfig = {
entryPoint: 'https://adfs.acme_tools.com/adfs/ls/',
issuer: 'acme_tools_com',
callbackUrl: 'https://relyingparty/adfs/postResponse',
privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'),
authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password',
identifierFormat: null,
signatureAlgorithm: 'sha1',
additionalParams: {
customQueryStringParam: 'CustomQueryStringParamValue'
}
};
var samlObj = new SAML( samlConfig );
samlObj.generateUniqueID = function () { return '12345678901234567890' };
samlObj.getAuthorizeUrl({}, {}, function(err, url) {
try {
var qry = require('querystring').parse(require('url').parse(url).query);
qry.SigAlg.should.match('http://www.w3.org/2000/09/xmldsig#rsa-sha1');
qry.Signature.should.match('MeFo+LjufxP5A+sCRwzR/YH/RV6W14aYSFjUdie62JxkI6hDcVhoSZQUJ3wtWMhL59gJj05tTFnXAZRqUQVsavyy41cmUZVeCsat0gaHBQOILXpp9deB0iSJt1EVQTOJkVx8uu2/WYu/bBiH7w2bpwuCf1gJhlqZb/ca3B6yjHSMjnnVfc2LbNPWHpE5464lrs79VjDXf9GQWfrBr95dh3P51IAb7C+77KDWQUl9WfZfyyuEgS83vyZ0UGOxT4AObJ6NOcLs8+iidDdWJJkBaKQev6U+AghCjLQUYOrflivLIIyqATKu2q9PbOse6Phmnxok50+broXSG23+e+742Q==');
qry.customQueryStringParam.should.match('CustomQueryStringParamValue');
done();
} catch (err2) {
done(err2);
}
});
});
});

describe( 'getAdditionalParams checks /', function() {
Expand Down Expand Up @@ -2831,6 +2925,47 @@ describe( 'passport-saml /', function() {
}
});
});
it('errors if bad privateKey to requestToURL', function(done){
var samlObj = new SAML({
entryPoint: "foo",
privateKey: "-----BEGIN CERTIFICATE-----\n"+
"8mvhvrcCOiJ3mjgKNN1F31jOBJuZNmq0U7n9v+Z+3NfyU/0E9jkrnFvm5ks+p8kl\n" +
"BjuBk9RAkazsU9l02XMS/VxOOIifxKC7R9bDtx0hjolYxgqxPIO5s4rmjj0rLzvo\n" +
"vQTTTx/tB5e+hbdx922QSeTjP4DO4ms6cIexcH+ZEUOJ3wXiHToJW83SXLRtwPI9\n" +
"JbWKeS9nWPnzcedbDNZkGtohW5vf32BHuvLsWcl6eFXRSkdX/7+rgpXmDRB7caQ+\n" +
"2SXVY7ORily7LTKg1cFmuKHDzKTGFIp5/GU6dwIDAQABAoIBAArgFQ+Uk4UN4diY\n" +
"gJWCAaQlTVmP0UEHZQt/NmJrc9ZVduuhOP0hH6gF53nREHz5UQb4nXB2Ksa3MtYD\n" +
"Z1vhJcu/T7pvmib4q+Ij6oAmlyeL/xwVY3IUURMxX3tCdPItlk4PEFELKeqQOiIS\n" +
"7B0DYxWfJbMle3c95w5ruYEr2A+fHCKVSlDpg7uPd9VQ6t7bGMZZvc9tDSC1qPXQ\n" +
"Gd/WOMXxi+t/TpyVZ6tOcEekQzAMLmWElUUPx3TJ0ur0Zl2LZ7IvQEXXias4lUHV\n" +
"fnH3akDCMmdhlJSVqUfplrh85zAOh6fLloZagphj/Kpgfw1TZ+njSDYqSLYE0NZ1\n" +
"j+83feECgYEA2aNGgbc+t6QLrJJ63l9Mz541lVV3IUAxZ5ACqOnMkQVuLoa5IMwM\n" +
"oENIo38ptfHQqjQ9x8/tEINFqOHnQuOJ/+1xP9f0Me+0clRDCqjGYqNYgmakKyD7\n" +
"vey/q6kwHk679RVGiI1p+HdoA+CbEKWHJiRxE0RhAA3G3wGAq7kpJocCgYEAxp4/\n" +
"tCft+eHVRivspfDN//axc2TR6qWP9E1ueGvbiXPXv0Puag0W9cER/df/s5jW4Rqg\n" +
"CE8649HPUZ0FJT+YaeKgu2Sw9SMcGl4/uyHzg7KnXIeYyQZJPqQkKyXmIix8cw3+\n" +
"HBGRtwX5nOy0DgFdaMiH0F08peNI9QHKKTBoWJECgYEAyymJ1ekzWMaAR1Zt8EvS\n" +
"LjWoG4EuthFwjRZ4BSpLVk1Vb4VAKAeS+cAVfNpmG3xip6Ag0/ebe0CvtFk9QsmZ\n" +
"txj2EP0M7div/9H8y2SF3OpS41fhhIlDtyXcPuivDHu/Jaf4sdwgwlrk9EmlN0Lu\n" +
"CIMYMz4vtpclwGNss+EjMt0CgYEAqepD0Vm/iuCaVhfJsgSaFvnywSdlNfpBdtyv\n" +
"PzH2dFa4IZZ55hwgoklznNgmlnyQh68BbVpqpO+fDtDnz//h4ePRYb84a96Hcj9j\n" +
"AjJ/YxF5f/04xfEsw/wkPQ2FHYM1TDCSTWzyXcMs0gTl3H1qbfPvzF+XPMt+ZKwN\n" +
"SMNy4SECgYB3ig6t+XVfNkw8oBOh0Gx37XKbmImXsA8ucDAX9KUbMIvD03XCEf34\n" +
"jF3SNJh0SmHoT62vc+cJqPxMDP6E7Q1nZxsEyaAkKr2H4dSM4SlRm0VB+bS+jXsz\n" +
"PCiRGSm8eupuxfix05LMMreo4mC7e3Ir4JhdCsXxAMZIvbNyXcvUMA==\n" +
"-----END CERTIFICATE-----\n"
});
var request = '<?xml version=\\"1.0\\"?><samlp:AuthnRequest xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" ID=\\"_ea40a8ab177df048d645\\" Version=\\"2.0\\" IssueInstant=\\"2017-08-22T19:30:01.363Z\\" ProtocolBinding=\\"urn:oasis:names$tc:SAML:2.0:bindings:HTTP-POST\\" AssertionConsumerServiceURL=\\"https://example.com/login/callback\\" Destination=\\"https://www.example.com\\"><saml:Issuer xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">onelogin_saml</saml:Issuer><s$mlp:NameIDPolicy xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protocol\\" Format=\\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\\" AllowCreate=\\"true\\"/><samlp:RequestedAuthnContext xmlns:samlp=\\"urn:oasis:names:tc:SAML:2.0:protoc$l\\" Comparison=\\"exact\\"><saml:AuthnContextClassRef xmlns:saml=\\"urn:oasis:names:tc:SAML:2.0:assertion\\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp$AuthnRequest>';
samlObj.requestToUrl(request, null, 'authorize', {}, function(err) {
try {
should.exist(err);
err.message.should.containEql('no start line');
done();
} catch (err2) {
done(err2);
}
});
});
});

describe('validateRedirect()', function() {
Expand Down