diff --git a/docs/idp-configuration.md b/docs/idp-configuration.md index 5a5891c3..e6ec6dde 100644 --- a/docs/idp-configuration.md +++ b/docs/idp-configuration.md @@ -57,7 +57,7 @@ const idp = new IdentityProvider({ - **tagPrefix: {[key: TagPrefixKey]: string}**
Declare the tag of specific xml document node. `TagPrefixKey` currently supports `encryptedAssertion` only. (See more [#220](https://github.com/tngan/samlify/issues/220)) -- **loginResponseTemplate: {context: String, attributes: Attributes}**
+- **loginResponseTemplate: {context: String, attributes: Attributes, additionalTemplates: LoginResponseAdditionalTemplates}**
Customize the login response template, and user can reuse it in the callback function to do runtime interpolation. (See [more](/template)) - **wantLogoutResponseSigned: Boolean**
diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts index 1ac37ac0..c4adc01b 100644 --- a/src/binding-redirect.ts +++ b/src/binding-redirect.ts @@ -114,6 +114,105 @@ function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacem } throw new Error('ERR_GENERATE_REDIRECT_LOGIN_REQUEST_MISSING_METADATA'); } + +/** +* @desc Redirect URL for login response +* @param {object} requestInfo corresponding request, used to obtain the id +* @param {object} entity object includes both idp and sp +* @param {object} user current logged user (e.g. req.user) +* @param {String} relayState the relaystate sent by sp corresponding request +* @param {function} customTagReplacement used when developers have their own login response template +*/ +function loginResponseRedirectURL(requestInfo: any, entity: any, user: any = {}, relayState?: string, customTagReplacement?: (template: string) => BindingContext): BindingContext { + const idpSetting = entity.idp.entitySetting; + const spSetting = entity.sp.entitySetting; + const metadata = { + idp: entity.idp.entityMeta, + sp: entity.sp.entityMeta, + }; + + let id: string = idpSetting.generateID(); + if (metadata && metadata.idp && metadata.sp) { + const base = metadata.sp.getAssertionConsumerService(binding.redirect); + let rawSamlResponse: string; + // + const nameIDFormat = idpSetting.nameIDFormat; + const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat; + const nowTime = new Date(); + // Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds) + const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000 ); + const tvalue: any = { + ID: id, + AssertionID: idpSetting.generateID(), + Destination: base, + SubjectRecipient: base, + Issuer: metadata.idp.getEntityID(), + Audience: metadata.sp.getEntityID(), + EntityID: metadata.sp.getEntityID(), + IssueInstant: nowTime.toISOString(), + AssertionConsumerServiceURL: base, + StatusCode: namespace.statusCode.success, + // can be customized + ConditionsNotBefore: nowTime.toISOString(), + ConditionsNotOnOrAfter: fiveMinutesLaterTime.toISOString(), + SubjectConfirmationDataNotOnOrAfter: fiveMinutesLaterTime.toISOString(), + NameIDFormat: selectedNameIDFormat, + NameID: user.email || '', + InResponseTo: get(requestInfo, 'extract.request.id', ''), + AuthnStatement: '', + AttributeStatement: '', + }; + + if (idpSetting.loginResponseTemplate && customTagReplacement) { + const template = customTagReplacement(idpSetting.loginResponseTemplate.context); + id = get(template, 'id', null); + rawSamlResponse = get(template, 'context', null); + } else { + + if (requestInfo !== null) { + tvalue.InResponseTo = requestInfo.extract.request.id; + } + rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue); + } + + const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting; + const config = { + privateKey, + privateKeyPass, + signatureAlgorithm, + signingCert: metadata.idp.getX509Certificate('signing'), + isBase64Output: false, + }; + // step: sign assertion ? -> encrypted ? -> sign message ? + if (metadata.sp.isWantAssertionsSigned()) { + rawSamlResponse = libsaml.constructSAMLSignature({ + ...config, + rawSamlMessage: rawSamlResponse, + transformationAlgorithms: spSetting.transformationAlgorithms, + referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']", + signatureConfig: { + prefix: 'ds', + location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", action: 'after' }, + }, + }); + } + + // Like in post binding, SAML response is always signed + return { + id, + context: buildRedirectURL({ + baseUrl: base, + type: urlParams.samlResponse, + isSigned: true, + context: rawSamlResponse, + entitySetting: idpSetting, + relayState, + }), + }; + } + throw new Error('ERR_GENERATE_REDIRECT_LOGIN_RESPONSE_MISSING_METADATA'); +} + /** * @desc Redirect URL for logout request * @param {object} user current logged user (e.g. req.user) @@ -127,7 +226,7 @@ function logoutRequestRedirectURL(user, entity, relayState?: string, customTagRe let id: string = initSetting.generateID(); const nameIDFormat = initSetting.nameIDFormat; const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat; - + if (metadata && metadata.init && metadata.target) { const base = metadata.target.getSingleLogoutService(binding.redirect); let rawSamlRequest: string = ''; @@ -213,6 +312,7 @@ function logoutResponseRedirectURL(requestInfo: any, entity: any, relayState?: s const redirectBinding = { loginRequestRedirectURL, + loginResponseRedirectURL, logoutRequestRedirectURL, logoutResponseRedirectURL, }; diff --git a/src/binding-simplesign.ts b/src/binding-simplesign.ts new file mode 100644 index 00000000..a5af5535 --- /dev/null +++ b/src/binding-simplesign.ts @@ -0,0 +1,225 @@ +/** +* @file binding-simplesign.ts +* @author Orange +* @desc Binding-level API, declare the functions using POST SimpleSign binding +*/ + +import { wording, StatusCode } from './urn'; +import { BindingContext, SimpleSignComputedContext } from './entity'; +import libsaml from './libsaml'; +import utility, { get } from './utility'; + +const binding = wording.binding; +const urlParams = wording.urlParams; + +export interface BuildSimpleSignConfig { + type: string; + context: string; + entitySetting: any; + relayState?: string; +} + +export interface BindingSimpleSignContext { + id: string; + context: string; + signature: any; + 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 Refractored part of simple signature generation for login/logout request +* @param {string} type +* @param {string} rawSamlRequest +* @param {object} entitySetting +* @return {string} +*/ +function buildSimpleSignature(opts: BuildSimpleSignConfig) : string { + const { + type, + context, + entitySetting, + } = opts; + let { relayState = '' } = opts; + const queryParam = libsaml.getQueryParamByType(type); + + if (relayState !== '') { + relayState = pvPair(urlParams.relayState, relayState); + } + + const sigAlg = pvPair(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm); + const octetString = context + relayState + sigAlg; + return libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm); +} + +/** +* @desc Generate a base64 encoded login request +* @param {string} referenceTagXPath reference uri +* @param {object} entity object includes both idp and sp +* @param {function} customTagReplacement used when developers have their own login response template +*/ +function base64LoginRequest(entity: any, customTagReplacement?: (template: string) => BindingContext): SimpleSignComputedContext { + const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta }; + const spSetting = entity.sp.entitySetting; + let id: string = ''; + + if (metadata && metadata.idp && metadata.sp) { + const base = metadata.idp.getSingleSignOnService(binding.simpleSign); + let rawSamlRequest: string; + if (spSetting.loginRequestTemplate && customTagReplacement) { + const info = customTagReplacement(spSetting.loginRequestTemplate.context); + id = get(info, 'id', null); + rawSamlRequest = get(info, 'context', null); + } else { + const nameIDFormat = spSetting.nameIDFormat; + const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat; + id = spSetting.generateID(); + rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, { + ID: id, + Destination: base, + Issuer: metadata.sp.getEntityID(), + IssueInstant: new Date().toISOString(), + AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.simpleSign), + EntityID: metadata.sp.getEntityID(), + AllowCreate: spSetting.allowCreate, + NameIDFormat: selectedNameIDFormat + } as any); + } + + let simpleSignatureContext : any = null; + if (metadata.idp.isWantAuthnRequestsSigned()) { + const simpleSignature = buildSimpleSignature({ + type: urlParams.samlRequest, + context: rawSamlRequest, + entitySetting: spSetting, + relayState: spSetting.relayState, + }); + + simpleSignatureContext = { + signature: simpleSignature, + sigAlg: spSetting.requestSignatureAlgorithm, + }; + } + // No need to embeded XML signature + return { + id, + context: utility.base64Encode(rawSamlRequest), + ...simpleSignatureContext, + }; + } + throw new Error('ERR_GENERATE_POST_SIMPLESIGN_LOGIN_REQUEST_MISSING_METADATA'); +} +/** +* @desc Generate a base64 encoded login response +* @param {object} requestInfo corresponding request, used to obtain the id +* @param {object} entity object includes both idp and sp +* @param {object} user current logged user (e.g. req.user) +* @param {string} relayState the relay state +* @param {function} customTagReplacement used when developers have their own login response template +*/ +async function base64LoginResponse(requestInfo: any = {}, entity: any, user: any = {}, relayState?: string, customTagReplacement?: (template: string) => BindingContext): Promise { + const idpSetting = entity.idp.entitySetting; + const spSetting = entity.sp.entitySetting; + const id = idpSetting.generateID(); + const metadata = { + idp: entity.idp.entityMeta, + sp: entity.sp.entityMeta, + }; + const nameIDFormat = idpSetting.nameIDFormat; + const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat; + if (metadata && metadata.idp && metadata.sp) { + const base = metadata.sp.getAssertionConsumerService(binding.simpleSign); + let rawSamlResponse: string; + const nowTime = new Date(); + // Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds) + const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000 ); + const tvalue: any = { + ID: id, + AssertionID: idpSetting.generateID(), + Destination: base, + Audience: metadata.sp.getEntityID(), + EntityID: metadata.sp.getEntityID(), + SubjectRecipient: base, + Issuer: metadata.idp.getEntityID(), + IssueInstant: nowTime.toISOString(), + AssertionConsumerServiceURL: base, + StatusCode: StatusCode.Success, + // can be customized + ConditionsNotBefore: nowTime.toISOString(), + ConditionsNotOnOrAfter: fiveMinutesLaterTime.toISOString(), + SubjectConfirmationDataNotOnOrAfter: fiveMinutesLaterTime.toISOString(), + NameIDFormat: selectedNameIDFormat, + NameID: user.email || '', + InResponseTo: get(requestInfo, 'extract.request.id', ''), + AuthnStatement: '', + AttributeStatement: '', + }; + if (idpSetting.loginResponseTemplate && customTagReplacement) { + const template = customTagReplacement(idpSetting.loginResponseTemplate.context); + rawSamlResponse = get(template, 'context', null); + } else { + if (requestInfo !== null) { + tvalue.InResponseTo = requestInfo.extract.request.id; + } + rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue); + } + const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting; + const config = { + privateKey, + privateKeyPass, + signatureAlgorithm, + signingCert: metadata.idp.getX509Certificate('signing'), + isBase64Output: false, + }; + // step: sign assertion ? -> encrypted ? -> sign message ? + if (metadata.sp.isWantAssertionsSigned()) { + rawSamlResponse = libsaml.constructSAMLSignature({ + ...config, + rawSamlMessage: rawSamlResponse, + transformationAlgorithms: spSetting.transformationAlgorithms, + referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']", + signatureConfig: { + prefix: 'ds', + location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", action: 'after' }, + }, + }); + } + + // SAML response must be signed sign message first, then encrypt + let simpleSignature: string = ''; + // like in post and redirect bindings, login response is always signed. + simpleSignature = buildSimpleSignature({ + type: urlParams.samlResponse, + context: rawSamlResponse, + entitySetting: idpSetting, + relayState: relayState, + } ); + + return Promise.resolve({ + id, + context: utility.base64Encode(rawSamlResponse), + signature: simpleSignature, + sigAlg: idpSetting.requestSignatureAlgorithm, + }); + + } + throw new Error('ERR_GENERATE_POST_SIMPLESIGN_LOGIN_RESPONSE_MISSING_METADATA'); +} + +const simpleSignBinding = { + base64LoginRequest, + base64LoginResponse, + }; + +export default simpleSignBinding; diff --git a/src/entity-idp.ts b/src/entity-idp.ts index 01634caf..f4705939 100644 --- a/src/entity-idp.ts +++ b/src/entity-idp.ts @@ -13,6 +13,8 @@ import { import libsaml from './libsaml'; import { namespace } from './urn'; import postBinding from './binding-post'; +import redirectBinding from './binding-redirect'; +import simpleSignBinding from './binding-simplesign'; import { flow, FlowResult } from './flow'; import { isString } from './utility'; import { BindingContext } from './entity'; @@ -28,7 +30,7 @@ export default function(props: IdentityProviderSettings) { * Identity prvider can be configured using either metadata importing or idpSetting */ export class IdentityProvider extends Entity { - + entityMeta: IdentityProviderMetadata; constructor(idpSetting: IdentityProviderSettings) { @@ -42,8 +44,20 @@ export class IdentityProvider extends Entity { // build attribute part if (idpSetting.loginResponseTemplate) { if (isString(idpSetting.loginResponseTemplate.context) && Array.isArray(idpSetting.loginResponseTemplate.attributes)) { + let attributeStatementTemplate; + let attributeTemplate; + if (!idpSetting.loginResponseTemplate.additionalTemplates || !idpSetting.loginResponseTemplate.additionalTemplates!.attributeStatementTemplate) { + attributeStatementTemplate = libsaml.defaultAttributeStatementTemplate; + } else { + attributeStatementTemplate = idpSetting.loginResponseTemplate.additionalTemplates!.attributeStatementTemplate!; + } + if (!idpSetting.loginResponseTemplate.additionalTemplates || !idpSetting.loginResponseTemplate.additionalTemplates!.attributeTemplate) { + attributeTemplate = libsaml.defaultAttributeTemplate; + } else { + attributeTemplate = idpSetting.loginResponseTemplate.additionalTemplates!.attributeTemplate!; + } const replacement = { - AttributeStatement: libsaml.attributeStatementBuilder(idpSetting.loginResponseTemplate.attributes), + AttributeStatement: libsaml.attributeStatementBuilder(idpSetting.loginResponseTemplate.attributes, attributeTemplate, attributeStatementTemplate), }; entitySetting.loginResponseTemplate = { ...entitySetting.loginResponseTemplate, @@ -64,6 +78,7 @@ export class IdentityProvider extends Entity { * @param user current logged user (e.g. req.user) * @param customTagReplacement used when developers have their own login response template * @param encryptThenSign whether or not to encrypt then sign first (if signing) + * @param relayState the relayState from corresponding request */ public async createLoginResponse( sp: ServiceProvider, @@ -72,21 +87,41 @@ export class IdentityProvider extends Entity { user: { [key: string]: any }, customTagReplacement?: (template: string) => BindingContext, encryptThenSign?: boolean, + relayState?: string, ) { const protocol = namespace.binding[binding]; - // can only support post binding for login response - if (protocol === namespace.binding.post) { - const context = await postBinding.base64LoginResponse(requestInfo, { - idp: this, - sp, - }, user, customTagReplacement, encryptThenSign); - return { - ...context, - entityEndpoint: (sp.entityMeta as ServiceProviderMetadata).getAssertionConsumerService(binding), - type: 'SAMLResponse' - }; + // can support post, redirect and post simple sign bindings for login response + let context: any = null; + switch (protocol) { + case namespace.binding.post: + context = await postBinding.base64LoginResponse(requestInfo, { + idp: this, + sp, + }, user, customTagReplacement, encryptThenSign); + break; + + case namespace.binding.simpleSign: + context = await simpleSignBinding.base64LoginResponse( requestInfo, { + idp: this, sp, + }, user, relayState, customTagReplacement); + break; + + case namespace.binding.redirect: + return redirectBinding.loginResponseRedirectURL(requestInfo, { + idp: this, + sp, + }, user, relayState, customTagReplacement); + + default: + throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING'); } - throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING'); + + return { + ...context, + relayState, + entityEndpoint: (sp.entityMeta as ServiceProviderMetadata).getAssertionConsumerService(binding) as string, + type: 'SAMLResponse' + }; } /** @@ -104,7 +139,7 @@ export class IdentityProvider extends Entity { parserType: 'SAMLRequest', type: 'login', binding: binding, - request: req + request: req }); } } diff --git a/src/entity-sp.ts b/src/entity-sp.ts index c89e33bc..454d5664 100644 --- a/src/entity-sp.ts +++ b/src/entity-sp.ts @@ -7,6 +7,7 @@ import Entity, { BindingContext, PostBindingContext, ESamlHttpRequest, + SimpleSignBindingContext, } from './entity'; import { IdentityProviderConstructor as IdentityProvider, @@ -16,6 +17,7 @@ import { import { namespace } from './urn'; import redirectBinding from './binding-redirect'; import postBinding from './binding-post'; +import simpleSignBinding from './binding-simplesign'; import { flow, FlowResult } from './flow'; /* @@ -56,28 +58,38 @@ export class ServiceProvider extends Entity { idp: IdentityProvider, binding = 'redirect', customTagReplacement?: (template: string) => BindingContext, - ): BindingContext | PostBindingContext { + ): BindingContext | PostBindingContext| SimpleSignBindingContext { const nsBinding = namespace.binding; const protocol = nsBinding[binding]; if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) { throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG'); } - if (protocol === nsBinding.redirect) { - return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement); - } + let context: any = null; + switch (protocol) { + case nsBinding.redirect: + return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement); - if (protocol === nsBinding.post) { - const context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement); - return { - ...context, - relayState: this.entitySetting.relayState, - entityEndpoint: idp.entityMeta.getSingleSignOnService(binding) as string, - type: 'SAMLRequest', - }; - } - // Will support artifact in the next release - throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING'); + case nsBinding.post: + context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement); + break; + + case nsBinding.simpleSign: + // Object context = {id, context, signature, sigAlg} + context = simpleSignBinding.base64LoginRequest( { idp, sp: this }, customTagReplacement); + break; + + default: + // Will support artifact in the next release + throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING'); + } + + return { + ...context, + relayState: this.entitySetting.relayState, + entityEndpoint: idp.entityMeta.getSingleSignOnService(binding) as string, + type: 'SAMLRequest', + }; } /** diff --git a/src/entity.ts b/src/entity.ts index 28da535f..5f7436ca 100644 --- a/src/entity.ts +++ b/src/entity.ts @@ -48,6 +48,17 @@ export interface PostBindingContext extends BindingContext { type: string; } +export interface SimpleSignBindingContext extends PostBindingContext { + sigAlg?: string; + signature?: string; + keyInfo?: string; +} + +export interface SimpleSignComputedContext extends BindingContext { + sigAlg?: string; + signature?: string; +} + export interface ParseResult { samlContent: string; extract: any; diff --git a/src/flow.ts b/src/flow.ts index 7750563b..4b016741 100644 --- a/src/flow.ts +++ b/src/flow.ts @@ -1,4 +1,4 @@ -import { inflateString, base64Decode } from './utility'; +import { inflateString, base64Decode, isNonEmptyArray } from './utility'; import { verifyTime } from './validator'; import libsaml from './libsaml'; import { @@ -19,6 +19,7 @@ import { MessageSignatureOrder, StatusCode } from './urn'; +import simpleSignBinding from './binding-simplesign'; const bindDict = wording.binding; const urlParams = wording.urlParams; @@ -26,6 +27,7 @@ const urlParams = wording.urlParams; export interface FlowResult { samlContent: string; extract: any; + sigAlg?: string|null ; } // get the default extractor fields based on the parserType @@ -49,9 +51,9 @@ function getDefaultExtractorFields(parserType: ParserType, assertion?: any): Ext } // proceed the redirect binding flow -async function redirectFlow(options) { +async function redirectFlow(options): Promise { - const { request, parserType, checkSignature = true, from } = options; + const { request, parserType, self, checkSignature = true, from } = options; const { query, octetString } = request; const { SigAlg: sigAlg, Signature: signature } = query; @@ -68,20 +70,32 @@ async function redirectFlow(options) { const xmlString = inflateString(decodeURIComponent(content)); - // validate the xml (remarks: login response must be gone through post flow) - if ( - parserType === urlParams.samlRequest || - parserType === urlParams.logoutRequest || - parserType === urlParams.logoutResponse - ) { - try { - await libsaml.isValidXml(xmlString); - } catch (e) { - return Promise.reject('ERR_INVALID_XML'); + // validate the xml + try { + await libsaml.isValidXml(xmlString); + } catch (e) { + return Promise.reject('ERR_INVALID_XML'); + } + + // check status based on different scenarios + await checkStatus(xmlString, parserType); + + let assertion: string = ''; + + if (parserType === urlParams.samlResponse){ + // Extract assertion shortcut + const verifiedDoc = extract(xmlString, [{ + key: 'assertion', + localPath: ['~Response', 'Assertion'], + attributes: [], + context: true + }]); + if (verifiedDoc && verifiedDoc.assertion){ + assertion = verifiedDoc.assertion as string; } } - const extractorFields = getDefaultExtractorFields(parserType); + const extractorFields = getDefaultExtractorFields(parserType, assertion.length > 0 ? assertion : null); const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = { samlContent: xmlString, @@ -89,9 +103,6 @@ async function redirectFlow(options) { extract: extract(xmlString, extractorFields), }; - // check status based on different scenarios - await checkStatus(xmlString, parserType); - // see if signature check is required // only verify message signature is enough if (checkSignature) { @@ -113,6 +124,49 @@ async function redirectFlow(options) { parseResult.sigAlg = decodeSigAlg; } + /** + * Validation part: validate the context of response after signature is verified and decrpyted (optional) + */ + const issuer = targetEntityMetadata.getEntityID(); + const extractedProperties = parseResult.extract; + + // unmatched issuer + if ( + (parserType === 'LogoutResponse' || parserType === 'SAMLResponse') + && extractedProperties + && extractedProperties.issuer !== issuer + ) { + return Promise.reject('ERR_UNMATCH_ISSUER'); + } + + // invalid session time + // only run the verifyTime when `SessionNotOnOrAfter` exists + if ( + parserType === 'SAMLResponse' + && extractedProperties.sessionIndex.sessionNotOnOrAfter + && !verifyTime( + undefined, + extractedProperties.sessionIndex.sessionNotOnOrAfter, + self.entitySetting.clockDrifts + ) + ) { + return Promise.reject('ERR_EXPIRED_SESSION'); + } + + // invalid time + // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + if ( + parserType === 'SAMLResponse' + && extractedProperties.conditions + && !verifyTime( + extractedProperties.conditions.notBefore, + extractedProperties.conditions.notOnOrAfter, + self.entitySetting.clockDrifts + ) + ) { + return Promise.reject('ERR_SUBJECT_UNCONFIRMED'); + } + return Promise.resolve(parseResult); } @@ -149,7 +203,7 @@ async function postFlow(options): Promise { if (parserType !== urlParams.samlResponse) { extractorFields = getDefaultExtractorFields(parserType, null); } - + // check status based on different scenarios await checkStatus(samlContent, parserType); @@ -238,6 +292,129 @@ async function postFlow(options): Promise { return Promise.resolve(parseResult); } + +// proceed the post simple sign binding flow +async function postSimpleSignFlow(options): Promise { + + const { request, parserType, self, checkSignature = true, from } = options; + + const { body, octetString } = request; + + const targetEntityMetadata = from.entityMeta; + + // ?SAMLRequest= or ?SAMLResponse= + const direction = libsaml.getQueryParamByType(parserType); + const encodedRequest: string = body[direction]; + const sigAlg: string = body['SigAlg']; + const signature: string = body['Signature']; + + // query must contain the saml content + if (encodedRequest === undefined) { + return Promise.reject('ERR_SIMPLESIGN_FLOW_BAD_ARGS'); + } + + const xmlString = String(base64Decode(encodedRequest)); + + // validate the xml + try { + await libsaml.isValidXml(xmlString); + } catch (e) { + return Promise.reject('ERR_INVALID_XML'); + } + + // check status based on different scenarios + await checkStatus(xmlString, parserType); + + let assertion: string = ''; + + if (parserType === urlParams.samlResponse){ + // Extract assertion shortcut + const verifiedDoc = extract(xmlString, [{ + key: 'assertion', + localPath: ['~Response', 'Assertion'], + attributes: [], + context: true + }]); + if (verifiedDoc && verifiedDoc.assertion){ + assertion = verifiedDoc.assertion as string; + } + } + + const extractorFields = getDefaultExtractorFields(parserType, assertion.length > 0 ? assertion : null); + + const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = { + samlContent: xmlString, + sigAlg: null, + extract: extract(xmlString, extractorFields), + }; + + // see if signature check is required + // only verify message signature is enough + if (checkSignature) { + if (!signature || !sigAlg) { + return Promise.reject('ERR_MISSING_SIG_ALG'); + } + + // put the below two assignemnts into verifyMessageSignature function + const base64Signature = Buffer.from(signature, 'base64'); + + const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg); + + if (!verified) { + // Fail to verify message signature + return Promise.reject('ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION'); + } + + parseResult.sigAlg = sigAlg; + } + + /** + * Validation part: validate the context of response after signature is verified and decrpyted (optional) + */ + const issuer = targetEntityMetadata.getEntityID(); + const extractedProperties = parseResult.extract; + + // unmatched issuer + if ( + (parserType === 'LogoutResponse' || parserType === 'SAMLResponse') + && extractedProperties + && extractedProperties.issuer !== issuer + ) { + return Promise.reject('ERR_UNMATCH_ISSUER'); + } + + // invalid session time + // only run the verifyTime when `SessionNotOnOrAfter` exists + if ( + parserType === 'SAMLResponse' + && extractedProperties.sessionIndex.sessionNotOnOrAfter + && !verifyTime( + undefined, + extractedProperties.sessionIndex.sessionNotOnOrAfter, + self.entitySetting.clockDrifts + ) + ) { + return Promise.reject('ERR_EXPIRED_SESSION'); + } + + // invalid time + // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf + if ( + parserType === 'SAMLResponse' + && extractedProperties.conditions + && !verifyTime( + extractedProperties.conditions.notBefore, + extractedProperties.conditions.notOnOrAfter, + self.entitySetting.clockDrifts + ) + ) { + return Promise.reject('ERR_SUBJECT_UNCONFIRMED'); + } + + return Promise.resolve(parseResult); +} + + function checkStatus(content: string, parserType: string): Promise { // only check response parser @@ -269,10 +446,10 @@ export function flow(options): Promise { const binding = options.binding; const parserType = options.parserType; - options.supportBindings = [BindingNamespace.Redirect, BindingNamespace.Post]; - // saml response only allows POST + options.supportBindings = [BindingNamespace.Redirect, BindingNamespace.Post, BindingNamespace.SimpleSign]; + // saml response allows POST, REDIRECT if (parserType === ParserType.SAMLResponse) { - options.supportBindings = [BindingNamespace.Post]; + options.supportBindings = [BindingNamespace.Post, BindingNamespace.Redirect, BindingNamespace.SimpleSign]; } if (binding === bindDict.post) { @@ -283,6 +460,10 @@ export function flow(options): Promise { return redirectFlow(options); } + if (binding === bindDict.simpleSign) { + return postSimpleSignFlow(options); + } + return Promise.reject('ERR_UNEXPECTED_FLOW'); } diff --git a/src/libsaml.ts b/src/libsaml.ts index 4159cbd2..2cf67cec 100644 --- a/src/libsaml.ts +++ b/src/libsaml.ts @@ -58,13 +58,23 @@ export interface LoginResponseAttribute { valueXmlnsXsi?: string; } +export interface LoginResponseAdditionalTemplates { + attributeStatementTemplate?: AttributeStatementTemplate; + attributeTemplate?:AttributeTemplate; +} + export interface BaseSamlTemplate { context: string; } export interface LoginResponseTemplate extends BaseSamlTemplate { attributes?: LoginResponseAttribute[]; + additionalTemplates?: LoginResponseAdditionalTemplates; } +export interface AttributeStatementTemplate extends BaseSamlTemplate { } + +export interface AttributeTemplate extends BaseSamlTemplate { } + export interface LoginRequestTemplate extends BaseSamlTemplate { } export interface LogoutRequestTemplate extends BaseSamlTemplate { } @@ -81,7 +91,7 @@ export interface LibSamlInterface { getQueryParamByType: (type: string) => string; createXPath: (local, isExtractAll?: boolean) => string; replaceTagsByValue: (rawXML: string, tagValues: any) => string; - attributeStatementBuilder: (attributes: LoginResponseAttribute[]) => string; + attributeStatementBuilder: (attributes: LoginResponseAttribute[], attributeTemplate : AttributeTemplate, attributeStatementTemplate : AttributeStatementTemplate) => string; constructSAMLSignature: (opts: SignatureConstructor) => string; verifySignature: (xml: string, opts) => [boolean, any]; createKeySection: (use: KeyUse, cert: string | Buffer) => {}; @@ -97,6 +107,8 @@ export interface LibSamlInterface { nrsaAliasMapping: any; defaultLoginRequestTemplate: LoginRequestTemplate; defaultLoginResponseTemplate: LoginResponseTemplate; + defaultAttributeStatementTemplate: AttributeStatementTemplate; + defaultAttributeTemplate: AttributeTemplate; defaultLogoutRequestTemplate: LogoutRequestTemplate; defaultLogoutResponseTemplate: LogoutResponseTemplate; } @@ -138,6 +150,23 @@ const libSaml = () => { const defaultLogoutRequestTemplate = { context: '{Issuer}{NameID}', }; + + /** + * @desc Default AttributeStatement template + * @type {AttributeStatementTemplate} + */ + const defaultAttributeStatementTemplate = { + context: '{Attributes}', + }; + + /** + * @desc Default Attribute template + * @type {AttributeTemplate} + */ + const defaultAttributeTemplate = { + context: '{Value}', + }; + /** * @desc Default login response template * @type {LoginResponseTemplate} @@ -145,6 +174,10 @@ const libSaml = () => { const defaultLoginResponseTemplate = { context: '{Issuer}{Issuer}{NameID}{Audience}{AuthnStatement}{AttributeStatement}', attributes: [], + additionalTemplates:{ + "attributeStatementTemplate": defaultAttributeStatementTemplate, + "attributeTemplate": defaultAttributeTemplate + } }; /** * @desc Default logout response template @@ -213,6 +246,8 @@ const libSaml = () => { getQueryParamByType, defaultLoginRequestTemplate, defaultLoginResponseTemplate, + defaultAttributeStatementTemplate, + defaultAttributeTemplate, defaultLogoutRequestTemplate, defaultLogoutResponseTemplate, @@ -231,16 +266,32 @@ const libSaml = () => { /** * @desc Helper function to build the AttributeStatement tag * @param {LoginResponseAttribute} attributes an array of attribute configuration + * @param {AttributeTemplate} attributeTemplate the attribut tag template to be used + * @param {AttributeStatementTemplate} attributeStatementTemplate the attributStatement tag template to be used * @return {string} */ - attributeStatementBuilder(attributes: LoginResponseAttribute[]): string { - const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => { - const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema'; - const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance'; - return `{${tagging('attr', valueTag)}}`; - }).join(''); - return `${attr}`; - }, + attributeStatementBuilder(attributes: LoginResponseAttribute[], attributeTemplate : AttributeTemplate, attributeStatementTemplate : AttributeStatementTemplate): string { + if (!attributeStatementTemplate){ + attributeStatementTemplate = defaultAttributeStatementTemplate; + } + if (!attributeTemplate){ + attributeTemplate = defaultAttributeTemplate; + } + const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => { + const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema'; + const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance'; + let attributeLine = attributeTemplate.context; + attributeLine = attributeLine.replace('{Name}',name); + attributeLine = attributeLine.replace('{NameFormat}',nameFormat); + attributeLine = attributeLine.replace('{ValueXmlnsXs}',valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs); + attributeLine = attributeLine.replace('{ValueXmlnsXsi}',valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi); + attributeLine = attributeLine.replace('{ValueXsiType}',valueXsiType); + attributeLine = attributeLine.replace('{Value}',`{${tagging('attr', valueTag)}}`); + return attributeLine; + }).join(''); + return attributeStatementTemplate.context.replace('{Attributes}',attr); +}, + /** * @desc Construct the XML signature for POST binding * @param {string} rawSamlMessage request/response xml string @@ -366,11 +417,12 @@ const libSaml = () => { // normalise the certificate string metadataCert = metadataCert.map(utility.normalizeCerString); - if (certificateNode.length === 0) { + // no certificate in node response nor metadata + if (certificateNode.length === 0 && metadataCert.length === 0) { throw new Error('NO_SELECTED_CERTIFICATE'); } - // no certificate node in response + // certificate node in response if (certificateNode.length !== 0) { const x509CertificateData = certificateNode[0].firstChild.data; const x509Certificate = utility.normalizeCerString(x509CertificateData); @@ -386,9 +438,12 @@ const libSaml = () => { sig.keyInfoProvider = new this.getKeyInfo(x509Certificate); + } else { + // Select first one from metadata + sig.keyInfoProvider = new this.getKeyInfo(metadataCert[0]); } - } + } sig.loadSignature(signatureNode); diff --git a/src/urn.ts b/src/urn.ts index 7a15a25c..df71255a 100644 --- a/src/urn.ts +++ b/src/urn.ts @@ -7,6 +7,7 @@ export enum BindingNamespace { Redirect = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', Post = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + SimpleSign = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign', Artifact = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' } @@ -47,6 +48,7 @@ const namespace = { binding: { redirect: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', post: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + simpleSign: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign', artifact: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', }, names: { @@ -184,6 +186,7 @@ const wording = { binding: { redirect: 'redirect', post: 'post', + simpleSign: 'simpleSign', artifact: 'artifact', }, certUse: { diff --git a/test/flow.ts b/test/flow.ts index 70cfc55b..16f94670 100644 --- a/test/flow.ts +++ b/test/flow.ts @@ -1,7 +1,7 @@ import esaml2 = require('../index'); import { readFileSync, writeFileSync } from 'fs'; import test from 'ava'; -import { PostBindingContext } from '../src/entity'; +import { PostBindingContext, SimpleSignBindingContext } from '../src/entity'; import * as uuid from 'uuid'; import * as url from 'url'; import util from '../src/utility'; @@ -42,7 +42,7 @@ const loginResponseTemplate = { const failedResponse: string = String(readFileSync('./test/misc/failed_response.xml')); -const createTemplateCallback = (_idp, _sp, user) => template => { +const createTemplateCallback = (_idp, _sp, _binding, user) => template => { const _id = '_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6'; const now = new Date(); const spEntityID = _sp.entityMeta.getEntityID(); @@ -52,7 +52,7 @@ const createTemplateCallback = (_idp, _sp, user) => template => { const tvalue = { ID: _id, AssertionID: idpSetting.generateID ? idpSetting.generateID() : `${uuid.v4()}`, - Destination: _sp.entityMeta.getAssertionConsumerService(binding.post), + Destination: _sp.entityMeta.getAssertionConsumerService(_binding), Audience: spEntityID, SubjectRecipient: spEntityID, NameIDFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', @@ -62,7 +62,7 @@ const createTemplateCallback = (_idp, _sp, user) => template => { ConditionsNotBefore: now.toISOString(), ConditionsNotOnOrAfter: fiveMinutesLater.toISOString(), SubjectConfirmationDataNotOnOrAfter: fiveMinutesLater.toISOString(), - AssertionConsumerServiceURL: _sp.entityMeta.getAssertionConsumerService(binding.post), + AssertionConsumerServiceURL: _sp.entityMeta.getAssertionConsumerService(_binding), EntityID: spEntityID, InResponseTo: '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4', StatusCode: 'urn:oasis:names:tc:SAML:2.0:status:Success', @@ -75,6 +75,38 @@ const createTemplateCallback = (_idp, _sp, user) => template => { }; }; +// Parse Redirect Url context + +const parseRedirectUrlContextCallBack = (_context) => { + const originalURL = url.parse(_context, true); + 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('&'); + + return { query: { + SAMLResponse: _SAMLResponse, + Signature: _Signature, + SigAlg: _SigAlg, }, + 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 = { @@ -149,6 +181,18 @@ test('create login request with redirect binding using default template and pars t.is(extract.nameIDPolicy.allowCreate, 'false'); }); +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; + 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}); + 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'); + t.is(extract.nameIDPolicy.allowCreate, 'false'); +}); + 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; t.is(typeof id, 'string'); @@ -183,6 +227,16 @@ test('signed in sp is not matched with the signed notation in idp with redirect } }); +test('signed in sp is not matched with the signed notation in idp with post simpleSign request', t => { + const _idp = identityProvider({ ...defaultIdpConfig, metadata: noSignedIdpMetadata }); + try { + const { id, context } = sp.createLoginRequest(_idp, 'simpleSign'); + t.fail(); + } catch (e) { + t.is(e.message, 'ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG'); + } +}); + test('create login request with redirect binding using [custom template]', t => { const _sp = serviceProvider({ ...defaultSpConfig, loginRequestTemplate: { @@ -218,15 +272,53 @@ test('create login request with post binding using [custom template]', t => { ? t.pass() : t.fail(); }); +test('create login request with post simpleSign binding using [custom template]', t => { + const _sp = serviceProvider({ + ...defaultSpConfig, loginRequestTemplate: { + context: '{Issuer}', + }, + }); + const { id, context, entityEndpoint, type, relayState, signature, sigAlg } = _sp.createLoginRequest(idp, 'simpleSign', template => { + return { + id: 'exposed_testing_id', + context: template, // all the tags are supposed to be replaced + }; + }) as SimpleSignBindingContext; + id === 'exposed_testing_id' && + isString(context) && + isString(relayState) && + isString(entityEndpoint) && + isString(signature) && + isString(sigAlg) && + type === 'SAMLRequest' + ? t.pass() : t.fail(); +}); + test('create login response with undefined binding', async t => { const user = { email: 'user@esaml2.com' }; - const error = await t.throwsAsync(() => idp.createLoginResponse(sp, {}, 'undefined', user, createTemplateCallback(idp, sp, user))); + const error = await t.throwsAsync(() => idp.createLoginResponse(sp, {}, 'undefined', user, createTemplateCallback(idp, sp, binding.post, user))); t.is(error.message, 'ERR_CREATE_RESPONSE_UNDEFINED_BINDING'); }); +test('create redirect login response', async t => { + const user = { email: 'user@esaml2.com' }; + const { id, context } = await idp.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idp, sp, binding.redirect, user), undefined, 'relaystate'); + isString(id) && isString(context) ? t.pass() : t.fail(); +}); + +test('create post simpleSign login response', async t => { + const user = { email: 'user@esaml2.com' }; + const { id, context, entityEndpoint, type, signature, sigAlg } = await idp.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, sp, binding.simpleSign, user), undefined, 'relaystate') as SimpleSignBindingContext; + isString(id) && + isString(context) && + isString(entityEndpoint) && + isString(signature) && + isString(sigAlg) ? t.pass() : t.fail(); +}); + test('create post login response', async t => { const user = { email: 'user@esaml2.com' }; - const { id, context } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user)); + const { id, context } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user)); isString(id) && isString(context) ? t.pass() : t.fail(); }); @@ -248,7 +340,7 @@ test('create logout request when idp only has one binding', t => { test('create logout response with undefined binding', t => { try { - const { id, context } = idp.createLogoutResponse(sp, {}, 'undefined', '', createTemplateCallback(idp, sp, {})); + const { id, context } = idp.createLogoutResponse(sp, {}, 'undefined', '', createTemplateCallback(idp, sp, binding.post, {})); t.fail(); } catch (e) { t.is(e.message, 'ERR_CREATE_LOGOUT_RESPONSE_UNDEFINED_BINDING'); @@ -256,12 +348,12 @@ test('create logout response with undefined binding', t => { }); test('create logout response with redirect binding', t => { - const { id, context } = idp.createLogoutResponse(sp, {}, 'redirect', '', createTemplateCallback(idp, sp, {})); + const { id, context } = idp.createLogoutResponse(sp, {}, 'redirect', '', createTemplateCallback(idp, sp, binding.post, {})); isString(id) && isString(context) ? t.pass() : t.fail(); }); test('create logout response with post binding', t => { - const { relayState, type, entityEndpoint, id, context } = idp.createLogoutResponse(sp, {}, 'post', '', createTemplateCallback(idp, sp, {})) as PostBindingContext; + const { relayState, type, entityEndpoint, id, context } = idp.createLogoutResponse(sp, {}, 'post', '', createTemplateCallback(idp, sp, binding.post, {})) as PostBindingContext; isString(id) && isString(context) && isString(entityEndpoint) && type === 'SAMLResponse' ? t.pass() : t.fail(); }); @@ -272,7 +364,7 @@ test('create logout response with post binding', t => { test('send response with signed assertion 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 } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user)); + const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); @@ -282,6 +374,49 @@ test('send response with signed assertion and parse it', async t => { t.is(extract.response.inResponseTo, 'request_id'); }); +// + REDIRECT +test('send response with signed assertion by redirect 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 } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idpNoEncrypt, sp, binding.redirect, user), undefined, 'relaystate'); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) ); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); +}); + +// SimpleSign +test('send response with signed assertion 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( + sp, + sampleRequestInfo, + 'simpleSign', + user, + createTemplateCallback(idpNoEncrypt, sp, 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 sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); +}); + test('send response with signed assertion + custom transformation algorithms and parse it', async t => { // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) const signedAssertionSp = serviceProvider( @@ -295,7 +430,7 @@ test('send response with signed assertion + custom transformation algorithms and ); const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(signedAssertionSp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user)); + const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(signedAssertionSp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); @@ -310,6 +445,76 @@ test('send response with signed assertion + custom transformation algorithms and } }); +test('send response with signed assertion + custom transformation algorithms by redirect and parse it', async t => { + // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) + const signedAssertionSp = serviceProvider( + { + ...defaultSpConfig, + transformationAlgorithms: [ + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#' + ] + } + ); + const user = { email: 'user@esaml2.com' }; + const { id, context } = await idpNoEncrypt.createLoginResponse(signedAssertionSp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idpNoEncrypt, signedAssertionSp, binding.redirect, user), undefined, 'relaystate'); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await signedAssertionSp.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) ); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); + + // Verify xmldsig#enveloped-signature is included in the response + if (samlContent.indexOf('http://www.w3.org/2000/09/xmldsig#enveloped-signature') === -1) { + t.fail(); + } +}); + +test('send response with signed assertion + custom transformation algorithms by post simplesign and parse it', async t => { + // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) + const signedAssertionSp = serviceProvider( + { + ...defaultSpConfig, + transformationAlgorithms: [ + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#' + ] + } + ); + + const user = { email: 'user@esaml2.com' } + const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse( + signedAssertionSp, + sampleRequestInfo, + 'simpleSign', + user, + createTemplateCallback(idpNoEncrypt, sp, 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 sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); + + // Verify xmldsig#enveloped-signature is included in the response + if (samlContent.indexOf('http://www.w3.org/2000/09/xmldsig#enveloped-signature') === -1) { + t.fail(); + } +}); + test('send response with [custom template] signed assertion and parse it', async t => { // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) const requestInfo = { extract: { request: { id: 'request_id' } } }; @@ -320,7 +525,7 @@ test('send response with [custom template] signed assertion and parse it', async 'post', user, // declare the callback to do custom template replacement - createTemplateCallback(idpcustomNoEncrypt, sp, user), + createTemplateCallback(idpcustomNoEncrypt, sp, binding.post, user), ); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'post', { body: { SAMLResponse } }); @@ -333,10 +538,67 @@ test('send response with [custom template] signed assertion and parse it', async t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); }); +test('send response with [custom template] signed assertion by redirect and parse it', async t => { + // 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 } = await idpcustomNoEncrypt.createLoginResponse( + sp, + requestInfo, + 'redirect', + user, + createTemplateCallback(idpcustomNoEncrypt, sp, binding.redirect, user), + undefined, + 'relaystate' + ); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.attributes.name, 'mynameinsp'); + t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com'); + t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); +}); + +test('send response with [custom template] signed assertion 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: { request: { id: 'request_id' } } }; + const user = { email: 'user@esaml2.com'}; + const { id, context: SAMLResponse, type, sigAlg, signature, entityEndpoint, relayState } = await idpcustomNoEncrypt.createLoginResponse( + sp, + requestInfo, + 'simpleSign', + user, + // declare the callback to do custom template replacement + createTemplateCallback(idpcustomNoEncrypt, sp, 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 sp.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(entityEndpoint, 'https://sp.example.org/sp/sso'); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.attributes.name, 'mynameinsp'); + t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com'); + t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); +}); + test('send response with signed message 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 } = await idpNoEncrypt.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spNoAssertSign, user)); + const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spNoAssertSign, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); @@ -346,6 +608,56 @@ test('send response with signed message and parse it', async t => { t.is(extract.response.inResponseTo, 'request_id'); }); +test('send response with signed message by redirect and parse it', async t => { + // 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 } = await idpNoEncrypt.createLoginResponse( + spNoAssertSign, + requestInfo, + 'redirect', + user, + createTemplateCallback(idpNoEncrypt, spNoAssertSign, binding.redirect, user), + undefined, + 'relaystate' + ); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) ); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); +}); + +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( + spNoAssertSign, + sampleRequestInfo, + 'simpleSign', + user, + createTemplateCallback(idpNoEncrypt, spNoAssertSign, 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 spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); +}); + test('send response with [custom template] and signed message 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' } } }; @@ -354,7 +666,7 @@ test('send response with [custom template] and signed message and parse it', asy spNoAssertSign, { extract: { authnrequest: { id: 'request_id' } } }, 'post', { email: 'user@esaml2.com' }, - createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, user), + createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, binding.post, user), ); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'post', { body: { SAMLResponse } }); @@ -367,13 +679,67 @@ test('send response with [custom template] and signed message and parse it', asy t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); }); +test('send response with [custom template] and signed message by redirect and parse it', async t => { + // 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 } = await idpcustomNoEncrypt.createLoginResponse( + spNoAssertSign, + requestInfo, + 'redirect', + user, + createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, binding.redirect, user), + undefined, + 'relaystate' + ); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) ); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.attributes.name, 'mynameinsp'); + t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com'); + t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); +}); + +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( + spNoAssertSign, + { extract: { authnrequest: { id: 'request_id' } } }, 'simpleSign', + { email: 'user@esaml2.com' }, + createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, 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 spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.attributes.name, 'mynameinsp'); + t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com'); + t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); +}); + test('send login response with signed assertion + signed message and parse it', async t => { const spWantMessageSign = serviceProvider({ ...defaultSpConfig, wantMessageSigned: true, }); const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spWantMessageSign, user)); + const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spWantMessageSign, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spWantMessageSign.parseLoginResponse (idpNoEncrypt, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); @@ -383,6 +749,60 @@ test('send login response with signed assertion + signed message and parse it', t.is(extract.response.inResponseTo, 'request_id'); }); +test('send response with signed assertion + signed message by redirect and parse it', async t => { + // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) + const spWantMessageSign = serviceProvider({ + ...defaultSpConfig, + wantMessageSigned: true, + }); + const requestInfo = { extract: { request: { id: 'request_id' } } }; + const user = { email: 'user@esaml2.com' }; + const { id, context } = await idpNoEncrypt.createLoginResponse( + spWantMessageSign, + requestInfo, + 'redirect', + user, + createTemplateCallback(idpNoEncrypt, spWantMessageSign, binding.redirect, user), + undefined, + 'relaystate' + ); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) ); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); +}); + +test('send login response with signed assertion + signed message by post simplesign and parse it', async t => { + const spWantMessageSign = serviceProvider({ + ...defaultSpConfig, + wantMessageSigned: true, + }); + const user = { email: 'user@esaml2.com' }; + const { id, context: SAMLResponse, type, 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 }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.response.inResponseTo, 'request_id'); +}); + test('send login response with [custom template] and signed assertion + signed message and parse it', async t => { const spWantMessageSign = serviceProvider({ ...defaultSpConfig, @@ -393,7 +813,7 @@ test('send login response with [custom template] and signed assertion + signed m spWantMessageSign, { extract: { authnrequest: { id: 'request_id' } } }, 'post', { email: 'user@esaml2.com' }, - createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, user), + createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, binding.post, user), ); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'post', { body: { SAMLResponse } }); @@ -406,9 +826,70 @@ test('send login response with [custom template] and signed assertion + signed m t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); }); +test('send response with [custom template] and signed assertion + signed message by redirect and parse it', async t => { + // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) + const spWantMessageSign = serviceProvider({ + ...defaultSpConfig, + wantMessageSigned: true, + }); + const requestInfo = { extract: { request: { id: 'request_id' } } }; + const user = { email: 'user@esaml2.com' }; + const { id, context } = await idpcustomNoEncrypt.createLoginResponse( + spWantMessageSign, + requestInfo, + 'redirect', + user, + createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, binding.redirect, user), + undefined, + 'relaystate' + ); + const query = url.parse(context).query; + t.is(query!.includes('SAMLResponse='), true); + t.is(query!.includes('SigAlg='), true); + t.is(query!.includes('Signature='), true); + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) ); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.attributes.name, 'mynameinsp'); + t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com'); + t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); +}); + +test('send login response with [custom template] and signed assertion + signed message by post simplesign and parse it', async t => { + const spWantMessageSign = serviceProvider({ + ...defaultSpConfig, + wantMessageSigned: true, + }); + const user = { email: 'user@esaml2.com'}; + const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( + spWantMessageSign, + { extract: { authnrequest: { id: 'request_id' } } }, + 'simpleSign', + { email: 'user@esaml2.com' }, + createTemplateCallback(idpcustomNoEncrypt, 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(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + t.is(typeof id, 'string'); + t.is(samlContent.startsWith(''), true); + t.is(extract.nameID, 'user@esaml2.com'); + t.is(extract.attributes.name, 'mynameinsp'); + t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com'); + t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4'); +}); + test('send login response with encrypted non-signed assertion and parse it', async t => { const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idp.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spNoAssertSign, user)); + const { id, context: SAMLResponse } = await idp.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spNoAssertSign, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); @@ -420,7 +901,7 @@ test('send login response with encrypted non-signed assertion and parse it', asy test('send login response with encrypted signed assertion and parse it', async t => { const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user)); + const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await sp.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); @@ -436,7 +917,7 @@ test('send login response with [custom template] and encrypted signed assertion sp, { extract: { request: { id: 'request_id' } } }, 'post', { email: 'user@esaml2.com' }, - createTemplateCallback(idpcustom, sp, user), + createTemplateCallback(idpcustom, sp, binding.post, user), ); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await sp.parseLoginResponse(idpcustom, 'post', { body: { SAMLResponse } }); @@ -455,7 +936,7 @@ test('send login response with encrypted signed assertion + signed message and p wantMessageSigned: true, }); const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idp.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWantMessageSign, user)); + const { id, context: SAMLResponse } = await idp.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWantMessageSign, binding.post, user)); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }); @@ -477,7 +958,7 @@ test('send login response with [custom template] encrypted signed assertion + si spWantMessageSign, { extract: { authnrequest: { id: 'request_id' } } }, 'post', { email: 'user@esaml2.com' }, - createTemplateCallback(idpcustom, spWantMessageSign, user), + createTemplateCallback(idpcustom, spWantMessageSign, binding.post, user), ); // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustom, 'post', { body: { SAMLResponse } }); @@ -562,7 +1043,7 @@ test('idp sends a post logout request with signature and sp parses it', async t // simulate init-slo test('sp sends a post logout response without signature and parse', async t => { - const { context: SAMLResponse } = sp.createLogoutResponse(idp, null, 'post', '', createTemplateCallback(idp, sp, {})) as PostBindingContext; + const { context: SAMLResponse } = sp.createLogoutResponse(idp, null, 'post', '', createTemplateCallback(idp, sp, binding.post, {})) as PostBindingContext; const { samlContent, extract } = await idp.parseLogoutResponse(sp, 'post', { body: { SAMLResponse }}); t.is(extract.signature, null); t.is(extract.issuer, 'https://sp.example.org/metadata'); @@ -571,7 +1052,7 @@ test('sp sends a post logout response without signature and parse', async t => { }); test('sp sends a post logout response with signature and parse', async t => { - const { relayState, type, entityEndpoint, id, context: SAMLResponse } = sp.createLogoutResponse(idpWantLogoutResSign, sampleRequestInfo, 'post', '', createTemplateCallback(idpWantLogoutResSign, sp, {})) as PostBindingContext; + const { relayState, type, entityEndpoint, id, context: SAMLResponse } = sp.createLogoutResponse(idpWantLogoutResSign, sampleRequestInfo, 'post', '', createTemplateCallback(idpWantLogoutResSign, sp, binding.post, {})) as PostBindingContext; const { samlContent, extract } = await idpWantLogoutResSign.parseLogoutResponse(sp, 'post', { body: { SAMLResponse }}); t.is(typeof extract.signature, 'string'); t.is(extract.issuer, 'https://sp.example.org/metadata'); @@ -581,7 +1062,7 @@ test('sp sends a post logout response with signature and parse', async t => { test('send login response with encrypted non-signed assertion with EncryptThenSign and parse it', async t => { const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idpEncryptThenSign.createLoginResponse(spNoAssertSignCustomConfig, sampleRequestInfo, 'post', user, createTemplateCallback(idpEncryptThenSign, spNoAssertSignCustomConfig, user), true); + const { id, context: SAMLResponse } = await idpEncryptThenSign.createLoginResponse(spNoAssertSignCustomConfig, sampleRequestInfo, 'post', user, createTemplateCallback(idpEncryptThenSign, spNoAssertSignCustomConfig, binding.post, user), true); const { samlContent, extract } = await spNoAssertSignCustomConfig.parseLoginResponse(idpEncryptThenSign, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(' { const idpCustomizePfx = identityProvider(Object.assign(defaultIdpConfig, { tagPrefix: { encryptedAssertion: 'saml2', }})); - const { id, context: SAMLResponse } = await idpCustomizePfx.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpCustomizePfx, sp, user)); + const { id, context: SAMLResponse } = await idpCustomizePfx.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpCustomizePfx, sp, binding.post, user)); t.is((utility.base64Decode(SAMLResponse) as string).includes('saml2:EncryptedAssertion'), true); const { samlContent, extract } = await sp.parseLoginResponse(idpCustomizePfx, 'post', { body: { SAMLResponse } }); }); test('Customize prefix (default is saml) for encrypted assertion tag', async t => { const user = { email: 'test@email.com' }; - const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user)); + const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user)); t.is((utility.base64Decode(SAMLResponse) as string).includes('saml:EncryptedAssertion'), true); const { samlContent, extract } = await sp.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }); }); @@ -609,7 +1090,7 @@ test('Customize prefix (default is saml) for encrypted assertion tag', async t = test('avoid malformatted response', 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 } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user)); + const { context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user)); const rawResponse = String(utility.base64Decode(SAMLResponse, true)); const attackResponse = `evil@evil.com${rawResponse}`; try { @@ -620,10 +1101,46 @@ test('avoid malformatted response', async t => { } }); +test('avoid malformatted response with redirect 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 { id, context } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idpNoEncrypt, sp, binding.redirect, user), undefined, ''); + const originalURL = url.parse(context, true); + const SAMLResponse = originalURL.query.SAMLResponse; + const signature = originalURL.query.Signature; + const sigAlg = originalURL.query.SigAlg; + delete originalURL.query.Signature; + + const rawResponse = utility.inflateString(SAMLResponse as string); + const attackResponse = `evil@evil.com${rawResponse}`; + const octetString = "SAMLResponse=" + encodeURIComponent(utility.base64Encode(utility.deflateString(attackResponse))) + "&SigAlg=" + encodeURIComponent(sigAlg as string); + try { + await sp.parseLoginResponse(idpNoEncrypt, 'redirect', { query :{ SAMLResponse, SigAlg: sigAlg, Signature: signature}, octetString }); + } catch (e) { + // it must throw an error + t.is(true, true); + } +}); + +test('avoid malformatted 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 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 }); + } catch (e) { + // it must throw an error + t.is(true, true); + } +}); + test('should reject signature wrapped response - case 1', async t => { - // + // const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user)); + const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user)); //Decode const buffer = Buffer.from(SAMLResponse, 'base64'); const xml = buffer.toString(); @@ -645,9 +1162,9 @@ test('should reject signature wrapped response - case 1', async t => { }); test('should reject signature wrapped response - case 2', async t => { - // + // const user = { email: 'user@esaml2.com' }; - const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user)); + const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user)); //Decode const buffer = Buffer.from(SAMLResponse, 'base64'); const xml = buffer.toString(); @@ -676,6 +1193,26 @@ test('should throw two-tiers code error when the response does not return succes } }); +test('should throw two-tiers code error when the response by redirect does not return success status', async t => { + try { + const SAMLResponse = utility.base64Encode(utility.deflateString(failedResponse)); + const sigAlg = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + const encodedSigAlg = encodeURIComponent(sigAlg); + const octetString = "SAMLResponse=" + encodeURIComponent(SAMLResponse) + "&SigAlg=" + encodedSigAlg; + const _result = await sp.parseLoginResponse(idpNoEncrypt, 'redirect',{ query :{ SAMLResponse, SigAlg: encodedSigAlg} , octetString} ); + } catch (e) { + t.is(e.message, 'ERR_FAILED_STATUS with top tier code: urn:oasis:names:tc:SAML:2.0:status:Requester, second tier code: urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'); + } +}); + +test('should throw two-tiers code error when the response over simpleSign does not return success status', async t => { + try { + const _result = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(failedResponse) } }); + } catch (e) { + t.is(e.message, 'ERR_FAILED_STATUS with top tier code: urn:oasis:names:tc:SAML:2.0:status:Requester, second tier code: urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'); + } +}); + test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response without clock drift setup', async t => { const now = new Date(); @@ -686,7 +1223,7 @@ test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response const user = { email: 'user@esaml2.com' }; try { - const { context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user)); + const { context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user)); // simulate the time on client side when response arrives after 5.1 sec tk.freeze(fiveMinutesOneSecLater); await sp.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }); @@ -699,16 +1236,59 @@ test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response } }); -test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response with clock drift setup', async t => { +test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by redirect without clock drift setup', async t => { const now = new Date(); const fiveMinutesOneSecLater = new Date(now.getTime()); fiveMinutesOneSecLater.setMinutes(fiveMinutesOneSecLater.getMinutes() + 5); fiveMinutesOneSecLater.setSeconds(fiveMinutesOneSecLater.getSeconds() + 1); + + const user = { email: 'user@esaml2.com' }; + + try { + const { context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idp, sp, binding.redirect, user), undefined, 'relaystate'); + // simulate the time on client side when response arrives after 5.1 sec + tk.freeze(fiveMinutesOneSecLater); + await sp.parseLoginResponse(idp, 'redirect', parseRedirectUrlContextCallBack(SAMLResponse)); + // test failed, it shouldn't happen + t.is(true, false); + } catch (e) { + t.is(e, 'ERR_SUBJECT_UNCONFIRMED'); + } finally { + tk.reset(); + } +}); + +test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by simpleSign without clock drift setup', async t => { + + const now = new Date(); + const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000); + const user = { email: 'user@esaml2.com' }; try { - const { context: SAMLResponse } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWithClockDrift, user)); + 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); + // 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 }); + // test failed, it shouldn't happen + t.is(true, false); + } catch (e) { + t.is(e, 'ERR_SUBJECT_UNCONFIRMED'); + } finally { + tk.reset(); + } +}); + +test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response with clock drift setup', async t => { + + const now = new Date(); + const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000); + const user = { email: 'user@esaml2.com' }; + + try { + const { context: SAMLResponse } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWithClockDrift, binding.post, user)); // simulate the time on client side when response arrives after 5.1 sec tk.freeze(fiveMinutesOneSecLater); await spWithClockDrift.parseLoginResponse(idp, 'post', { body: { SAMLResponse } }); @@ -720,4 +1300,47 @@ test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML respo tk.reset(); } -}); \ No newline at end of file +}); + +test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by redirect with clock drift setup', async t => { + + const now = new Date(); + const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000); + const user = { email: 'user@esaml2.com' }; + + try { + const { context: SAMLResponse } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'redirect', user, createTemplateCallback(idp, spWithClockDrift, binding.redirect, user), undefined, ''); + // simulate the time on client side when response arrives after 5.1 sec + tk.freeze(fiveMinutesOneSecLater); + await spWithClockDrift.parseLoginResponse(idp, 'redirect', parseRedirectUrlContextCallBack(SAMLResponse)); + t.is(true, true); + } catch (e) { + // test failed, it shouldn't happen + t.is(e, false); + } finally { + tk.reset(); + } + +}); + +test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by simpleSign with clock drift setup', async t => { + + const now = new Date(); + const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000); + 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); + // 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 }); + t.is(true, true); + } catch (e) { + // test failed, it shouldn't happen + t.is(e, false); + } finally { + tk.reset(); + } + +}); diff --git a/test/misc/idpmeta.xml b/test/misc/idpmeta.xml index e81c9889..af371416 100644 --- a/test/misc/idpmeta.xml +++ b/test/misc/idpmeta.xml @@ -28,6 +28,7 @@ urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + diff --git a/test/misc/idpmeta_nosign.xml b/test/misc/idpmeta_nosign.xml index a30a4284..457d8b1f 100644 --- a/test/misc/idpmeta_nosign.xml +++ b/test/misc/idpmeta_nosign.xml @@ -21,6 +21,7 @@ urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + diff --git a/test/misc/idpmeta_onelogoutservice.xml b/test/misc/idpmeta_onelogoutservice.xml index c6d3af95..8b9c18cd 100644 --- a/test/misc/idpmeta_onelogoutservice.xml +++ b/test/misc/idpmeta_onelogoutservice.xml @@ -28,6 +28,7 @@ urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + diff --git a/test/misc/idpmeta_share_cert.xml b/test/misc/idpmeta_share_cert.xml index 33b0d5b3..74a466c2 100644 --- a/test/misc/idpmeta_share_cert.xml +++ b/test/misc/idpmeta_share_cert.xml @@ -21,6 +21,7 @@ urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + diff --git a/test/misc/spmeta.xml b/test/misc/spmeta.xml index 81068842..13e7c6fc 100644 --- a/test/misc/spmeta.xml +++ b/test/misc/spmeta.xml @@ -23,5 +23,7 @@ + + diff --git a/test/misc/spmeta_noassertsign.xml b/test/misc/spmeta_noassertsign.xml index 984917ed..e917b87a 100644 --- a/test/misc/spmeta_noassertsign.xml +++ b/test/misc/spmeta_noassertsign.xml @@ -25,8 +25,14 @@ - + + + diff --git a/test/misc/spmeta_noauthnsign.xml b/test/misc/spmeta_noauthnsign.xml index 0fc8d2c6..44f88afd 100644 --- a/test/misc/spmeta_noauthnsign.xml +++ b/test/misc/spmeta_noauthnsign.xml @@ -18,8 +18,14 @@ - + + +