diff --git a/src/example.ts b/src/example.ts index 61024554..6b2ac8c5 100644 --- a/src/example.ts +++ b/src/example.ts @@ -73,7 +73,11 @@ const serviceProviderConfig: IServiceProviderConfig = { name: "Required attrs" }, spidTestEnvUrl: "https://spid-testenv2:8088", - spidValidatorUrl: "http://localhost:8080" + spidValidatorUrl: "http://localhost:8080", + strictResponseValidation: { + "http://localhost:8080": true, + "https://spid-testenv2:8088": true + } }; const redisClient = redis.createClient({ diff --git a/src/index.ts b/src/index.ts index 3bdf7108..dc2d48a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,10 +31,10 @@ import { import { getAuthorizeRequestTamperer, getErrorCodeFromResponse, + getPreValidateResponse, getSamlIssuer, getSamlOptions, - getXmlFromSamlResponse, - preValidateResponse + getXmlFromSamlResponse } from "./utils/saml"; import { getMetadataTamperer } from "./utils/saml"; @@ -151,7 +151,7 @@ export function withSpid( redisClient, authorizeRequestTamperer, metadataTamperer, - preValidateResponse + getPreValidateResponse(serviceProviderConfig.strictResponseValidation) ); }) .map(spidStrategy => { diff --git a/src/utils/__mocks__/saml.ts b/src/utils/__mocks__/saml.ts index ff49bebe..f64733ac 100644 --- a/src/utils/__mocks__/saml.ts +++ b/src/utils/__mocks__/saml.ts @@ -1,4 +1,4 @@ -export const samlResponse = ` +export const samlResponse = ` http://localhost:8080 @@ -67,14 +67,14 @@ export const samlResponse = ` + 1}-02-26T07:32:05Z" Recipient="https://app-backend.dev.io.italia.it/assertionConsumerService"/> - https://spid.agid.gov.it/cd + https://app-backend.dev.io.italia.it @@ -132,3 +132,102 @@ export const samlRequest = ` `; + +export const samlResponseCIE = ` + + https://idserver.servizicie.interno.gov.it:8443/idp/profile/SAML2/POST/SSO + + + + + + + + + + + + + 3UGuOlUuag/oPOIif31jpuIJT829Eab+2dSEDegDlmU= + + + +AIa2vTA8uOKizFvCqNchj4Dby8eDOi5UaOEZYJ4NV0RorEj2wkSFbhX65FYLt68VUGY5YR1tqDfl d0ApvcdtkH4gucq2aCd1zTq8yk5dXp10IC49YdLXlDCRh3QcgulIDZhZs/K2nTEzrrfHC7dibYv/ vk/tY5AOih2jIqNslt1gxopuLREUTyG1NC7CcqfwhxCxxs1z5ngcN1D/cZv9sQT85lzwGCU65+5G ySdiSr0WzHEEcT1k9WnDwqW27i0tbCwC2NZ3xOHl0X7mKb35TzhdMpAz74ADnalk833EjZdVHu6x XdG5KqmjIW+mrddO71jDRXQ1eMrQBeCAfRQ0Mg== + + + + MIIDdTCCAl2gAwIBAgIUU79XEfveueyClDtLkqUlSPZ2o8owDQYJKoZIhvcNAQELBQAwLTErMCkG A1UEAwwiaWRzZXJ2ZXIuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdDAeFw0xODEwMTkwODM1MDVa Fw0zODEwMTkwODM1MDVaMC0xKzApBgNVBAMMImlkc2VydmVyLnNlcnZpemljaWUuaW50ZXJuby5n b3YuaXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHraj3iOTCIILTlOzicSEuFt03 kKvQDqGWRd5o7s1W7SP2EtcTmg3xron/sbrLEL/eMUQV/Biz6J4pEGoFpMZQHGxOVypmO7Nc8pkF ot7yUTApr6Ikuy4cUtbx0g5fkQLNb3upIg0Vg1jSnRXEvUCygr/9EeKCUOi/2ptmOVSLad+dT7Ti RsZTwY3FvRWcleDfyYwcIMgz5dLSNLMZqwzQZK1DzvWeD6aGtBKCYPRftacHoESD+6bhukHZ6w95 foRMJLOaBpkp+XfugFQioYvrM0AB1YQZ5DCQRhhc8jejwdY+bOB3eZ1lJY7Oannfu6XPW2fcknel yPt7PGf22rNfAgMBAAGjgYwwgYkwHQYDVR0OBBYEFK3Ah+Do3/zB9XjZ66i4biDpUEbAMGgGA1Ud EQRhMF+CImlkc2VydmVyLnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXSGOWh0dHBzOi8vaWRzZXJ2 ZXIuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsF AAOCAQEAVtpn/s+lYVf42pAtdgJnGTaSIy8KxHeZobKNYNFEY/XTaZEt9QeV5efUMBVVhxKTTHN0 046DR96WFYXs4PJ9Fpyq6Hmy3k/oUdmHJ1c2bwWF/nZ82CwOO081Yg0GBcfPEmKLUGOBK8T55ncW +RSZadvWTyhTtQhLUtLKcWyzKB5aS3kEE5LSzR8sw3owln9P41Mz+QtL3WeNESRHW0qoQkFotYXX W6Rvh69+GyzJLxvq2qd7D1qoJgOMrarshBKKPk+ABaLYoEf/cru4e0RDIp2mD0jkGOGDkn9XUl+3 ddALq/osTki6CEawkhiZEo6ABEAjEWNkH9W3/ZzvJnWo6Q== + + + + + + + + https://idserver.servizicie.interno.gov.it:8443/idp/profile/SAML2/POST/SSO + + + + + + + + + + + + + 5nSMW/zHmyhaVE4vWyxZvHMBDQWgktouXeWl9fKe504= + + + +UJ23xMKOYhCcRVunnDgor2WLqHEgYeyaAhHr16+kkO6poPog2a9PoiqGUU0Dg+YMvHRJVq0h0sKz M1zeVN1eR3JHIB8HAYtWDDxqTe/rTZcQ1lPWEA+bGqUlLTVc2ukvC4NSB17FT1j7VDIBL3UcdlQc SvR7W6Xw/D+J9Row4iX+rmsJRTy0I+8xj3FdRxMRGR+mSPhpZ1NbINMcSwOV9b+NXbQKqbHhqfH7 SJTGbS/RBZTzFX42jmrAM57TCRG/hwyt6TZyCY29n4dsa0xHGD8sLOvQZ5Zk7qB0HD2DSp31Fjpw zyklYfmGoXrkjdUNnUVyWck+cQXHaXJyokaTNA== + + + + MIIDdTCCAl2gAwIBAgIUU79XEfveueyClDtLkqUlSPZ2o8owDQYJKoZIhvcNAQELBQAwLTErMCkG A1UEAwwiaWRzZXJ2ZXIuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdDAeFw0xODEwMTkwODM1MDVa Fw0zODEwMTkwODM1MDVaMC0xKzApBgNVBAMMImlkc2VydmVyLnNlcnZpemljaWUuaW50ZXJuby5n b3YuaXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHraj3iOTCIILTlOzicSEuFt03 kKvQDqGWRd5o7s1W7SP2EtcTmg3xron/sbrLEL/eMUQV/Biz6J4pEGoFpMZQHGxOVypmO7Nc8pkF ot7yUTApr6Ikuy4cUtbx0g5fkQLNb3upIg0Vg1jSnRXEvUCygr/9EeKCUOi/2ptmOVSLad+dT7Ti RsZTwY3FvRWcleDfyYwcIMgz5dLSNLMZqwzQZK1DzvWeD6aGtBKCYPRftacHoESD+6bhukHZ6w95 foRMJLOaBpkp+XfugFQioYvrM0AB1YQZ5DCQRhhc8jejwdY+bOB3eZ1lJY7Oannfu6XPW2fcknel yPt7PGf22rNfAgMBAAGjgYwwgYkwHQYDVR0OBBYEFK3Ah+Do3/zB9XjZ66i4biDpUEbAMGgGA1Ud EQRhMF+CImlkc2VydmVyLnNlcnZpemljaWUuaW50ZXJuby5nb3YuaXSGOWh0dHBzOi8vaWRzZXJ2 ZXIuc2Vydml6aWNpZS5pbnRlcm5vLmdvdi5pdC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsF AAOCAQEAVtpn/s+lYVf42pAtdgJnGTaSIy8KxHeZobKNYNFEY/XTaZEt9QeV5efUMBVVhxKTTHN0 046DR96WFYXs4PJ9Fpyq6Hmy3k/oUdmHJ1c2bwWF/nZ82CwOO081Yg0GBcfPEmKLUGOBK8T55ncW +RSZadvWTyhTtQhLUtLKcWyzKB5aS3kEE5LSzR8sw3owln9P41Mz+QtL3WeNESRHW0qoQkFotYXX W6Rvh69+GyzJLxvq2qd7D1qoJgOMrarshBKKPk+ABaLYoEf/cru4e0RDIp2mD0jkGOGDkn9XUl+3 ddALq/osTki6CEawkhiZEo6ABEAjEWNkH9W3/ZzvJnWo6Q== + + + + + AAdzZWNyZXQxqDU6XhTO1MGlMAoXjWFIOcPfK4AhIPsnBAoTNelku/jA7/XaogQJhOrgxCiAIqavL2GUQqQ7VMYPRryyteifD34fsyrHmbPNr1Tz2YJe8wgENUlDvaY31unC/P1kwqTZ17jQYw3qoVZs4neWi9ZUo9j8BoiDAHdoyOOoTiVbDA== + + + + + + + https://app-backend.dev.io.italia.it + + + + + + https://www.spid.gov.it/SpidL3 + + + + + 1964-12-30 + + + TINIT-RSSBNC64T70G677R + + + BIANCA + + + ROSSI + + + + +`; diff --git a/src/utils/__tests__/saml.test.ts b/src/utils/__tests__/saml.test.ts index 4ccfb31e..9b76a6bb 100644 --- a/src/utils/__tests__/saml.test.ts +++ b/src/utils/__tests__/saml.test.ts @@ -3,10 +3,22 @@ import { isSome, tryCatch } from "fp-ts/lib/Option"; import { fromEither } from "fp-ts/lib/TaskEither"; import { SamlConfig } from "passport-saml"; import { DOMParser } from "xmldom"; -import { samlRequest, samlResponse } from "../__mocks__/saml"; -import { getXmlFromSamlResponse, preValidateResponse } from "../saml"; +import { samlRequest, samlResponse, samlResponseCIE } from "../__mocks__/saml"; +import { StrictResponseValidationOptions } from "../middleware"; +import { getPreValidateResponse, getXmlFromSamlResponse } from "../saml"; import * as saml from "../saml"; +const samlConfig: SamlConfig = ({ + attributes: { + attributes: { + attributes: ["name", "fiscalNumber", "familyName", "mobilePhone", "email"] + } + }, + authnContext: "https://www.spid.gov.it/SpidL2", + callbackUrl: "https://app-backend.dev.io.italia.it/assertionConsumerService", + issuer: "https://app-backend.dev.io.italia.it" +} as unknown) as SamlConfig; + describe("getXmlFromSamlResponse", () => { it("should parse a well formatted response body", () => { const expectedSAMLResponse = "Response"; @@ -27,10 +39,24 @@ describe("preValidateResponse", () => { remove: jest.fn(), save: jest.fn() }; + + const asyncExpectOnCallback = (callback: jest.Mock) => + new Promise(resolve => { + setTimeout(() => { + expect(callback).toBeCalledWith(null, true, expect.any(String)); + resolve(); + }, 100); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + afterAll(() => { jest.restoreAllMocks(); }); - it("B", async () => { + + it("should preValidate succeded with a valid saml Response", async () => { const mockBody = "MOCKED BODY"; mockGetXmlFromSamlResponse.mockImplementation(() => tryCatch(() => new DOMParser().parseFromString(samlResponse)) @@ -44,33 +70,42 @@ describe("preValidateResponse", () => { }) ); }); - preValidateResponse( - ({ - attributes: { - attributes: { - attributes: [ - "name", - "fiscalNumber", - "familyName", - "mobilePhone", - "email" - ] - } - }, - authnContext: "https://www.spid.gov.it/SpidL2", - callbackUrl: "http://localhost:3000/acs", - issuer: "https://spid.agid.gov.it/cd" - } as unknown) as SamlConfig, + const strictValidationOption: StrictResponseValidationOptions = { + "http://localhost:8080": true + }; + getPreValidateResponse(strictValidationOption)( + samlConfig, mockBody, mockRedisCacheProvider, mockCallback ); expect(mockGetXmlFromSamlResponse).toBeCalledWith(mockBody); - await new Promise(resolve => { - setTimeout(() => { - expect(mockCallback).toBeCalledWith(null, true, expect.any(String)); - resolve(); - }, 100); + await asyncExpectOnCallback(mockCallback); + }); + + it("should preValidate succeded with a valid CIE saml Response", async () => { + const mockBody = "MOCKED BODY"; + mockGetXmlFromSamlResponse.mockImplementation(() => + tryCatch(() => new DOMParser().parseFromString(samlResponseCIE)) + ); + // tslint:disable-next-line: no-identical-functions + mockGet.mockImplementation(() => { + return fromEither( + right({ + RequestXML: samlRequest, + createdAt: "2020-02-26T07:27:42Z", + idpIssuer: + "https://idserver.servizicie.interno.gov.it:8443/idp/profile/SAML2/POST/SSO" + }) + ); }); + getPreValidateResponse()( + samlConfig, + mockBody, + mockRedisCacheProvider, + mockCallback + ); + expect(mockGetXmlFromSamlResponse).toBeCalledWith(mockBody); + await asyncExpectOnCallback(mockCallback); }); }); diff --git a/src/utils/middleware.ts b/src/utils/middleware.ts index 6426a282..320ec64b 100644 --- a/src/utils/middleware.ts +++ b/src/utils/middleware.ts @@ -35,8 +35,14 @@ export interface IServiceProviderConfig { publicCert: string; idpMetadataRefreshIntervalMillis: number; spidValidatorUrl?: string; + strictResponseValidation?: StrictResponseValidationOptions; } +export type StrictResponseValidationOptions = Record< + string, + boolean | undefined +>; + export interface ISpidStrategyOptions { idp: { [key: string]: IDPEntityDescriptor | undefined }; // tslint:disable-next-line: no-any diff --git a/src/utils/saml.ts b/src/utils/saml.ts index d792cb89..6f566435 100644 --- a/src/utils/saml.ts +++ b/src/utils/saml.ts @@ -11,6 +11,7 @@ import { Either, fromOption, fromPredicate, + left, right, toError } from "fp-ts/lib/Either"; @@ -47,7 +48,8 @@ import { logger } from "./logger"; import { getSpidStrategyOption, IServiceProviderConfig, - ISpidStrategyOptions + ISpidStrategyOptions, + StrictResponseValidationOptions } from "./middleware"; export type SamlAttributeT = keyof typeof SPID_USER_ATTRIBUTES; @@ -953,7 +955,10 @@ const assertionValidation = ( ); }; -export const preValidateResponse: PreValidateResponseT = ( +export const getPreValidateResponse = ( + strictValidationOptions?: StrictResponseValidationOptions + // tslint:disable-next-line: no-big-function +): PreValidateResponseT => ( samlConfig, body, extendedCacheProvider, @@ -966,6 +971,10 @@ export const preValidateResponse: PreValidateResponseT = ( } const doc = maybeDoc.value; + const hasStrictValidation = fromNullable(strictValidationOptions) + .chain(_ => getSamlIssuer(doc).mapNullable(issuer => _[issuer])) + .getOrElse(false); + fromEitherToTaskEither( fromOption(new Error("Missing Reponse element inside SAML Response"))( fromNullable( @@ -1066,7 +1075,7 @@ export const preValidateResponse: PreValidateResponseT = ( .chain(_ => extendedCacheProvider .get(_.InResponseTo) - .map(SAMLResponseCache => ({ ..._, SAMLResponseCache })) + .map(SAMLRequestCache => ({ ..._, SAMLRequestCache })) ) .chain(_ => fromEitherToTaskEither( @@ -1074,7 +1083,7 @@ export const preValidateResponse: PreValidateResponseT = ( new Error("An error occurs parsing the cached SAML Request") )( optionTryCatch(() => - new DOMParser().parseFromString(_.SAMLResponseCache.RequestXML) + new DOMParser().parseFromString(_.SAMLRequestCache.RequestXML) ) ) ).map(Request => ({ ..._, Request })) @@ -1175,6 +1184,12 @@ export const preValidateResponse: PreValidateResponseT = ( ) ) .chain(Attributes => { + if (!hasStrictValidation) { + // Skip Attribute validation if IDP has non-strict validation option + return fromEitherToTaskEither( + right>(Attributes) + ); + } const missingAttributes = difference(setoidString)( // tslint:disable-next-line: no-any (samlConfig as any).attributes?.attributes?.attributes || [ @@ -1202,25 +1217,23 @@ export const preValidateResponse: PreValidateResponseT = ( ) .chain(_ => fromEitherToTaskEither( - validateIssuer(_.Response, _.SAMLResponseCache.idpIssuer).chain( - Issuer => - fromOption("Format missing")( - fromNullable(Issuer.getAttribute("Format")) - ).fold( - () => right(_), - _1 => - fromPredicate( - FormatValue => !FormatValue || FormatValue === ISSUER_FORMAT, - () => - new Error("Format attribute of Issuer element is invalid") - )(_1) - ) + validateIssuer(_.Response, _.SAMLRequestCache.idpIssuer).chain(Issuer => + fromOption("Format missing")( + fromNullable(Issuer.getAttribute("Format")) + ).fold( + () => right(_), + _1 => + fromPredicate( + FormatValue => !FormatValue || FormatValue === ISSUER_FORMAT, + () => new Error("Format attribute of Issuer element is invalid") + )(_1) + ) ) ).map(() => _) ) .chain(_ => fromEitherToTaskEither( - validateIssuer(_.Assertion, _.SAMLResponseCache.idpIssuer).chain( + validateIssuer(_.Assertion, _.SAMLRequestCache.idpIssuer).chain( Issuer => NonEmptyString.decode(Issuer.getAttribute("Format")) .mapLeft( @@ -1236,6 +1249,12 @@ export const preValidateResponse: PreValidateResponseT = ( new Error("Format attribute of Issuer element is invalid") ) ) + .fold( + err => + // Skip Issuer Format validation if IDP has non-strict validation option + !hasStrictValidation ? right(_) : left(err), + _1 => right(_) + ) ) ).map(() => _) )