From 49e8361cc3d0ff29be3673758e33d581b5043316 Mon Sep 17 00:00:00 2001 From: "Matt R. Wilson" Date: Mon, 30 Jan 2023 13:10:22 -0700 Subject: [PATCH] feat: octet string builder Adds `octetStringBuilder` to libsaml to DRY up the signature alg code. The octet string is needed in four places: creating and verifying the signature for the Redirect and SimpleSign bindings. Previously, both creations were done adhoc while both verifications where not done by the samlify at all and instead left to user code. #308 #360 The new util is now used during verification if the request object does not include a `octetString` key. --- src/binding-redirect.ts | 75 ++++++++++++++++---------------- src/binding-simplesign.ts | 30 ++++--------- src/flow.ts | 22 ++++++++-- src/libsaml.ts | 50 +++++++++++++++++++++ test/flow.ts | 91 ++++++++++++++------------------------- test/issues.ts | 28 ++++++------ 6 files changed, 161 insertions(+), 135 deletions(-) diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts index 4ff402d9..96697239 100644 --- a/src/binding-redirect.ts +++ b/src/binding-redirect.ts @@ -25,58 +25,59 @@ export interface BuildRedirectConfig { /** * @private -* @desc Helper of generating URL param/value pair -* @param {string} param key -* @param {string} value value of key -* @param {boolean} first determine whether the param is the starting one in order to add query header '?' -* @return {string} -*/ -function pvPair(param: string, value: string, first?: boolean): string { - return (first === true ? '?' : '&') + param + '=' + value; -} -/** -* @private -* @desc Refractored part of URL generation for login/logout request +* @desc Refactored part of URL generation for login/logout request * @param {string} type * @param {boolean} isSigned * @param {string} rawSamlRequest * @param {object} entitySetting * @return {string} */ -function buildRedirectURL(opts: BuildRedirectConfig) { +function buildRedirectURL(opts: BuildRedirectConfig): string { const { baseUrl, type, - isSigned, context, - entitySetting, + relayState, + isSigned, + entitySetting } = opts; - let { relayState = '' } = opts; - const noParams = (url.parse(baseUrl).query || []).length === 0; - const queryParam = libsaml.getQueryParamByType(type); - // In general, this xmlstring is required to do deflate -> base64 -> urlencode - const samlRequest = encodeURIComponent(utility.base64Encode(utility.deflateString(context))); - if (relayState !== '') { - relayState = pvPair(urlParams.relayState, encodeURIComponent(relayState)); + + const redirectUrl = new url.URL(baseUrl) + + const direction = libsaml.getQueryParamByType(type); + // In general, this XML string is required to do deflate -> base64 -> URL encode + const encodedContext = utility.base64Encode(utility.deflateString(context)); + redirectUrl.searchParams.set(direction, encodedContext); + + if (relayState) { + redirectUrl.searchParams.set(urlParams.relayState, relayState); } + if (isSigned) { - const sigAlg = pvPair(urlParams.sigAlg, encodeURIComponent(entitySetting.requestSignatureAlgorithm)); - const octetString = samlRequest + relayState + sigAlg; - return baseUrl - + pvPair(queryParam, octetString, noParams) - + pvPair(urlParams.signature, encodeURIComponent( - libsaml.constructMessageSignature( - queryParam + '=' + octetString, - entitySetting.privateKey, - entitySetting.privateKeyPass, - undefined, - entitySetting.requestSignatureAlgorithm - ).toString() - ) - ); + const octetString = libsaml.octetStringBuilder( + binding.redirect, + direction, + { // can just be `Object.fromEntries(redirectUrl.searchParams)` when the targeted Node version is updated. + [direction]: encodedContext, + [urlParams.relayState]: relayState, + [urlParams.sigAlg]: entitySetting.requestSignatureAlgorithm, + } + ); + const signature = libsaml.constructMessageSignature( + octetString, + entitySetting.privateKey, + entitySetting.privateKeyPass, + true, + entitySetting.requestSignatureAlgorithm + ).toString(); + + redirectUrl.searchParams.set(urlParams.signature, signature); + redirectUrl.searchParams.set(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm); } - return baseUrl + pvPair(queryParam, samlRequest + relayState, noParams); + + return redirectUrl.toString(); } + /** * @desc Redirect URL for login request * @param {object} entity object includes both idp and sp diff --git a/src/binding-simplesign.ts b/src/binding-simplesign.ts index e40cc0d1..fc6f2596 100644 --- a/src/binding-simplesign.ts +++ b/src/binding-simplesign.ts @@ -26,17 +26,6 @@ export interface BindingSimpleSignContext { sigAlg: string; } -/** -* @private -* @desc Helper of generating URL param/value pair -* @param {string} param key -* @param {string} value value of key -* @param {boolean} first determine whether the param is the starting one in order to add query header '?' -* @return {string} -*/ -function pvPair(param: string, value: string, first?: boolean): string { - return (first === true ? '?' : '&') + param + '=' + value; -} /** * @private * @desc Refactored part of simple signature generation for login/logout request @@ -51,17 +40,16 @@ function buildSimpleSignature(opts: BuildSimpleSignConfig) : string { context, entitySetting, } = opts; - let { relayState = '' } = opts; - const queryParam = libsaml.getQueryParamByType(type); - - if (relayState !== '') { - relayState = pvPair(urlParams.relayState, relayState); - } + // ?SAMLRequest= or ?SAMLResponse= + const direction = libsaml.getQueryParamByType(type); + const octetString = libsaml.octetStringBuilder(binding.simpleSign, direction, { + [direction]: context, + [urlParams.relayState]: opts.relayState, + [urlParams.sigAlg]: entitySetting.requestSignatureAlgorithm, + }); - const sigAlg = pvPair(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm); - const octetString = context + relayState + sigAlg; return libsaml.constructMessageSignature( - queryParam + '=' + octetString, + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, @@ -117,7 +105,7 @@ function base64LoginRequest(entity: any, customTagReplacement?: (template: strin sigAlg: spSetting.requestSignatureAlgorithm, }; } - // No need to embeded XML signature + // No need to embed XML signature return { id, context: utility.base64Encode(rawSamlRequest), diff --git a/src/flow.ts b/src/flow.ts index f58a5360..ef99edee 100644 --- a/src/flow.ts +++ b/src/flow.ts @@ -53,7 +53,7 @@ function getDefaultExtractorFields(parserType: ParserType, assertion?: any): Ext async function redirectFlow(options): Promise { const { request, parserType, self, checkSignature = true, from } = options; - const { query, octetString } = request; + const { query } = request; const { SigAlg: sigAlg, Signature: signature } = query; const targetEntityMetadata = from.entityMeta; @@ -109,6 +109,9 @@ async function redirectFlow(options): Promise { return Promise.reject('ERR_MISSING_SIG_ALG'); } + // Look for the octet string on the request object first as a backwards compat feature + const octetString = request.octetString || libsaml.octetStringBuilder(bindDict.redirect, direction, query) + // put the below two assignments into verifyMessageSignature function const base64Signature = Buffer.from(decodeURIComponent(signature), 'base64'); const decodeSigAlg = decodeURIComponent(sigAlg); @@ -296,9 +299,7 @@ async function postFlow(options): Promise { async function postSimpleSignFlow(options): Promise { const { request, parserType, self, checkSignature = true, from } = options; - - const { body, octetString } = request; - + const { body } = request; const targetEntityMetadata = from.entityMeta; // ?SAMLRequest= or ?SAMLResponse= @@ -354,6 +355,19 @@ async function postSimpleSignFlow(options): Promise { return Promise.reject('ERR_MISSING_SIG_ALG'); } + // Look for the octet string on the request object first as a backwards compat feature + const octetString = request.octetString || libsaml.octetStringBuilder( + bindDict.simpleSign, + direction, + { + ...body, + // SimpleSign wants the XML already base64-decoded before computing the octet string. + // For encapsulation, it would be nice to have the helper decode it, + // but for optimization, lets not decode it twice. + [direction]: xmlString + } + ) + // put the below two assignments into verifyMessageSignature function const base64Signature = Buffer.from(signature, 'base64'); diff --git a/src/libsaml.ts b/src/libsaml.ts index 56f1b4b2..f21dbcc6 100644 --- a/src/libsaml.ts +++ b/src/libsaml.ts @@ -240,6 +240,55 @@ const libSaml = () => { return prefix + camelContent.charAt(0).toUpperCase() + camelContent.slice(1); } + /** + * Create the octet string for the signature algorithms. + * + * Used in both Redirect and POST-SimpleSign bindings. + * HTTP-Redirect Binding as defined in the Bindings OASIS Standard, Section 3.4.4.1. + * http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf + * HTTP-POST-SimpleSign Binding as defined in the "SimpleSign" Binding OASIS Committee Draft. + * https://www.oasis-open.org/committees/download.php/30234/sstc-saml-binding-simplesign-cd-04.pdf + * + * @public + * @param {string} binding "redirect" or "simpleSign" + * @param {string} direction "SAMLRequest" or "SAMLResponse" + * @param {object} values Object that includes SAMLRequest/SAMLResponse, SigAlg, and optional RelayState. All other values are ignored. + * @return {string} + */ + function octetStringBuilder(binding: string, direction: string, values: Record): string { + const content = values[direction] + const sigAlg = values[urlParams.sigAlg] + const relayState = values[urlParams.relayState] + + if (typeof content !== 'string') { + throw Error('ERR_OCTET_BAD_ARGS_CONTENT') + } + + if (typeof sigAlg !== 'string') { + throw Error('ERR_MISSING_SIG_ALG') + } + + const params: string[][] = [[direction, content]] + + if (typeof relayState === 'string' && relayState.length !== 0) { + params.push([urlParams.relayState, relayState]) + } + + params.push([urlParams.sigAlg, sigAlg]) + + // Redirect binding needs each param URL encoded, `URLSearchParams` gives us this out of the box and maintains order. + if (binding === wording.binding.redirect){ + return new URLSearchParams(params).toString() + } + + // The SimpleSign octet string combines the params with &,= but doesn't encode for URLs. + if (binding === wording.binding.simpleSign){ + return params.map(([k,v]) => `${k}=${v}`).join('&') + } + + throw Error('ERR_OCTET_UNDEFINED_BINDING') + } + return { createXPath, @@ -250,6 +299,7 @@ const libSaml = () => { defaultAttributeTemplate, defaultLogoutRequestTemplate, defaultLogoutResponseTemplate, + octetStringBuilder, /** * @desc Replace the tag (e.g. {tag}) inside the raw XML diff --git a/test/flow.ts b/test/flow.ts index 19bf5532..9c12d230 100644 --- a/test/flow.ts +++ b/test/flow.ts @@ -82,31 +82,16 @@ const parseRedirectUrlContextCallBack = (_context: string) => { const _SAMLResponse = originalURL.query.SAMLResponse; const _Signature = originalURL.query.Signature; const _SigAlg = originalURL.query.SigAlg; - delete originalURL.query.Signature; - const _octetString = Object.keys(originalURL.query).map(q => q + '=' + encodeURIComponent(originalURL.query[q] as string)).join('&'); + const octetString = libsaml.octetStringBuilder('redirect','SAMLResponse', originalURL.query); return { query: { SAMLResponse: _SAMLResponse, Signature: _Signature, SigAlg: _SigAlg, }, - octetString: _octetString, + octetString, }; }; -// Build SimpleSign octetString -const buildSimpleSignOctetString = (type:string, context:string, sigAlg:string|undefined, relayState:string|undefined, signature: string|undefined) =>{ - const rawRequest = String(utility.base64Decode(context, true)); - let octetString:string = ''; - octetString += type + '=' + rawRequest; - if (relayState !== undefined && relayState.length > 0){ - octetString += '&RelayState=' + relayState; - } - if (signature !== undefined && signature.length >0 && sigAlg && sigAlg.length > 0){ - octetString += '&SigAlg=' + sigAlg; - } - return octetString; -}; - // Define of metadata const defaultIdpConfig = { @@ -182,11 +167,10 @@ test('create login request with redirect binding using default template and pars }); test('create login request with post simpleSign binding using default template and parse it', async t => { - const { relayState, id, context: SAMLRequest, type, sigAlg, signature } = sp.createLoginRequest(idp, 'simpleSign') as SimpleSignBindingContext; + const { id, context: SAMLRequest, type, sigAlg, signature } = sp.createLoginRequest(idp, 'simpleSign') as SimpleSignBindingContext; t.is(typeof id, 'string'); t.is(typeof SAMLRequest, 'string'); - const octetString = buildSimpleSignOctetString(type, SAMLRequest, sigAlg, relayState,signature); - const { samlContent, extract } = await idp.parseLoginRequest(sp, 'simpleSign', { body: { SAMLRequest, Signature: signature, SigAlg:sigAlg }, octetString}); + const { extract } = await idp.parseLoginRequest(sp, 'simpleSign', { body: { SAMLRequest, Signature: signature, SigAlg:sigAlg }}); t.is(extract.issuer, 'https://sp.example.org/metadata'); t.is(typeof extract.request.id, 'string'); t.is(extract.nameIDPolicy.format, 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'); @@ -194,7 +178,7 @@ test('create login request with post simpleSign binding using default template a }); test('create login request with post binding using default template and parse it', async t => { - const { relayState, type, entityEndpoint, id, context: SAMLRequest } = sp.createLoginRequest(idp, 'post') as PostBindingContext; + const { type, entityEndpoint, id, context: SAMLRequest } = sp.createLoginRequest(idp, 'post') as PostBindingContext; t.is(typeof id, 'string'); t.is(typeof SAMLRequest, 'string'); t.is(typeof entityEndpoint, 'string'); @@ -262,10 +246,11 @@ test('create login request with redirect binding signing with unencrypted PKCS#8 const { context } = _sp.createLoginRequest(idp, 'redirect'); - const parsed = parseRedirectUrlContextCallBack(context) - const signature = Buffer.from(parsed.query.Signature as string, 'base64'); + const parsed: Record = url.parse(context, true).query; + const octetString = libsaml.octetStringBuilder('redirect','SAMLRequest', parsed) + const signature = Buffer.from(parsed.Signature, 'base64'); - const valid = libsaml.verifyMessageSignature(_sp.entityMeta, parsed.octetString, signature, parsed.query.SigAlg as string); + const valid = libsaml.verifyMessageSignature(_sp.entityMeta, octetString, signature, parsed.SigAlg); t.true(valid, 'signature did not validate'); }); @@ -279,10 +264,11 @@ test('create login request with redirect binding signing with encrypted PKCS#8', const { context } = _sp.createLoginRequest(idp, 'redirect'); - const parsed = parseRedirectUrlContextCallBack(context) - const signature = Buffer.from(parsed.query.Signature as string, 'base64'); + const parsed: Record = url.parse(context, true).query; + const octetString = libsaml.octetStringBuilder('redirect','SAMLRequest', parsed) + const signature = Buffer.from(parsed.Signature, 'base64'); - const valid = libsaml.verifyMessageSignature(_sp.entityMeta, parsed.octetString, signature, parsed.query.SigAlg as string); + const valid = libsaml.verifyMessageSignature(_sp.entityMeta, octetString, signature, parsed.SigAlg); t.true(valid, 'signature did not validate'); }); @@ -442,8 +428,7 @@ test('send response with signed assertion by post simplesign and parse it', asyn 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg: sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -525,7 +510,7 @@ test('send response with signed assertion + custom transformation algorithms by ); const user = { email: 'user@esaml2.com' } - const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse( + const { id, context: SAMLResponse, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse( signedAssertionSp, sampleRequestInfo, 'simpleSign', @@ -535,8 +520,7 @@ test('send response with signed assertion + custom transformation algorithms by 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -606,7 +590,7 @@ test('send response with [custom template] signed assertion by post simpleSign a // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) const requestInfo = { extract: { request: { id: 'request_id' } } }; const user = { email: 'user@esaml2.com'}; - const { id, context: SAMLResponse, type, sigAlg, signature, entityEndpoint, relayState } = await idpcustomNoEncrypt.createLoginResponse( + const { id, context: SAMLResponse, sigAlg, signature, entityEndpoint, relayState } = await idpcustomNoEncrypt.createLoginResponse( sp, requestInfo, 'simpleSign', @@ -617,8 +601,7 @@ test('send response with [custom template] signed assertion by post simpleSign a 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -673,7 +656,7 @@ test('send response with signed message by redirect and parse it', async t => { test('send response with signed message by post simplesign and parse it', async t => { // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse( + const { id, context: SAMLResponse, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse( spNoAssertSign, sampleRequestInfo, 'simpleSign', @@ -683,8 +666,7 @@ test('send response with signed message by post simplesign and parse it', async 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -745,9 +727,8 @@ test('send response with [custom template] and signed message by redirect and pa test('send response with [custom template] and signed message by post simplesign and parse it', async t => { // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) - const requestInfo = { extract: { authnrequest: { id: 'request_id' } } }; const user = { email: 'user@esaml2.com'}; - const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( + const { id, context: SAMLResponse, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( spNoAssertSign, { extract: { authnrequest: { id: 'request_id' } } }, 'simpleSign', { email: 'user@esaml2.com' }, @@ -756,8 +737,7 @@ test('send response with [custom template] and signed message by post simplesign 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -821,15 +801,14 @@ test('send login response with signed assertion + signed message by post simples wantMessageSigned: true, }); const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo, + const { id, context: SAMLResponse, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idpNoEncrypt, spWantMessageSign, binding.simpleSign, user), undefined, 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spWantMessageSign.parseLoginResponse (idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse (idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -900,7 +879,7 @@ test('send login response with [custom template] and signed assertion + signed m wantMessageSigned: true, }); const user = { email: 'user@esaml2.com'}; - const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( + const { id, context: SAMLResponse, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( spWantMessageSign, { extract: { authnrequest: { id: 'request_id' } } }, 'simpleSign', @@ -910,8 +889,7 @@ test('send login response with [custom template] and signed assertion + signed m 'relaystate' ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -1160,15 +1138,14 @@ test('avoid malformatted response with redirect binding', async t => { } }); -test('avoid malformatted response with simplesign binding', async t => { +test('avoid malformed response with simplesign binding', async t => { // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) const user = { email: 'user@email.com' }; - const { context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idpNoEncrypt, sp, binding.simpleSign, user), undefined, 'relaystate'); + const { context: SAMLResponse, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idpNoEncrypt, sp, binding.simpleSign, user), undefined, 'relaystate'); const rawResponse = String(utility.base64Decode(SAMLResponse, true)); const attackResponse = `evil@evil.com${rawResponse}`; - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); try { - await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(attackResponse), Signature: signature, SigAlg:sigAlg }, octetString }); + await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(attackResponse), RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); } catch (e) { // it must throw an error t.is(true, true); @@ -1305,11 +1282,10 @@ test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response const user = { email: 'user@esaml2.com' }; try { - const { context: SAMLResponse, type, sigAlg, signature, relayState } = await idp.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, sp, binding.simpleSign, user), undefined, 'relaystate'); - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); + const { context: SAMLResponse, sigAlg, signature, relayState } = await idp.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, sp, binding.simpleSign, user), undefined, 'relaystate'); // simulate the time on client side when response arrives after 5.1 sec tk.freeze(fiveMinutesOneSecLater); - await sp.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + await sp.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); // test failed, it shouldn't happen t.is(true, false); } catch (e) { @@ -1368,11 +1344,10 @@ test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML respo const user = { email: 'user@esaml2.com' }; try { - const { context: SAMLResponse, type, signature, sigAlg, relayState } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, spWithClockDrift, binding.simpleSign, user), undefined, 'relaystate'); - const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); + const { context: SAMLResponse, signature, sigAlg, relayState } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, spWithClockDrift, binding.simpleSign, user), undefined, 'relaystate'); // simulate the time on client side when response arrives after 5.1 sec tk.freeze(fiveMinutesOneSecLater); - await spWithClockDrift.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + await spWithClockDrift.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, RelayState: relayState, Signature: signature, SigAlg:sigAlg }}); t.is(true, true); } catch (e) { // test failed, it shouldn't happen diff --git a/test/issues.ts b/test/issues.ts index 3c768f01..8340de7d 100644 --- a/test/issues.ts +++ b/test/issues.ts @@ -1,5 +1,5 @@ import esaml2 = require('../index'); -import { readFileSync, writeFileSync } from 'fs'; +import { readFileSync } from 'fs'; import test from 'ava'; import * as fs from 'fs'; import * as url from 'url'; @@ -10,8 +10,6 @@ import { extract } from '../src/extractor'; const { IdentityProvider: identityProvider, ServiceProvider: serviceProvider, - IdPMetadata: idpMetadata, - SPMetadata: spMetadata, Utility: utility, SamlLib: libsaml, Constants: ref, @@ -43,17 +41,17 @@ test('#31 query param for sso/slo returns error', t => { nameIDFormat: ['urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'], assertionConsumerService: [{ Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - Location: 'sp.example.com/acs', + Location: 'https://sp.example.com/acs', }, { Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - Location: 'sp.example.com/acs', + Location: 'https://sp.example.com/acs', }], singleLogoutService: [{ Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - Location: 'sp.example.com/slo', + Location: 'https://sp.example.com/slo', }, { Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - Location: 'sp.example.com/slo', + Location: 'https://sp.example.com/slo', }], }; const idpcfg = { @@ -61,17 +59,17 @@ test('#31 query param for sso/slo returns error', t => { nameIDFormat: ['urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'], singleSignOnService: [{ Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - Location: 'idp.example.com/sso', + Location: 'https://idp.example.com/sso', }, { Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - Location: 'idp.example.com/sso', + Location: 'https://idp.example.com/sso', }], singleLogoutService: [{ Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - Location: 'idp.example.com/sso/slo', + Location: 'https://idp.example.com/sso/slo', }, { Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - Location: 'idp.example.com/sso/slo', + Location: 'https://idp.example.com/sso/slo', }], }; const idp = identityProvider(idpcfg); @@ -151,11 +149,11 @@ test('#31 query param for sso/slo returns error', t => { }); test('#91 idp gets single sign on service from the metadata', t => { - t.is(idp.entityMeta.getSingleSignOnService('post'), 'idp.example.com/sso'); + t.is(idp.entityMeta.getSingleSignOnService('post'), 'https://idp.example.com/sso'); }); - + test('#98 undefined AssertionConsumerServiceURL with redirect request', t => { - const { id, context } = sp98.createLoginRequest(idp, 'redirect'); + const { context } = sp98.createLoginRequest(idp, 'redirect'); const originalURL = url.parse(context, true); const request = originalURL.query.SAMLRequest as string; const rawRequest = utility.inflateString(decodeURIComponent(request)); @@ -164,4 +162,4 @@ test('#31 query param for sso/slo returns error', t => { const index = Object.keys(authnRequest.attributes).find((i: string) => authnRequest.attributes[i].nodeName === 'AssertionConsumerServiceURL') as any; t.is(authnRequest.attributes[index].nodeValue, 'https://example.org/response'); }); -})(); \ No newline at end of file +})();