From 059463bef8cdc724e2cdc83239a57147cf92f112 Mon Sep 17 00:00:00 2001 From: Andrew Lavery Date: Thu, 4 Apr 2019 13:45:35 -0700 Subject: [PATCH] fix compile errors from strict null checks --- src/binding-post.ts | 10 +++++----- src/binding-redirect.ts | 8 ++++---- src/entity-idp.ts | 5 +++-- src/entity-sp.ts | 2 +- src/extractor.ts | 12 ++++++------ src/flow.ts | 4 ++-- src/libsaml.ts | 22 ++++++++++++---------- src/metadata-sp.ts | 10 +++++----- src/metadata.ts | 4 ++-- src/schema-validator.ts | 4 ++-- src/utility.ts | 6 +++++- src/validator.ts | 8 ++++---- test/flow.ts | 8 ++++---- test/index.ts | 4 ++-- tsconfig.json | 1 + 15 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/binding-post.ts b/src/binding-post.ts index 9e574923..39805387 100644 --- a/src/binding-post.ts +++ b/src/binding-post.ts @@ -18,7 +18,7 @@ const binding = wording.binding; * @param {object} entity object includes both idp and sp * @param {function} customTagReplacement used when developers have their own login response template */ -function base64LoginRequest(referenceTagXPath: string, entity: any, customTagReplacement: (template: string) => BindingContext): BindingContext { +function base64LoginRequest(referenceTagXPath: string, entity: any, customTagReplacement?: (template: string) => BindingContext): BindingContext { const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta }; const spSetting = entity.sp.entitySetting; let id: string = ''; @@ -26,7 +26,7 @@ function base64LoginRequest(referenceTagXPath: string, entity: any, customTagRep if (metadata && metadata.idp && metadata.sp) { const base = metadata.idp.getSingleSignOnService(binding.post); let rawSamlRequest: string; - if (spSetting.loginRequestTemplate) { + if (spSetting.loginRequestTemplate && customTagReplacement) { const info = customTagReplacement(spSetting.loginRequestTemplate.context); id = get(info, 'id', null); rawSamlRequest = get(info, 'context', null); @@ -78,7 +78,7 @@ function base64LoginRequest(referenceTagXPath: string, entity: any, customTagRep * @param {function} customTagReplacement used when developers have their own login response template * @param {boolean} encryptThenSign whether or not to encrypt then sign first (if signing). Defaults to sign-then-encrypt */ -async function base64LoginResponse(requestInfo: any = {}, entity: any, user: any = {}, customTagReplacement: (template: string) => BindingContext, encryptThenSign: boolean = false): Promise { +async function base64LoginResponse(requestInfo: any = {}, entity: any, user: any = {}, customTagReplacement?: (template: string) => BindingContext, encryptThenSign: boolean = false): Promise { const idpSetting = entity.idp.entitySetting; const spSetting = entity.sp.entitySetting; const id = idpSetting.generateID(); @@ -117,7 +117,7 @@ async function base64LoginResponse(requestInfo: any = {}, entity: any, user: any AuthnStatement: '', AttributeStatement: '', }; - if (idpSetting.loginResponseTemplate) { + if (idpSetting.loginResponseTemplate && customTagReplacement) { const template = customTagReplacement(idpSetting.loginResponseTemplate.context); rawSamlResponse = get(template, 'context', null); } else { @@ -215,7 +215,7 @@ function base64LogoutRequest(user, referenceTagXPath, entity, customTagReplaceme let id: string = ''; if (metadata && metadata.init && metadata.target) { let rawSamlRequest: string; - if (initSetting.logoutRequestTemplate) { + if (initSetting.logoutRequestTemplate && customTagReplacement) { const template = customTagReplacement(initSetting.logoutRequestTemplate.context); id = get(template, 'id', null); rawSamlRequest = get(template, 'context', null); diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts index 14fac0d3..30035ccb 100644 --- a/src/binding-redirect.ts +++ b/src/binding-redirect.ts @@ -62,7 +62,7 @@ function buildRedirectURL(opts: BuildRedirectConfig) { 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, null, entitySetting.requestSignatureAlgorithm))); + return baseUrl + pvPair(queryParam, octetString, noParams) + pvPair(urlParams.signature, encodeURIComponent(libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm))); } return baseUrl + pvPair(queryParam, samlRequest + relayState, noParams); } @@ -81,7 +81,7 @@ function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacem if (metadata && metadata.idp && metadata.sp) { const base = metadata.idp.getSingleSignOnService(binding.redirect); let rawSamlRequest: string; - if (spSetting.loginRequestTemplate) { + if (spSetting.loginRequestTemplate && customTagReplacement) { const info = customTagReplacement(spSetting.loginRequestTemplate); id = get(info, 'id', null); rawSamlRequest = get(info, 'context', null); @@ -136,7 +136,7 @@ function logoutRequestRedirectURL(user, entity, relayState?: string, customTagRe NameID: user.logoutNameID, SessionIndex: user.sessionIndex, }; - if (initSetting.logoutRequestTemplate) { + if (initSetting.logoutRequestTemplate && customTagReplacement) { const info = customTagReplacement(initSetting.logoutRequestTemplate, requiredTags); id = get(info, 'id', null); rawSamlRequest = get(info, 'context', null); @@ -173,7 +173,7 @@ function logoutResponseRedirectURL(requestInfo: any, entity: any, relayState?: s if (metadata && metadata.init && metadata.target) { const base = metadata.target.getSingleLogoutService(binding.redirect); let rawSamlResponse: string; - if (initSetting.logoutResponseTemplate) { + if (initSetting.logoutResponseTemplate && customTagReplacement) { const template = customTagReplacement(initSetting.logoutResponseTemplate); id = get(template, 'id', null); rawSamlResponse = get(template, 'context', null); diff --git a/src/entity-idp.ts b/src/entity-idp.ts index 2a53787d..01634caf 100644 --- a/src/entity-idp.ts +++ b/src/entity-idp.ts @@ -15,6 +15,7 @@ import { namespace } from './urn'; import postBinding from './binding-post'; import { flow, FlowResult } from './flow'; import { isString } from './utility'; +import { BindingContext } from './entity'; /** * Identity prvider can be configured using either metadata importing or idpSetting @@ -46,7 +47,7 @@ export class IdentityProvider extends Entity { }; entitySetting.loginResponseTemplate = { ...entitySetting.loginResponseTemplate, - context: libsaml.replaceTagsByValue(entitySetting.loginResponseTemplate.context, replacement), + context: libsaml.replaceTagsByValue(entitySetting.loginResponseTemplate!.context, replacement), }; } else { console.warn('Invalid login response template'); @@ -69,7 +70,7 @@ export class IdentityProvider extends Entity { requestInfo: { [key: string]: any }, binding: string, user: { [key: string]: any }, - customTagReplacement?: (...args: any[]) => any, + customTagReplacement?: (template: string) => BindingContext, encryptThenSign?: boolean, ) { const protocol = namespace.binding[binding]; diff --git a/src/entity-sp.ts b/src/entity-sp.ts index d935fde3..e3df1c21 100644 --- a/src/entity-sp.ts +++ b/src/entity-sp.ts @@ -55,7 +55,7 @@ export class ServiceProvider extends Entity { public createLoginRequest( idp: IdentityProvider, binding = 'redirect', - customTagReplacement?: (...args: any[]) => any, + customTagReplacement?: (template: string) => BindingContext, ): BindingContext | PostBindingContext { const nsBinding = namespace.binding; const protocol = nsBinding[binding]; diff --git a/src/extractor.ts b/src/extractor.ts index cdab3fde..5b14057f 100644 --- a/src/extractor.ts +++ b/src/extractor.ts @@ -1,6 +1,6 @@ import { DOMParser } from 'xmldom'; -import { select } from 'xpath'; -import { uniq, last, zipObject } from './utility'; +import { select, SelectedValue } from 'xpath'; +import { uniq, last, zipObject, notEmpty } from './utility'; import camelCase from 'camelcase'; const dom = DOMParser; @@ -219,7 +219,7 @@ export function extract(context: string, fields) { return { ...result, - [key]: uniq(select(multiXPaths, targetDoc).map((n: Node) => n.nodeValue)) + [key]: uniq(select(multiXPaths, targetDoc).map((n: Node) => n.nodeValue).filter(notEmpty)) }; } // eo special case: multiple path @@ -285,7 +285,7 @@ export function extract(context: string, fields) { */ if (isEntire) { const node = select(baseXPath, targetDoc); - let value = null; + let value: string | string[] | null = null; if (node.length === 1) { value = node[0].toString(); } @@ -347,14 +347,14 @@ export function extract(context: string, fields) { } */ if (attributes.length === 0) { - let attributeValue = null; + let attributeValue: SelectedValue[] | Array<(string | null)> | null = null; const node = select(baseXPath, targetDoc); if (node.length === 1) { const fullPath = `string(${baseXPath}${attributeXPath})`; attributeValue = select(fullPath, targetDoc); } if (node.length > 1) { - attributeValue = node.map((n: Node) => n.firstChild.nodeValue); + attributeValue = node.map((n: Node) => n.firstChild!.nodeValue); } return { ...result, diff --git a/src/flow.ts b/src/flow.ts index 34e185a8..bd9ed909 100644 --- a/src/flow.ts +++ b/src/flow.ts @@ -80,7 +80,7 @@ async function redirectFlow(options) { const extractorFields = getDefaultExtractorFields(parserType); - const parseResult: { samlContent: string, extract: any, sigAlg: string } = { + const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = { samlContent: xmlString, sigAlg: null, extract: extract(xmlString, extractorFields), @@ -135,7 +135,7 @@ async function postFlow(options): Promise { const decryptRequired = from.entitySetting.isAssertionEncrypted; - let extractorFields = []; + let extractorFields: ExtractorFields = []; // validate the xml first await libsaml.isValidXml(samlContent); diff --git a/src/libsaml.ts b/src/libsaml.ts index 385b08ef..23866729 100644 --- a/src/libsaml.ts +++ b/src/libsaml.ts @@ -7,13 +7,13 @@ import { DOMParser } from 'xmldom'; import utility, { flattenDeep, isString } from './utility'; import { algorithms, wording, namespace } from './urn'; -import { select } from 'xpath'; +import { select, SelectedValue } from 'xpath'; import { MetadataInterface } from './metadata'; import * as nrsa from 'node-rsa'; import { SignedXml, FileKeyInfo } from 'xml-crypto'; import * as xmlenc from '@authenio/xml-encryption'; import { extract } from './extractor'; -import { getValidatorModule } from './schema-validator'; +import { getValidatorModule, SchemaValidator } from './schema-validator'; import camelCase from 'camelcase'; const signatureAlgorithms = algorithms.signature; @@ -158,9 +158,11 @@ const libSaml = () => { * @return {string/null} signing algorithm short-hand for the module node-rsa */ function getSigningScheme(sigAlg?: string): string | null { - const algAlias = nrsaAliasMapping[sigAlg]; - if (!(algAlias === undefined)) { - return algAlias; + if (sigAlg) { + const algAlias = nrsaAliasMapping[sigAlg]; + if (!(algAlias === undefined)) { + return algAlias; + } } return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1]; // default value } @@ -312,8 +314,8 @@ const libSaml = () => { const wrappingElementsXPath = "/*[contains(local-name(), 'Response')]/*[local-name(.)='Assertion']/*[local-name(.)='Subject']/*[local-name(.)='SubjectConfirmation']/*[local-name(.)='SubjectConfirmationData']//*[local-name(.)='Assertion' or local-name(.)='Signature']"; // select the signature node - let selection = []; - let assertionNode = null; + let selection: any = []; + let assertionNode: string | null = null; const messageSignatureNode = select(messageSignatureXpath, doc); const assertionSignatureNode = select(assertionSignatureXpath, doc); const wrappingElementNode = select(wrappingElementsXPath, doc); @@ -510,7 +512,7 @@ const libSaml = () => { * @param {string} xml response in xml string format * @return {Promise} a promise to resolve the finalized xml */ - encryptAssertion(sourceEntity, targetEntity, xml: string) { + encryptAssertion(sourceEntity, targetEntity, xml?: string) { // Implement encryption after signature if it has return new Promise((resolve, reject) => { @@ -599,7 +601,7 @@ const libSaml = () => { */ async isValidXml(input: string) { try { - await mod.validate(input); + await mod!.validate(input); return Promise.resolve(); } catch (e) { throw e; @@ -609,7 +611,7 @@ const libSaml = () => { }; // load the validator module before the function runtime -let mod = null; +let mod: SchemaValidator | null = null; (async () => mod = await getValidatorModule())(); export default libSaml(); diff --git a/src/metadata-sp.ts b/src/metadata-sp.ts index a1e3072a..5cafe0aa 100644 --- a/src/metadata-sp.ts +++ b/src/metadata-sp.ts @@ -81,19 +81,19 @@ export class SpMetadata extends Metadata { } if (signingCert) { - descriptors.KeyDescriptor.push(libsaml.createKeySection('signing', signingCert).KeyDescriptor); + descriptors.KeyDescriptor!.push(libsaml.createKeySection('signing', signingCert).KeyDescriptor); } else { //console.warn('Construct service provider - missing signing certificate'); } if (encryptCert) { - descriptors.KeyDescriptor.push(libsaml.createKeySection('encryption', encryptCert).KeyDescriptor); + descriptors.KeyDescriptor!.push(libsaml.createKeySection('encryption', encryptCert).KeyDescriptor); } else { //console.warn('Construct service provider - missing encrypt certificate'); } if (isNonEmptyArray(nameIDFormat)) { - nameIDFormat.forEach(f => descriptors.NameIDFormat.push(f)); + nameIDFormat.forEach(f => descriptors.NameIDFormat!.push(f)); } if (isNonEmptyArray(singleLogoutService)) { @@ -107,7 +107,7 @@ export class SpMetadata extends Metadata { if (a.isDefault) { attr.isDefault = true; } - descriptors.SingleLogoutService.push([{ _attr: attr }]); + descriptors.SingleLogoutService!.push([{ _attr: attr }]); }); } @@ -122,7 +122,7 @@ export class SpMetadata extends Metadata { if (a.isDefault) { attr.isDefault = true; } - descriptors.AssertionConsumerService.push([{ _attr: attr }]); + descriptors.AssertionConsumerService!.push([{ _attr: attr }]); }); } else { // console.warn('Missing endpoint of AssertionConsumerService'); diff --git a/src/metadata.ts b/src/metadata.ts index 58848f13..41836383 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -28,7 +28,7 @@ export default class Metadata implements MetadataInterface { * @param {string | Buffer} metadata xml * @param {object} extraParse for custom metadata extractor */ - constructor(xml: string | Buffer, extraParse = []) { + constructor(xml: string | Buffer, extraParse: any = []) { this.xmlString = xml.toString(); this.meta = extract(this.xmlString, extraParse.concat([ { @@ -134,7 +134,7 @@ export default class Metadata implements MetadataInterface { * @return {string/object} location */ public getSingleLogoutService(binding: string | undefined): string | object { - if (isString(binding)) { + if (binding && isString(binding)) { const bindType = namespace.binding[binding]; let singleLogoutService = this.meta.singleLogoutService; if (!(singleLogoutService instanceof Array)) { diff --git a/src/schema-validator.ts b/src/schema-validator.ts index b7055b15..4a05aaa5 100644 --- a/src/schema-validator.ts +++ b/src/schema-validator.ts @@ -7,7 +7,7 @@ enum SchemaValidators { XMLLINT = 'node-xmllint' } -interface SchemaValidator { +export interface SchemaValidator { validate: (xml: string) => Promise; } @@ -26,7 +26,7 @@ const getValidatorModule: GetValidatorModuleSpec = async () => { const selectedValidator: string = moduleResolver(SchemaValidators.JAVAC) || moduleResolver(SchemaValidators.LIBXML) - || moduleResolver(SchemaValidators.XMLLINT); + || moduleResolver(SchemaValidators.XMLLINT) || ""; const xsd = 'saml-schema-protocol-2.0.xsd'; diff --git a/src/utility.ts b/src/utility.ts index 0abd73c3..73f6d4da 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -168,7 +168,7 @@ function getPublicKeyPemFromCertificate(x509Certificate: string) { * @return {string} string in pem format * If passphrase is used to protect the .pem content (recommend) */ -export function readPrivateKey(keyString: string | Buffer, passphrase: string, isOutputString?: boolean) { +export function readPrivateKey(keyString: string | Buffer, passphrase: string | undefined, isOutputString?: boolean) { return isString(passphrase) ? this.convertToString(pki.privateKeyToPem(pki.decryptRsaPrivateKey(String(keyString), passphrase)), isOutputString) : keyString; } /** @@ -184,6 +184,10 @@ export function isNonEmptyArray(a) { return Array.isArray(a) && a.length > 0; } +export function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + const utility = { isString, base64Encode, diff --git a/src/validator.ts b/src/validator.ts index 98891167..04cb1449 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -5,8 +5,8 @@ function verifyTime(utcNotBefore?: string, utcNotOnOrAfter?: string): boolean { return true; // throw exception todo } - let notBeforeLocal = null; - let notOnOrAfterLocal = null; + let notBeforeLocal: Date | null = null; + let notOnOrAfterLocal: Date | null = null; if (utcNotBefore && !utcNotOnOrAfter) { notBeforeLocal = new Date(utcNotBefore); @@ -17,8 +17,8 @@ function verifyTime(utcNotBefore?: string, utcNotOnOrAfter?: string): boolean { return now < notOnOrAfterLocal; } - notBeforeLocal = new Date(utcNotBefore); - notOnOrAfterLocal = new Date(utcNotOnOrAfter); + notBeforeLocal = new Date(utcNotBefore!); + notOnOrAfterLocal = new Date(utcNotOnOrAfter!); return +notBeforeLocal <= +now && now < notOnOrAfterLocal; } diff --git a/test/flow.ts b/test/flow.ts index f57b30e8..dc66b701 100644 --- a/test/flow.ts +++ b/test/flow.ts @@ -478,7 +478,7 @@ test('send login response with [custom template] encrypted signed assertion + si test('idp sends a redirect logout request without signature and sp parses it', async t => { const { id, context } = idp.createLogoutRequest(sp, 'redirect', { logoutNameID: 'user@esaml2.com' }); const query = url.parse(context).query; - t.is(query.includes('SAMLRequest='), true); + t.is(query!.includes('SAMLRequest='), true); t.is(typeof id, 'string'); t.is(typeof context, 'string'); const originalURL = url.parse(context, true); @@ -497,9 +497,9 @@ test('idp sends a redirect logout request without signature and sp parses it', a test('idp sends a redirect logout request with signature and sp parses it', async t => { const { id, context } = idp.createLogoutRequest(spWantLogoutReqSign, 'redirect', { logoutNameID: 'user@esaml2.com' }); const query = url.parse(context).query; - t.is(query.includes('SAMLRequest='), true); - t.is(query.includes('SigAlg='), true); - t.is(query.includes('Signature='), true); + t.is(query!.includes('SAMLRequest='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); t.is(typeof id, 'string'); t.is(typeof context, 'string'); const originalURL = url.parse(context, true); diff --git a/test/index.ts b/test/index.ts index 07ae02e7..a611ecfc 100644 --- a/test/index.ts +++ b/test/index.ts @@ -160,10 +160,10 @@ test('getAssertionConsumerService with two bindings', t => { t.is(libsaml.constructMessageSignature(octetString, _spPrivPem, _spPrivKeyPass).toString('base64'), signatureB64SHA1); }); test('sign a SAML message with RSA-SHA256', t => { - t.is(libsaml.constructMessageSignature(octetStringSHA256, _spPrivPem, _spPrivKeyPass, null, signatureAlgorithms.RSA_SHA256).toString('base64'), signatureB64SHA256); + t.is(libsaml.constructMessageSignature(octetStringSHA256, _spPrivPem, _spPrivKeyPass, undefined, signatureAlgorithms.RSA_SHA256).toString('base64'), signatureB64SHA256); }); test('sign a SAML message with RSA-SHA512', t => { - t.is(libsaml.constructMessageSignature(octetStringSHA512, _spPrivPem, _spPrivKeyPass, null, signatureAlgorithms.RSA_SHA512).toString('base64'), signatureB64SHA512); + t.is(libsaml.constructMessageSignature(octetStringSHA512, _spPrivPem, _spPrivKeyPass, undefined, signatureAlgorithms.RSA_SHA512).toString('base64'), signatureB64SHA512); }); test('verify binary SAML message signed with RSA-SHA1', t => { const signature = libsaml.constructMessageSignature(octetString, _spPrivPem, _spPrivKeyPass, false); diff --git a/tsconfig.json b/tsconfig.json index 50eba9ac..740e9394 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "outDir": "./build", "baseUrl": "./", "removeComments": false, + "strictNullChecks": true, "paths": {}, "lib": [ "dom",