diff --git a/docs/template.md b/docs/template.md index 2e45d4b1..e0f8b3e7 100644 --- a/docs/template.md +++ b/docs/template.md @@ -19,14 +19,14 @@ then the attributes part will be included in the template string: ```xml - {attrUserEmail} - @@ -43,57 +43,57 @@ the tag name is auto-generated with prefix `attr` and the suffix is formatted as Developer can design their own request and response template for log-in and log-out respectively. There are optional parameters in setting object. ```javascript -const saml = require('samlify'); +const saml = require("samlify"); // load the template every time before each request/response is sent const sp = saml.ServiceProvider({ - //... - loginRequestTemplate: { - context: readFileSync('./loginResponseTemplate.xml'), - } + //... + loginRequestTemplate: { + context: readFileSync("./loginResponseTemplate.xml"), + }, }); ``` In SP configuration, `loginRequestTemplate` is the template of SAML Request, it can be either file name or XML string. This is the default template we've used in our module. ```xml - - + {Issuer} - - + ``` When you apply your own template, remember to do custom tag replacement when you send out the request. `replaceTagFromTemplate` is just the name here to illustrate but it's not fixed. ```javascript -router.get('/spinitsso-redirect', (req, res) => { - - const { id, context } = sp.createLoginRequest(idp, 'redirect', loginRequestTemplate => { - // Here is the callback function for custom template - // the input parameter is the value of loginRequestTemplate - // The following is the input parameter of rcallback in different actions - // sp.createLoginRequest -> loginRequestTemplate - // sp.createLogoutResponse -> logoutResponseTemplate - // idp.createLoginResponse -> loginResponseTemplate - // idp.createLogoutRequest -> logoutRequestTemplate - // replaceTagFromTemplate is a function to do dynamically substitution of tags - return replaceTagFromTemplate(loginRequestTemplate); - }); - - return res.redirect(context); - +router.get("/spinitsso-redirect", (req, res) => { + const { id, context } = sp.createLoginRequest(idp, "redirect", { + customTagReplacement: (loginRequestTemplate) => { + // Here is the callback function for custom template + // the input parameter is the value of loginRequestTemplate + // The following is the input parameter of rcallback in different actions + // sp.createLoginRequest -> loginRequestTemplate + // sp.createLogoutResponse -> logoutResponseTemplate + // idp.createLoginResponse -> loginResponseTemplate + // idp.createLogoutRequest -> logoutRequestTemplate + // replaceTagFromTemplate is a function to do dynamically substitution of tags + return replaceTagFromTemplate(loginRequestTemplate); + }, + }); + + return res.redirect(context); }); ``` diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts index 4ff402d9..178a6856 100644 --- a/src/binding-redirect.ts +++ b/src/binding-redirect.ts @@ -83,10 +83,11 @@ function buildRedirectURL(opts: BuildRedirectConfig) { * @param {function} customTagReplacement used when developers have their own login response template * @return {string} redirect URL */ -function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacement?: (template: string) => BindingContext): BindingContext { +function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp, relayState?: string }, customTagReplacement?: (template: string) => BindingContext): BindingContext { const metadata: any = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta }; const spSetting: any = entity.sp.entitySetting; + const relayState = entity.relayState === undefined ? spSetting.relayState : entity.relayState; let id: string = ''; if (metadata && metadata.idp && metadata.sp) { @@ -119,7 +120,7 @@ function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacem isSigned: metadata.sp.isAuthnRequestSigned(), entitySetting: spSetting, baseUrl: base, - relayState: spSetting.relayState, + relayState: relayState, }), }; } diff --git a/src/entity-sp.ts b/src/entity-sp.ts index 454d5664..4dd7d781 100644 --- a/src/entity-sp.ts +++ b/src/entity-sp.ts @@ -23,7 +23,7 @@ import { flow, FlowResult } from './flow'; /* * @desc interface function */ -export default function(props: ServiceProviderSettings) { +export default function (props: ServiceProviderSettings) { return new ServiceProvider(props); } @@ -54,13 +54,52 @@ export class ServiceProvider extends Entity { * @param {string} binding protocol binding * @param {function} customTagReplacement used when developers have their own login response template */ + public createLoginRequest( + idp: IdentityProvider, + binding?: string, + customTagReplacement?: (template: string) => BindingContext + ): BindingContext | PostBindingContext | SimpleSignBindingContext; + + /** + * @desc Generates the login request for developers to design their own method + * @param {IdentityProvider} idp object of identity provider + * @param {string} binding protocol binding + * @param {object} options optional parameters + * @param {function} options.customTagReplacement used when developers have their own login response template + * @param {string} options.relayState optional relay state + */ + public createLoginRequest( + idp: IdentityProvider, + binding?: string, + options?: { + customTagReplacement?: (template: string) => BindingContext; + relayState?: string; + } + ): BindingContext | PostBindingContext | SimpleSignBindingContext; + public createLoginRequest( idp: IdentityProvider, binding = 'redirect', - customTagReplacement?: (template: string) => BindingContext, - ): BindingContext | PostBindingContext| SimpleSignBindingContext { + optionsOrCustomTagReplacement?: + | { customTagReplacement?: (template: string) => BindingContext; relayState?: string; } + | ((template: string) => BindingContext) + ): BindingContext | PostBindingContext | SimpleSignBindingContext { const nsBinding = namespace.binding; const protocol = nsBinding[binding]; + + // Handle both old signature (function) and new signature (options object) + let customTagReplacement: ((template: string) => BindingContext) | undefined; + let relayState: string | undefined; + + if (typeof optionsOrCustomTagReplacement === 'function') { + // Old signature: third parameter is customTagReplacement function + customTagReplacement = optionsOrCustomTagReplacement; + } else if (optionsOrCustomTagReplacement && typeof optionsOrCustomTagReplacement === 'object') { + // New signature: third parameter is options object + customTagReplacement = optionsOrCustomTagReplacement.customTagReplacement; + relayState = optionsOrCustomTagReplacement.relayState; + } + if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) { throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG'); } @@ -68,7 +107,7 @@ export class ServiceProvider extends Entity { let context: any = null; switch (protocol) { case nsBinding.redirect: - return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement); + return redirectBinding.loginRequestRedirectURL({ idp, sp: this, relayState }, customTagReplacement); case nsBinding.post: context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement); @@ -76,13 +115,13 @@ export class ServiceProvider extends Entity { case nsBinding.simpleSign: // Object context = {id, context, signature, sigAlg} - context = simpleSignBinding.base64LoginRequest( { idp, sp: this }, customTagReplacement); + 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, diff --git a/test/flow.ts b/test/flow.ts index 75481710..d75c82b3 100644 --- a/test/flow.ts +++ b/test/flow.ts @@ -43,7 +43,7 @@ const loginResponseTemplate = { const failedResponse: string = String(readFileSync('./test/misc/failed_response.xml')); const createTemplateCallback = (_idp, _sp, _binding, user) => template => { - const _id = '_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6'; + const _id = '_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6'; const now = new Date(); const spEntityID = _sp.entityMeta.getEntityID(); const idpSetting = _idp.entitySetting; @@ -85,23 +85,25 @@ const parseRedirectUrlContextCallBack = (_context: string) => { 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, }, + 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 buildSimpleSignOctetString = (type: string, context: string, sigAlg: string | undefined, relayState: string | undefined, signature: string | undefined) => { const rawRequest = String(utility.base64Decode(context, true)); - let octetString:string = ''; + let octetString: string = ''; octetString += type + '=' + rawRequest; - if (relayState !== undefined && relayState.length > 0){ + if (relayState !== undefined && relayState.length > 0) { octetString += '&RelayState=' + relayState; } - if (signature !== undefined && signature.length >0 && sigAlg && sigAlg.length > 0){ + if (signature !== undefined && signature.length > 0 && sigAlg && sigAlg.length > 0) { octetString += '&SigAlg=' + sigAlg; } return octetString; @@ -151,7 +153,8 @@ const idpEncryptThenSign = identityProvider({ ...defaultIdpConfig, messageSignin const spWantLogoutReqSign = serviceProvider({ ...defaultSpConfig, wantLogoutRequestSigned: true }); const idpWantLogoutResSign = identityProvider({ ...defaultIdpConfig, wantLogoutResponseSigned: true }); const spNoAssertSign = serviceProvider({ ...defaultSpConfig, metadata: spmetaNoAssertSign }); -const spNoAssertSignCustomConfig = serviceProvider({ ...defaultSpConfig, +const spNoAssertSignCustomConfig = serviceProvider({ + ...defaultSpConfig, metadata: spmetaNoAssertSign, signatureConfig: { prefix: 'ds', @@ -174,7 +177,7 @@ test('create login request with redirect binding using default template and pars const SigAlg = originalURL.query.SigAlg; delete originalURL.query.Signature; const octetString = Object.keys(originalURL.query).map(q => q + '=' + encodeURIComponent(originalURL.query[q] as string)).join('&'); - const { samlContent, extract } = await idp.parseLoginRequest(sp, 'redirect', { query: { SAMLRequest, Signature, SigAlg }, octetString}); + const { samlContent, extract } = await idp.parseLoginRequest(sp, 'redirect', { query: { SAMLRequest, Signature, 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'); @@ -185,8 +188,8 @@ test('create login request with post simpleSign binding using default template a 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}); + 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'); @@ -199,7 +202,7 @@ test('create login request with post binding using default template and parse it t.is(typeof SAMLRequest, 'string'); t.is(typeof entityEndpoint, 'string'); t.is(type, 'SAMLRequest'); - const { extract } = await idp.parseLoginRequest(sp, 'post', { body: { SAMLRequest }}); + const { extract } = await idp.parseLoginRequest(sp, 'post', { body: { SAMLRequest } }); 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'); @@ -243,11 +246,13 @@ test('create login request with redirect binding using [custom template]', t => context: '{Issuer}', }, }); - const { id, context } = _sp.createLoginRequest(idp, 'redirect', template => { - return { - id: 'exposed_testing_id', - context: template, // all the tags are supposed to be replaced - }; + const { id, context } = _sp.createLoginRequest(idp, 'redirect', { + customTagReplacement: template => { + return { + id: 'exposed_testing_id', + context: template, // all the tags are supposed to be replaced + }; + } }); (id === 'exposed_testing_id' && isString(context)) ? t.pass() : t.fail(); }); @@ -263,7 +268,7 @@ test('create login request with redirect binding signing with unencrypted PKCS#8 const { context } = _sp.createLoginRequest(idp, 'redirect'); const parsed = parseRedirectUrlContextCallBack(context) - const signature = Buffer.from(parsed.query.Signature as string, 'base64'); + const signature = Buffer.from(parsed.query.Signature as string, 'base64'); const valid = libsaml.verifyMessageSignature(_sp.entityMeta, parsed.octetString, signature, parsed.query.SigAlg as string); t.true(valid, 'signature did not validate'); @@ -280,23 +285,128 @@ test('create login request with redirect binding signing with encrypted PKCS#8', const { context } = _sp.createLoginRequest(idp, 'redirect'); const parsed = parseRedirectUrlContextCallBack(context) - const signature = Buffer.from(parsed.query.Signature as string, 'base64'); + const signature = Buffer.from(parsed.query.Signature as string, 'base64'); const valid = libsaml.verifyMessageSignature(_sp.entityMeta, parsed.octetString, signature, parsed.query.SigAlg as string); t.true(valid, 'signature did not validate'); }); -test('create login request with post binding using [custom template]', t => { +test('create login request with redirect binding using custom relay state', async t => { + const customRelayState = 'https://example.com/return'; + const { id, context } = sp.createLoginRequest(idp, 'redirect', { relayState: customRelayState }); + + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + + // Parse the URL to extract query parameters + const originalURL = url.parse(context, true); + const SAMLRequest = originalURL.query.SAMLRequest; + const Signature = originalURL.query.Signature; + const SigAlg = originalURL.query.SigAlg; + const RelayState = originalURL.query.RelayState; + + // Verify that RelayState is included in the URL + t.is(RelayState, customRelayState); + + // Verify that the request can still be parsed correctly + delete originalURL.query.Signature; + const octetString = Object.keys(originalURL.query).map(q => q + '=' + encodeURIComponent(originalURL.query[q] as string)).join('&'); + const { samlContent, extract } = await idp.parseLoginRequest(sp, 'redirect', { query: { SAMLRequest, Signature, 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 redirect binding without relay state', async t => { + const { id, context } = sp.createLoginRequest(idp, 'redirect'); + + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + + // Parse the URL to extract query parameters + const originalURL = url.parse(context, true); + const RelayState = originalURL.query.RelayState; + + // Verify that RelayState is not included in the URL when not provided + t.falsy(RelayState); +}); + +test('create login request with redirect binding using SP default relay state', async t => { + const defaultRelayState = 'https://sp.example.org/default-return'; + const spWithDefaultRelay = serviceProvider({ + ...defaultSpConfig, + relayState: defaultRelayState, + }); + + const { id, context } = spWithDefaultRelay.createLoginRequest(idp, 'redirect'); + + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + + // Parse the URL to extract query parameters + const originalURL = url.parse(context, true); + const RelayState = originalURL.query.RelayState; + + // Verify that the SP's default RelayState is used + t.is(RelayState, defaultRelayState); +}); + +test('create login request with redirect binding overriding SP default relay state', async t => { + const defaultRelayState = 'https://sp.example.org/default-return'; + const customRelayState = 'https://example.com/custom-return'; + const spWithDefaultRelay = serviceProvider({ + ...defaultSpConfig, + relayState: defaultRelayState, + }); + + const { id, context } = spWithDefaultRelay.createLoginRequest(idp, 'redirect', { relayState: customRelayState }); + + t.is(typeof id, 'string'); + t.is(typeof context, 'string'); + + // Parse the URL to extract query parameters + const originalURL = url.parse(context, true); + const RelayState = originalURL.query.RelayState; + + // Verify that the custom RelayState overrides the SP's default + t.is(RelayState, customRelayState); + t.not(RelayState, defaultRelayState); +}); + +test('create login request with redirect binding using old API signature (backward compatibility)', async t => { const _sp = serviceProvider({ ...defaultSpConfig, loginRequestTemplate: { context: '{Issuer}', }, }); - const { id, context, entityEndpoint, type, relayState } = _sp.createLoginRequest(idp, 'post', template => { + + // Test old API signature: createLoginRequest(idp, binding, customTagReplacement) + const { id, context } = _sp.createLoginRequest(idp, 'redirect', template => { return { - id: 'exposed_testing_id', - context: template, // all the tags are supposed to be replaced + id: 'old_api_test_id', + context: template, }; + }); + + t.is(id, 'old_api_test_id'); + t.is(typeof context, 'string'); +}); + +test('create login request with post binding using [custom template]', t => { + const _sp = serviceProvider({ + ...defaultSpConfig, loginRequestTemplate: { + context: '{Issuer}', + }, + }); + const { id, context, entityEndpoint, type, relayState } = _sp.createLoginRequest(idp, 'post', { + customTagReplacement: template => { + return { + id: 'exposed_testing_id', + context: template, // all the tags are supposed to be replaced + }; + } }) as PostBindingContext; id === 'exposed_testing_id' && isString(context) && @@ -312,11 +422,13 @@ test('create login request with post simpleSign binding using [custom template]' 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 - }; + const { id, context, entityEndpoint, type, relayState, signature, sigAlg } = _sp.createLoginRequest(idp, 'simpleSign', { + customTagReplacement: template => { + return { + id: 'exposed_testing_id', + context: template, // all the tags are supposed to be replaced + }; + } }) as SimpleSignBindingContext; id === 'exposed_testing_id' && isString(context) && @@ -420,7 +532,7 @@ test('send response with signed assertion by redirect and parse it', async t => 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) ); + const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -443,7 +555,7 @@ test('send response with signed assertion by post simplesign and parse it', asyn ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -457,8 +569,8 @@ test('send response with signed assertion + custom transformation algorithms and { ...defaultSpConfig, transformationAlgorithms: [ - 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', - 'http://www.w3.org/2001/10/xml-exc-c14n#' + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#' ] } ); @@ -485,8 +597,8 @@ test('send response with signed assertion + custom transformation algorithms by { ...defaultSpConfig, transformationAlgorithms: [ - 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', - 'http://www.w3.org/2001/10/xml-exc-c14n#' + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#' ] } ); @@ -499,7 +611,7 @@ test('send response with signed assertion + custom transformation algorithms by 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) ); + const { samlContent, extract } = await signedAssertionSp.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -518,8 +630,8 @@ test('send response with signed assertion + custom transformation algorithms by { ...defaultSpConfig, transformationAlgorithms: [ - 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', - 'http://www.w3.org/2001/10/xml-exc-c14n#' + 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#' ] } ); @@ -533,10 +645,10 @@ test('send response with signed assertion + custom transformation algorithms by createTemplateCallback(idpNoEncrypt, sp, binding.simpleSign, user), undefined, 'relaystate' - ) as SimpleSignBindingContext; + ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -552,7 +664,7 @@ test('send response with signed assertion + custom transformation algorithms by 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' } } }; - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse } = await idpcustomNoEncrypt.createLoginResponse( sp, requestInfo, @@ -584,7 +696,7 @@ test('send response with [custom template] signed assertion by redirect and pars 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); @@ -605,7 +717,7 @@ test('send response with [custom template] signed assertion by redirect and pars 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 user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse, type, sigAlg, signature, entityEndpoint, relayState } = await idpcustomNoEncrypt.createLoginResponse( sp, requestInfo, @@ -618,7 +730,7 @@ test('send response with [custom template] signed assertion by post simpleSign a ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -654,7 +766,7 @@ test('send response with signed message by redirect and parse it', async t => { 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); @@ -662,7 +774,7 @@ test('send response with signed message by redirect and parse it', async t => { 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) ); + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -684,7 +796,7 @@ test('send response with signed message by post simplesign and parse it', async ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -695,7 +807,7 @@ test('send response with signed message by post simplesign and parse it', async 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' } } }; - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse } = await idpcustomNoEncrypt.createLoginResponse( spNoAssertSign, { extract: { authnrequest: { id: 'request_id' } } }, 'post', @@ -725,7 +837,7 @@ test('send response with [custom template] and signed message by redirect and pa 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); @@ -733,7 +845,7 @@ test('send response with [custom template] and signed message by redirect and pa 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) ); + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -746,7 +858,7 @@ test('send response with [custom template] and signed message by redirect and pa test('send response with [custom template] and signed message by post simplesign and parse it', async t => { // sender (caution: only use metadata and public key when declare pair-up in oppoent entity) const requestInfo = { extract: { authnrequest: { id: 'request_id' } } }; - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( spNoAssertSign, { extract: { authnrequest: { id: 'request_id' } } }, 'simpleSign', @@ -757,7 +869,7 @@ test('send response with [custom template] and signed message by post simplesign ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -775,7 +887,7 @@ test('send login response with signed assertion + signed message and parse it', const user = { email: 'user@esaml2.com' }; 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 } }); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -799,7 +911,7 @@ test('send response with signed assertion + signed message by redirect and parse 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); @@ -807,7 +919,7 @@ test('send response with signed assertion + signed message by redirect and parse 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) ); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -829,7 +941,7 @@ test('send login response with signed assertion + signed message by post simples ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spWantMessageSign.parseLoginResponse (idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -842,7 +954,7 @@ test('send login response with [custom template] and signed assertion + signed m ...defaultSpConfig, wantMessageSigned: true, }); - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse } = await idpcustomNoEncrypt.createLoginResponse( spWantMessageSign, { extract: { authnrequest: { id: 'request_id' } } }, 'post', @@ -876,7 +988,7 @@ test('send response with [custom template] and signed assertion + signed message 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); @@ -884,7 +996,7 @@ test('send response with [custom template] and signed assertion + signed message 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) ); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context)); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -899,7 +1011,7 @@ test('send login response with [custom template] and signed assertion + signed m ...defaultSpConfig, wantMessageSigned: true, }); - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse( spWantMessageSign, { extract: { authnrequest: { id: 'request_id' } } }, @@ -911,7 +1023,7 @@ test('send login response with [custom template] and signed assertion + signed m ) as SimpleSignBindingContext; // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity) const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); - const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString }); + const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(typeof id, 'string'); t.is(samlContent.startsWith(''), true); @@ -946,7 +1058,7 @@ test('send login response with encrypted signed assertion and parse it', async t }); test('send login response with [custom template] and encrypted signed assertion and parse it', async t => { - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse } = await idpcustom.createLoginResponse( sp, { extract: { request: { id: 'request_id' } } }, 'post', @@ -987,7 +1099,7 @@ test('send login response with [custom template] encrypted signed assertion + si wantMessageSigned: true, }); const requestInfo = { extract: { authnrequest: { id: 'request_id' } } }; - const user = { email: 'user@esaml2.com'}; + const user = { email: 'user@esaml2.com' }; const { id, context: SAMLResponse } = await idpcustom.createLoginResponse( spWantMessageSign, { extract: { authnrequest: { id: 'request_id' } } }, 'post', @@ -1015,7 +1127,7 @@ test('idp sends a redirect logout request without signature and sp parses it', a const originalURL = url.parse(context, true); const SAMLRequest = encodeURIComponent(originalURL.query.SAMLRequest as string); let result; - const { samlContent, extract } = result = await sp.parseLogoutRequest(idp, 'redirect', { query: { SAMLRequest }}); + const { samlContent, extract } = result = await sp.parseLogoutRequest(idp, 'redirect', { query: { SAMLRequest } }); t.is(result.sigAlg, null); t.is(typeof samlContent, 'string'); t.is(extract.nameID, 'user@esaml2.com'); @@ -1039,7 +1151,7 @@ test('idp sends a redirect logout request with signature and sp parses it', asyn const SigAlg = originalURL.query.SigAlg; delete originalURL.query.Signature; const octetString = Object.keys(originalURL.query).map(q => q + '=' + encodeURIComponent(originalURL.query[q] as string)).join('&'); - const { extract } = await spWantLogoutReqSign.parseLogoutRequest(idp, 'redirect', { query: { SAMLRequest, Signature, SigAlg }, octetString}); + const { extract } = await spWantLogoutReqSign.parseLogoutRequest(idp, 'redirect', { query: { SAMLRequest, Signature, SigAlg }, octetString }); t.is(extract.nameID, 'user@esaml2.com'); t.is(extract.issuer, 'https://idp.example.com/metadata'); t.is(typeof extract.request.id, 'string'); @@ -1078,7 +1190,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, sampleRequestInfo, 'post', '', createTemplateCallback(idp, sp, binding.post, {})) as PostBindingContext; - const { extract } = await idp.parseLogoutResponse(sp, 'post', { body: { SAMLResponse }}); + const { extract } = await idp.parseLogoutResponse(sp, 'post', { body: { SAMLResponse } }); t.is(extract.signature, null); t.is(extract.issuer, 'https://sp.example.org/metadata'); t.is(typeof extract.response.id, 'string'); @@ -1087,7 +1199,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, binding.post, {})) as PostBindingContext; - const { samlContent, extract } = await idpWantLogoutResSign.parseLogoutResponse(sp, 'post', { body: { SAMLResponse }}); + 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'); t.is(typeof extract.response.id, 'string'); @@ -1106,9 +1218,11 @@ test('send login response with encrypted non-signed assertion with EncryptThenSi test('Customize prefix (saml2) for encrypted assertion tag', async t => { const user = { email: 'test@email.com' }; - const idpCustomizePfx = identityProvider(Object.assign(defaultIdpConfig, { tagPrefix: { - encryptedAssertion: 'saml2', - }})); + const idpCustomizePfx = identityProvider(Object.assign(defaultIdpConfig, { + tagPrefix: { + encryptedAssertion: 'saml2', + } + })); 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 } }); @@ -1150,7 +1264,7 @@ test('avoid malformatted response with redirect binding', async t => { 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 }); + await sp.parseLoginResponse(idpNoEncrypt, 'redirect', { query: { SAMLResponse, SigAlg: sigAlg, Signature: signature }, octetString }); t.fail(); } catch (e) { // it must throw an error @@ -1166,7 +1280,7 @@ test('avoid malformatted response with simplesign binding', async t => { const attackResponse = `evil@evil.com${rawResponse}`; const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature); try { - await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(attackResponse), Signature: signature, SigAlg:sigAlg }, octetString }); + await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(attackResponse), Signature: signature, SigAlg: sigAlg }, octetString }); t.fail(); } catch (e) { // it must throw an error @@ -1216,7 +1330,7 @@ test('should use signed contents in signature wrapped response - case 2', async //Put stripped version under SubjectConfirmationData of modified version const xmlWrapped = outer.replace(/<\/saml:Conditions>/, '' + stripped.replace('', '') + ''); const wrappedResponse = Buffer.from(xmlWrapped).toString('base64'); - const {extract} = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse: wrappedResponse } }); + const { extract } = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse: wrappedResponse } }); t.is(extract.nameID, 'user@esaml2.com'); }); @@ -1235,7 +1349,7 @@ test('should throw two-tiers code error when the response by redirect does not r const sigAlg = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; const encodedSigAlg = encodeURIComponent(sigAlg); const octetString = 'SAMLResponse=' + encodeURIComponent(SAMLResponse) + '&SigAlg=' + encodedSigAlg; - await sp.parseLoginResponse(idpNoEncrypt, 'redirect',{ query :{ SAMLResponse, SigAlg: encodedSigAlg} , octetString} ); + await sp.parseLoginResponse(idpNoEncrypt, 'redirect', { query: { SAMLResponse, SigAlg: encodedSigAlg }, octetString }); t.fail(); } 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'); @@ -1309,7 +1423,7 @@ test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response 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 }); + await sp.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); // test failed, it shouldn't happen t.fail(); } catch (e) { @@ -1372,7 +1486,7 @@ test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML respo 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 }); + await spWithClockDrift.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg: sigAlg }, octetString }); t.is(true, true); } catch (e) { // test failed, it shouldn't happen