Skip to content

Commit 647bce9

Browse files
committed
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.
1 parent a34b0bc commit 647bce9

File tree

6 files changed

+161
-139
lines changed

6 files changed

+161
-139
lines changed

src/binding-redirect.ts

+38-37
Original file line numberDiff line numberDiff line change
@@ -25,58 +25,59 @@ export interface BuildRedirectConfig {
2525

2626
/**
2727
* @private
28-
* @desc Helper of generating URL param/value pair
29-
* @param {string} param key
30-
* @param {string} value value of key
31-
* @param {boolean} first determine whether the param is the starting one in order to add query header '?'
32-
* @return {string}
33-
*/
34-
function pvPair(param: string, value: string, first?: boolean): string {
35-
return (first === true ? '?' : '&') + param + '=' + value;
36-
}
37-
/**
38-
* @private
39-
* @desc Refractored part of URL generation for login/logout request
28+
* @desc Refactored part of URL generation for login/logout request
4029
* @param {string} type
4130
* @param {boolean} isSigned
4231
* @param {string} rawSamlRequest
4332
* @param {object} entitySetting
4433
* @return {string}
4534
*/
46-
function buildRedirectURL(opts: BuildRedirectConfig) {
35+
function buildRedirectURL(opts: BuildRedirectConfig): string {
4736
const {
4837
baseUrl,
4938
type,
50-
isSigned,
5139
context,
52-
entitySetting,
40+
relayState,
41+
isSigned,
42+
entitySetting
5343
} = opts;
54-
let { relayState = '' } = opts;
55-
const noParams = (url.parse(baseUrl).query || []).length === 0;
56-
const queryParam = libsaml.getQueryParamByType(type);
57-
// In general, this xmlstring is required to do deflate -> base64 -> urlencode
58-
const samlRequest = encodeURIComponent(utility.base64Encode(utility.deflateString(context)));
59-
if (relayState !== '') {
60-
relayState = pvPair(urlParams.relayState, encodeURIComponent(relayState));
44+
45+
const redirectUrl = new url.URL(baseUrl)
46+
47+
const direction = libsaml.getQueryParamByType(type);
48+
// In general, this XML string is required to do deflate -> base64 -> URL encode
49+
const encodedContext = utility.base64Encode(utility.deflateString(context));
50+
redirectUrl.searchParams.set(direction, encodedContext);
51+
52+
if (relayState) {
53+
redirectUrl.searchParams.set(urlParams.relayState, relayState);
6154
}
55+
6256
if (isSigned) {
63-
const sigAlg = pvPair(urlParams.sigAlg, encodeURIComponent(entitySetting.requestSignatureAlgorithm));
64-
const octetString = samlRequest + relayState + sigAlg;
65-
return baseUrl
66-
+ pvPair(queryParam, octetString, noParams)
67-
+ pvPair(urlParams.signature, encodeURIComponent(
68-
libsaml.constructMessageSignature(
69-
queryParam + '=' + octetString,
70-
entitySetting.privateKey,
71-
entitySetting.privateKeyPass,
72-
undefined,
73-
entitySetting.requestSignatureAlgorithm
74-
).toString()
75-
)
76-
);
57+
const octetString = libsaml.octetStringBuilder(
58+
binding.redirect,
59+
direction,
60+
{ // can just be `Object.fromEntries(redirectUrl.searchParams)` when the targeted Node version is updated.
61+
[direction]: encodedContext,
62+
[urlParams.relayState]: relayState,
63+
[urlParams.sigAlg]: entitySetting.requestSignatureAlgorithm,
64+
}
65+
);
66+
const signature = libsaml.constructMessageSignature(
67+
octetString,
68+
entitySetting.privateKey,
69+
entitySetting.privateKeyPass,
70+
true,
71+
entitySetting.requestSignatureAlgorithm
72+
).toString();
73+
74+
redirectUrl.searchParams.set(urlParams.signature, signature);
75+
redirectUrl.searchParams.set(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm);
7776
}
78-
return baseUrl + pvPair(queryParam, samlRequest + relayState, noParams);
77+
78+
return redirectUrl.toString();
7979
}
80+
8081
/**
8182
* @desc Redirect URL for login request
8283
* @param {object} entity object includes both idp and sp

src/binding-simplesign.ts

+9-21
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,6 @@ export interface BindingSimpleSignContext {
2626
sigAlg: string;
2727
}
2828

29-
/**
30-
* @private
31-
* @desc Helper of generating URL param/value pair
32-
* @param {string} param key
33-
* @param {string} value value of key
34-
* @param {boolean} first determine whether the param is the starting one in order to add query header '?'
35-
* @return {string}
36-
*/
37-
function pvPair(param: string, value: string, first?: boolean): string {
38-
return (first === true ? '?' : '&') + param + '=' + value;
39-
}
4029
/**
4130
* @private
4231
* @desc Refactored part of simple signature generation for login/logout request
@@ -51,17 +40,16 @@ function buildSimpleSignature(opts: BuildSimpleSignConfig) : string {
5140
context,
5241
entitySetting,
5342
} = opts;
54-
let { relayState = '' } = opts;
55-
const queryParam = libsaml.getQueryParamByType(type);
56-
57-
if (relayState !== '') {
58-
relayState = pvPair(urlParams.relayState, relayState);
59-
}
43+
// ?SAMLRequest= or ?SAMLResponse=
44+
const direction = libsaml.getQueryParamByType(type);
45+
const octetString = libsaml.octetStringBuilder(binding.simpleSign, direction, {
46+
[direction]: context,
47+
[urlParams.relayState]: opts.relayState,
48+
[urlParams.sigAlg]: entitySetting.requestSignatureAlgorithm,
49+
});
6050

61-
const sigAlg = pvPair(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm);
62-
const octetString = context + relayState + sigAlg;
6351
return libsaml.constructMessageSignature(
64-
queryParam + '=' + octetString,
52+
octetString,
6553
entitySetting.privateKey,
6654
entitySetting.privateKeyPass,
6755
undefined,
@@ -117,7 +105,7 @@ function base64LoginRequest(entity: any, customTagReplacement?: (template: strin
117105
sigAlg: spSetting.requestSignatureAlgorithm,
118106
};
119107
}
120-
// No need to embeded XML signature
108+
// No need to embed XML signature
121109
return {
122110
id,
123111
context: utility.base64Encode(rawSamlRequest),

src/flow.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function getDefaultExtractorFields(parserType: ParserType, assertion?: any): Ext
5353
async function redirectFlow(options): Promise<FlowResult> {
5454

5555
const { request, parserType, self, checkSignature = true, from } = options;
56-
const { query, octetString } = request;
56+
const { query } = request;
5757
const { SigAlg: sigAlg, Signature: signature } = query;
5858

5959
const targetEntityMetadata = from.entityMeta;
@@ -109,6 +109,9 @@ async function redirectFlow(options): Promise<FlowResult> {
109109
return Promise.reject('ERR_MISSING_SIG_ALG');
110110
}
111111

112+
// Look for the octet string on the request object first as a backwards compat feature
113+
const octetString = request.octetString || libsaml.octetStringBuilder(bindDict.redirect, direction, query)
114+
112115
// put the below two assignments into verifyMessageSignature function
113116
const base64Signature = Buffer.from(decodeURIComponent(signature), 'base64');
114117
const decodeSigAlg = decodeURIComponent(sigAlg);
@@ -296,9 +299,7 @@ async function postFlow(options): Promise<FlowResult> {
296299
async function postSimpleSignFlow(options): Promise<FlowResult> {
297300

298301
const { request, parserType, self, checkSignature = true, from } = options;
299-
300-
const { body, octetString } = request;
301-
302+
const { body } = request;
302303
const targetEntityMetadata = from.entityMeta;
303304

304305
// ?SAMLRequest= or ?SAMLResponse=
@@ -354,6 +355,19 @@ async function postSimpleSignFlow(options): Promise<FlowResult> {
354355
return Promise.reject('ERR_MISSING_SIG_ALG');
355356
}
356357

358+
// Look for the octet string on the request object first as a backwards compat feature
359+
const octetString = request.octetString || libsaml.octetStringBuilder(
360+
bindDict.simpleSign,
361+
direction,
362+
{
363+
...body,
364+
// SimpleSign wants the XML already base64-decoded before computing the octet string.
365+
// For encapsulation, it would be nice to have the helper decode it,
366+
// but for optimization, lets not decode it twice.
367+
[direction]: xmlString
368+
}
369+
)
370+
357371
// put the below two assignments into verifyMessageSignature function
358372
const base64Signature = Buffer.from(signature, 'base64');
359373

src/libsaml.ts

+50
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,55 @@ const libSaml = () => {
238238
return prefix + camelContent.charAt(0).toUpperCase() + camelContent.slice(1);
239239
}
240240

241+
/**
242+
* Create the octet string for the signature algorithms.
243+
*
244+
* Used in both Redirect and POST-SimpleSign bindings.
245+
* HTTP-Redirect Binding as defined in the Bindings OASIS Standard, Section 3.4.4.1.
246+
* http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
247+
* HTTP-POST-SimpleSign Binding as defined in the "SimpleSign" Binding OASIS Committee Draft.
248+
* https://www.oasis-open.org/committees/download.php/30234/sstc-saml-binding-simplesign-cd-04.pdf
249+
*
250+
* @public
251+
* @param {string} binding "redirect" or "simpleSign"
252+
* @param {string} direction "SAMLRequest" or "SAMLResponse"
253+
* @param {object} values Object that includes SAMLRequest/SAMLResponse, SigAlg, and optional RelayState. All other values are ignored.
254+
* @return {string}
255+
*/
256+
function octetStringBuilder(binding: string, direction: string, values: Record<string, unknown>): string {
257+
const content = values[direction]
258+
const sigAlg = values[urlParams.sigAlg]
259+
const relayState = values[urlParams.relayState]
260+
261+
if (typeof content !== 'string') {
262+
throw Error('ERR_OCTET_BAD_ARGS_CONTENT')
263+
}
264+
265+
if (typeof sigAlg !== 'string') {
266+
throw Error('ERR_MISSING_SIG_ALG')
267+
}
268+
269+
const params: string[][] = [[direction, content]]
270+
271+
if (typeof relayState === 'string' && relayState.length !== 0) {
272+
params.push([urlParams.relayState, relayState])
273+
}
274+
275+
params.push([urlParams.sigAlg, sigAlg])
276+
277+
// Redirect binding needs each param URL encoded, `URLSearchParams` gives us this out of the box and maintains order.
278+
if (binding === wording.binding.redirect){
279+
return new URLSearchParams(params).toString()
280+
}
281+
282+
// The SimpleSign octet string combines the params with &,= but doesn't encode for URLs.
283+
if (binding === wording.binding.simpleSign){
284+
return params.map(([k,v]) => `${k}=${v}`).join('&')
285+
}
286+
287+
throw Error('ERR_OCTET_UNDEFINED_BINDING')
288+
}
289+
241290
return {
242291

243292
createXPath,
@@ -248,6 +297,7 @@ const libSaml = () => {
248297
defaultAttributeTemplate,
249298
defaultLogoutRequestTemplate,
250299
defaultLogoutResponseTemplate,
300+
octetStringBuilder,
251301

252302
/**
253303
* @desc Replace the tag (e.g. {tag}) inside the raw XML

0 commit comments

Comments
 (0)