diff --git a/.mocharc.json b/.mocharc.json index 9755384b..bbe611bc 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -2,6 +2,7 @@ "diff": true, "extension": "spec.ts", "package": "./package.json", + "recursive": true, "reporter": "spec", "require": ["choma", "ts-node/register"], "files": "test/**/*.spec.ts", diff --git a/src/passport-saml/algorithms.ts b/src/node-saml/algorithms.ts similarity index 100% rename from src/passport-saml/algorithms.ts rename to src/node-saml/algorithms.ts diff --git a/src/node-saml/index.ts b/src/node-saml/index.ts new file mode 100644 index 00000000..a406d4d3 --- /dev/null +++ b/src/node-saml/index.ts @@ -0,0 +1,4 @@ +import type { CacheItem, CacheProvider } from "./inmemory-cache-provider"; +import { SAML } from "./saml"; + +export { SAML, CacheItem, CacheProvider }; diff --git a/src/passport-saml/inmemory-cache-provider.ts b/src/node-saml/inmemory-cache-provider.ts similarity index 100% rename from src/passport-saml/inmemory-cache-provider.ts rename to src/node-saml/inmemory-cache-provider.ts diff --git a/src/passport-saml/saml-post-signing.ts b/src/node-saml/saml-post-signing.ts similarity index 100% rename from src/passport-saml/saml-post-signing.ts rename to src/node-saml/saml-post-signing.ts diff --git a/src/passport-saml/saml.ts b/src/node-saml/saml.ts similarity index 91% rename from src/passport-saml/saml.ts rename to src/node-saml/saml.ts index 85311fee..626edb5f 100644 --- a/src/passport-saml/saml.ts +++ b/src/node-saml/saml.ts @@ -1,11 +1,11 @@ import Debug from "debug"; -const debug = Debug("passport-saml"); +const debug = Debug("node-saml"); import * as zlib from "zlib"; import * as xml2js from "xml2js"; import * as xmlCrypto from "xml-crypto"; import * as crypto from "crypto"; import * as xmldom from "xmldom"; -import * as url from "url"; +import { URL } from "url"; import * as querystring from "querystring"; import * as xmlbuilder from "xmlbuilder"; import * as xmlenc from "xml-encryption"; @@ -13,26 +13,21 @@ import * as util from "util"; import { CacheProvider as InMemoryCacheProvider } from "./inmemory-cache-provider"; import * as algorithms from "./algorithms"; import { signAuthnRequestPost } from "./saml-post-signing"; -import type { Request } from "express"; import { ParsedQs } from "qs"; import { AudienceRestrictionXML, - AuthenticateOptions, - AuthorizeOptions, AuthorizeRequestXML, CertCallback, LogoutRequestXML, - Profile, - RequestWithUser, - SamlOptions, SamlIDPListConfig, SamlIDPEntryConfig, + SamlOptions, ServiceMetadataXML, XMLInput, XMLObject, XMLOutput, - SamlConfig, } from "./types"; +import { AuthenticateOptions, AuthorizeOptions, Profile, SamlConfig } from "../passport-saml/types"; import { assertRequired } from "./utility"; const inflateRawAsync = util.promisify(zlib.inflateRaw); @@ -62,7 +57,7 @@ async function processValidlySignedPostRequestAsync( } else { throw new Error("Missing SAML issuer"); } - const nameID = await self.getNameIdAsync(self, dom); + const nameID = await self._getNameIdAsync(self, dom); if (nameID) { profile.nameID = nameID.value!; if (nameID.format) { @@ -107,6 +102,8 @@ async function promiseWithNameID(nameid: Node): Promise { } class SAML { + // note that some methods in SAML are not yet marked as private as they are used in testing. + // those methods start with an underscore, e.g. _generateUniqueID options: SamlOptions; // This is only for testing cacheProvider!: InMemoryCacheProvider; @@ -172,34 +169,34 @@ class SAML { return options; } - getProtocol(req: Request | { headers?: undefined; protocol?: undefined }) { - return this.options.protocol || (req.protocol || "http").concat("://"); - } - - getCallbackUrl(req: Request | { headers?: undefined; protocol?: undefined }) { + private getCallbackUrl(host?: string | undefined) { // Post-auth destination if (this.options.callbackUrl) { return this.options.callbackUrl; } else { - let host; - if (req.headers) { - host = req.headers.host; + const url = new URL("http://localhost"); + if (host) { + url.host = host; } else { - host = this.options.host; + url.host = this.options.host; + } + if (this.options.protocol) { + url.protocol = this.options.protocol; } - return this.getProtocol(req) + host + this.options.path; + url.pathname = this.options.path; + return url.toString(); } } - generateUniqueID() { + _generateUniqueID() { return crypto.randomBytes(10).toString("hex"); } - generateInstant() { + private generateInstant() { return new Date().toISOString(); } - signRequest(samlMessage: querystring.ParsedUrlQueryInput): void { + private signRequest(samlMessage: querystring.ParsedUrlQueryInput): void { this.options.privateKey = assertRequired(this.options.privateKey, "privateKey is required"); const samlMessageToSign: querystring.ParsedUrlQueryInput = {}; @@ -218,17 +215,17 @@ class SAML { samlMessageToSign.SigAlg = samlMessage.SigAlg; } signer.update(querystring.stringify(samlMessageToSign)); - samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateKey), "base64"); + samlMessage.Signature = signer.sign(this._keyToPEM(this.options.privateKey), "base64"); } - async generateAuthorizeRequestAsync( - req: Request, + private async generateAuthorizeRequestAsync( isPassive: boolean, - isHttpPostBinding: boolean + isHttpPostBinding: boolean, + host: string | undefined ): Promise { this.options.entryPoint = assertRequired(this.options.entryPoint, "entryPoint is required"); - const id = "_" + this.generateUniqueID(); + const id = "_" + this._generateUniqueID(); const instant = this.generateInstant(); if (this.options.validateInResponseTo) { @@ -256,7 +253,7 @@ class SAML { } if (!this.options.disableRequestAcsUrl) { - request["samlp:AuthnRequest"]["@AssertionConsumerServiceURL"] = this.getCallbackUrl(req); + request["samlp:AuthnRequest"]["@AssertionConsumerServiceURL"] = this.getCallbackUrl(host); } if (this.options.identifierFormat != null) { @@ -354,8 +351,8 @@ class SAML { return stringRequest; } - async generateLogoutRequest(req: RequestWithUser) { - const id = "_" + this.generateUniqueID(); + async _generateLogoutRequest(user: Profile) { + const id = "_" + this._generateUniqueID(); const instant = this.generateInstant(); const request = { @@ -371,24 +368,24 @@ class SAML { "#text": this.options.issuer, }, "saml:NameID": { - "@Format": req.user!.nameIDFormat, - "#text": req.user!.nameID, + "@Format": user!.nameIDFormat, + "#text": user!.nameID, }, }, } as LogoutRequestXML; - if (req.user!.nameQualifier != null) { - request["samlp:LogoutRequest"]["saml:NameID"]["@NameQualifier"] = req.user!.nameQualifier; + if (user!.nameQualifier != null) { + request["samlp:LogoutRequest"]["saml:NameID"]["@NameQualifier"] = user!.nameQualifier; } - if (req.user!.spNameQualifier != null) { - request["samlp:LogoutRequest"]["saml:NameID"]["@SPNameQualifier"] = req.user!.spNameQualifier; + if (user!.spNameQualifier != null) { + request["samlp:LogoutRequest"]["saml:NameID"]["@SPNameQualifier"] = user!.spNameQualifier; } - if (req.user!.sessionIndex) { + if (user!.sessionIndex) { request["samlp:LogoutRequest"]["saml2p:SessionIndex"] = { "@xmlns:saml2p": "urn:oasis:names:tc:SAML:2.0:protocol", - "#text": req.user!.sessionIndex, + "#text": user!.sessionIndex, }; } @@ -396,8 +393,8 @@ class SAML { return xmlbuilder.create((request as unknown) as Record).end(); } - generateLogoutResponse(req: Request, logoutRequest: Profile) { - const id = "_" + this.generateUniqueID(); + _generateLogoutResponse(logoutRequest: Profile) { + const id = "_" + this._generateUniqueID(); const instant = this.generateInstant(); const request = { @@ -423,7 +420,7 @@ class SAML { return xmlbuilder.create(request).end(); } - async requestToUrlAsync( + async _requestToUrlAsync( request: string | null | undefined, response: string | null, operation: string, @@ -439,11 +436,11 @@ class SAML { } const base64 = buffer.toString("base64"); - let target = url.parse(this.options.entryPoint, true); + let target = new URL(this.options.entryPoint); if (operation === "logout") { if (this.options.logoutUrl) { - target = url.parse(this.options.logoutUrl, true); + target = new URL(this.options.logoutUrl); } } else if (operation !== "authorize") { throw new Error("Unknown operation: " + operation); @@ -468,25 +465,20 @@ class SAML { this.signRequest(samlMessage); } Object.keys(samlMessage).forEach((k) => { - target.query[k] = samlMessage[k]; + target.searchParams.set(k, samlMessage[k] as string); }); - // Delete 'search' to for pulling query string from 'query' - // https://nodejs.org/api/url.html#url_url_format_urlobj - target.search = null; - - return url.format(target); + return target.toString(); } - getAdditionalParams( - req: Request, + _getAdditionalParams( + RelayState: string, operation: string, overrideParams?: querystring.ParsedUrlQuery ): querystring.ParsedUrlQuery { const additionalParams: querystring.ParsedUrlQuery = {}; - const RelayState = (req.query && req.query.RelayState) || (req.body && req.body.RelayState); - if (RelayState) { + if (typeof RelayState === "string" && RelayState.length > 0) { additionalParams.RelayState = RelayState; } @@ -515,19 +507,23 @@ class SAML { return additionalParams; } - async getAuthorizeUrlAsync(req: Request, options: AuthorizeOptions): Promise { - const request = await this.generateAuthorizeRequestAsync(req, this.options.passive, false); + async getAuthorizeUrlAsync( + RelayState: string, + host: string | undefined, + options: AuthorizeOptions + ): Promise { + const request = await this.generateAuthorizeRequestAsync(this.options.passive, false, host); const operation = "authorize"; const overrideParams = options ? options.additionalParams || {} : {}; - return await this.requestToUrlAsync( + return await this._requestToUrlAsync( request, null, operation, - this.getAdditionalParams(req, operation, overrideParams) + this._getAdditionalParams(RelayState, operation, overrideParams) ); } - async getAuthorizeFormAsync(req: Request): Promise { + async getAuthorizeFormAsync(RelayState: string, host?: string): Promise { this.options.entryPoint = assertRequired(this.options.entryPoint, "entryPoint is required"); // The quoteattr() function is used in a context, where the result will not be evaluated by javascript @@ -560,7 +556,7 @@ class SAML { ); }; - const request = await this.generateAuthorizeRequestAsync(req, this.options.passive, true); + const request = await this.generateAuthorizeRequestAsync(this.options.passive, true, host); let buffer: Buffer; if (this.options.skipRequestCompression) { buffer = Buffer.from(request!, "utf8"); @@ -569,7 +565,7 @@ class SAML { } const operation = "authorize"; - const additionalParameters = this.getAdditionalParams(req, operation); + const additionalParameters = this._getAdditionalParams(RelayState, operation); const samlMessage: querystring.ParsedUrlQueryInput = { SAMLRequest: buffer!.toString("base64"), }; @@ -605,41 +601,49 @@ class SAML { ].join("\r\n"); } - async getLogoutUrlAsync(req: RequestWithUser, options: AuthenticateOptions & AuthorizeOptions) { - const request = await this.generateLogoutRequest(req); + async getLogoutUrlAsync( + user: Profile, + RelayState: string, + options: AuthenticateOptions & AuthorizeOptions + ) { + const request = await this._generateLogoutRequest(user); const operation = "logout"; const overrideParams = options ? options.additionalParams || {} : {}; - return await this.requestToUrlAsync( + return await this._requestToUrlAsync( request, null, operation, - this.getAdditionalParams(req, operation, overrideParams) + this._getAdditionalParams(RelayState, operation, overrideParams) ); } getLogoutResponseUrl( - req: RequestWithUser, + samlLogoutRequest: Profile, + RelayState: string, options: AuthenticateOptions & AuthorizeOptions, callback: (err: Error | null, url?: string | null) => void ): void { - util.callbackify(() => this.getLogoutResponseUrlAsync(req, options))(callback); + util.callbackify(() => this.getLogoutResponseUrlAsync(samlLogoutRequest, RelayState, options))( + callback + ); } - async getLogoutResponseUrlAsync( - req: RequestWithUser, - options: AuthenticateOptions & AuthorizeOptions + private async getLogoutResponseUrlAsync( + samlLogoutRequest: Profile, + RelayState: string, + options: AuthenticateOptions & AuthorizeOptions // add RelayState, ): Promise { - const response = this.generateLogoutResponse(req, req.samlLogoutRequest); + const response = this._generateLogoutResponse(samlLogoutRequest); const operation = "logout"; const overrideParams = options ? options.additionalParams || {} : {}; - return await this.requestToUrlAsync( + return await this._requestToUrlAsync( null, response, operation, - this.getAdditionalParams(req, operation, overrideParams) + this._getAdditionalParams(RelayState, operation, overrideParams) ); } - certToPEM(cert: string): string { + _certToPEM(cert: string): string { cert = cert.match(/.{1,64}/g)!.join("\n"); if (cert.indexOf("-BEGIN CERTIFICATE-") === -1) cert = "-----BEGIN CERTIFICATE-----\n" + cert; @@ -648,7 +652,7 @@ class SAML { return cert; } - async certsToCheck(): Promise { + private async certsToCheck(): Promise { let checkedCerts: string[]; if (typeof this.options.cert === "function") { @@ -712,7 +716,7 @@ class SAML { sig.keyInfoProvider = { file: "", getKeyInfo: () => "", - getKey: () => Buffer.from(this.certToPEM(cert)), + getKey: () => Buffer.from(this._certToPEM(cert)), }; signature = this.normalizeNewlines(signature.toString()); sig.loadSignature(signature); @@ -907,7 +911,7 @@ class SAML { } } - async validateInResponseTo(inResponseTo: string | null): Promise { + private async validateInResponseTo(inResponseTo: string | null): Promise { if (this.options.validateInResponseTo) { if (inResponseTo) { const result = await this.cacheProvider.getAsync(inResponseTo); @@ -945,7 +949,7 @@ class SAML { return await processValidlySignedSamlLogoutAsync(this, doc, dom); } - async hasValidSignatureForRedirect( + private async hasValidSignatureForRedirect( container: ParsedQs, originalQuery: string | null ): Promise { @@ -983,7 +987,7 @@ class SAML { } } - validateSignatureForRedirect( + private validateSignatureForRedirect( urlString: crypto.BinaryLike, signature: string, alg: string, @@ -1007,10 +1011,10 @@ class SAML { const verifier = crypto.createVerify(matchingAlgo); verifier.update(urlString); - return verifier.verify(this.certToPEM(cert), signature, "base64"); + return verifier.verify(this._certToPEM(cert), signature, "base64"); } - verifyLogoutRequest(doc: XMLOutput) { + private verifyLogoutRequest(doc: XMLOutput) { this.verifyIssuer(doc.LogoutRequest); const nowMs = new Date().getTime(); const conditions = doc.LogoutRequest.$; @@ -1024,7 +1028,7 @@ class SAML { } } - async verifyLogoutResponse(doc: XMLOutput) { + private async verifyLogoutResponse(doc: XMLOutput) { const statusCode = doc.LogoutResponse.Status[0].StatusCode[0].$.Value; if (statusCode !== "urn:oasis:names:tc:SAML:2.0:status:Success") throw new Error("Bad status code: " + statusCode); @@ -1038,7 +1042,7 @@ class SAML { return true; } - verifyIssuer(samlMessage: XMLOutput) { + private verifyIssuer(samlMessage: XMLOutput) { if (this.options.idpIssuer != null) { const issuer = samlMessage.Issuer; if (issuer) { @@ -1052,7 +1056,7 @@ class SAML { } } - async processValidlySignedAssertionAsync( + private async processValidlySignedAssertionAsync( xml: xml2js.convertableToString, samlResponseXml: string, inResponseTo: string @@ -1231,7 +1235,7 @@ class SAML { return { profile, loggedOut: false }; } - checkTimestampsValidityError(nowMs: number, notBefore: string, notOnOrAfter: string) { + private checkTimestampsValidityError(nowMs: number, notBefore: string, notOnOrAfter: string) { if (this.options.acceptedClockSkewMs == -1) return null; if (notBefore) { @@ -1248,7 +1252,7 @@ class SAML { return null; } - checkAudienceValidityError( + private checkAudienceValidityError( expectedAudience: string, audienceRestrictions: AudienceRestrictionXML[] ) { @@ -1293,7 +1297,7 @@ class SAML { return await processValidlySignedPostRequestAsync(this, doc, dom); } - async getNameIdAsync(self: SAML, doc: Node): Promise { + async _getNameIdAsync(self: SAML, doc: Node): Promise { const nameIds = xmlCrypto.xpath( doc, "/*[local-name()='LogoutRequest']/*[local-name()='NameID']" @@ -1427,14 +1431,14 @@ class SAML { "@index": "1", "@isDefault": "true", "@Binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - "@Location": this.getCallbackUrl({}), + "@Location": this.getCallbackUrl(), }; return xmlbuilder .create((metadata as unknown) as Record) .end({ pretty: true, indent: " ", newline: "\n" }); } - keyToPEM(key: string | Buffer): typeof key extends string | Buffer ? string | Buffer : Error { + _keyToPEM(key: string | Buffer): typeof key extends string | Buffer ? string | Buffer : Error { key = assertRequired(key, "key is required"); if (typeof key !== "string") return key; diff --git a/src/node-saml/types.ts b/src/node-saml/types.ts new file mode 100644 index 00000000..3767154b --- /dev/null +++ b/src/node-saml/types.ts @@ -0,0 +1,119 @@ +import type { CacheProvider } from "./inmemory-cache-provider"; + +export type SignatureAlgorithm = "sha1" | "sha256" | "sha512"; + +export interface SamlSigningOptions { + privateKey?: string | Buffer; + signatureAlgorithm?: SignatureAlgorithm; + xmlSignatureTransforms?: string[]; + digestAlgorithm?: string; +} + +export interface AudienceRestrictionXML { + Audience?: XMLObject[]; +} + +export type XMLValue = string | number | boolean | null | XMLObject | XMLValue[]; + +export type XMLObject = { + [key: string]: XMLValue; +}; + +export type XMLInput = XMLObject; + +export type XMLOutput = Record; + +export interface AuthorizeRequestXML { + "samlp:AuthnRequest": XMLInput; +} + +export type CertCallback = ( + callback: (err: Error | null, cert?: string | string[]) => void +) => void; + +/** + * These are SAML options that must be provided to construct a new SAML Strategy + */ +export interface MandatorySamlOptions { + cert: string | string[] | CertCallback; +} + +export interface SamlIDPListConfig { + entries: SamlIDPEntryConfig[]; + getComplete?: string; +} + +export interface SamlIDPEntryConfig { + providerId: string; + name?: string; + loc?: string; +} + +export interface LogoutRequestXML { + "samlp:LogoutRequest": { + "saml:NameID": XMLInput; + [key: string]: XMLValue; + }; +} + +export interface ServiceMetadataXML { + EntityDescriptor: { + [key: string]: XMLValue; + SPSSODescriptor: XMLObject; + }; +} + +export type RacComparision = "exact" | "minimum" | "maximum" | "better"; + +interface SamlScopingConfig { + idpList?: SamlIDPListConfig[]; + proxyCount?: number; + requesterId?: string[] | string; +} + +/** + * The options required to use a SAML strategy + * These may be provided by means of defaults specified in the constructor + */ +export interface SamlOptions extends SamlSigningOptions, MandatorySamlOptions { + // Core + callbackUrl?: string; + path: string; + protocol?: string; + host: string; + entryPoint?: string; + issuer: string; + decryptionPvk?: string | Buffer; + + // Additional SAML behaviors + additionalParams: Record; + additionalAuthorizeParams: Record; + identifierFormat?: string | null; + acceptedClockSkewMs: number; + attributeConsumingServiceIndex?: string; + disableRequestedAuthnContext: boolean; + authnContext: string[]; + forceAuthn: boolean; + skipRequestCompression: boolean; + authnRequestBinding?: string; + racComparison: RacComparision; + providerName?: string; + passive: boolean; + idpIssuer?: string; + audience?: string; + scoping?: SamlScopingConfig; + wantAssertionsSigned?: boolean; + + // InResponseTo Validation + validateInResponseTo: boolean; + requestIdExpirationPeriodMs: number; + cacheProvider: CacheProvider; + + // Logout + logoutUrl: string; + additionalLogoutParams: Record; + logoutCallbackUrl?: string; + + // extras + disableRequestAcsUrl: boolean; +} diff --git a/src/passport-saml/utility.ts b/src/node-saml/utility.ts similarity index 100% rename from src/passport-saml/utility.ts rename to src/node-saml/utility.ts diff --git a/src/passport-saml/index.ts b/src/passport-saml/index.ts index 24e48ba4..e4ddf280 100644 --- a/src/passport-saml/index.ts +++ b/src/passport-saml/index.ts @@ -1,5 +1,5 @@ -import type { CacheItem, CacheProvider } from "./inmemory-cache-provider"; -import { SAML } from "./saml"; +import type { CacheItem, CacheProvider } from "../node-saml/inmemory-cache-provider"; +import { SAML } from "../node-saml"; import Strategy = require("./strategy"); import MultiSamlStrategy = require("./multiSamlStrategy"); import type { diff --git a/src/passport-saml/multiSamlStrategy.ts b/src/passport-saml/multiSamlStrategy.ts index ef07ab6f..8ec92cfc 100644 --- a/src/passport-saml/multiSamlStrategy.ts +++ b/src/passport-saml/multiSamlStrategy.ts @@ -1,11 +1,9 @@ import * as util from "util"; -import * as saml from "./saml"; -import { CacheProvider as InMemoryCacheProvider } from "./inmemory-cache-provider"; +import { SAML } from "../node-saml"; import SamlStrategy = require("./strategy"); import type { Request } from "express"; import { AuthenticateOptions, - AuthorizeOptions, MultiSamlConfig, RequestWithUser, SamlConfig, @@ -42,7 +40,7 @@ class MultiSamlStrategy extends SamlStrategy { return this.error(err); } - const samlService = new saml.SAML({ ...this._options, ...samlOptions }); + const samlService = new SAML({ ...this._options, ...samlOptions }); const strategy = Object.assign({}, this, { _saml: samlService }); Object.setPrototypeOf(strategy, this); super.authenticate.call(strategy, req, options); @@ -58,7 +56,7 @@ class MultiSamlStrategy extends SamlStrategy { return callback(err); } - const samlService = new saml.SAML(Object.assign({}, this._options, samlOptions)); + const samlService = new SAML(Object.assign({}, this._options, samlOptions)); const strategy = Object.assign({}, this, { _saml: samlService }); Object.setPrototypeOf(strategy, this); super.logout.call(strategy, req, callback); @@ -81,7 +79,7 @@ class MultiSamlStrategy extends SamlStrategy { return callback(err); } - const samlService = new saml.SAML(Object.assign({}, this._options, samlOptions)); + const samlService = new SAML(Object.assign({}, this._options, samlOptions)); const strategy = Object.assign({}, this, { _saml: samlService }); Object.setPrototypeOf(strategy, this); return callback( diff --git a/src/passport-saml/strategy.ts b/src/passport-saml/strategy.ts index 8feac264..303bd0b5 100644 --- a/src/passport-saml/strategy.ts +++ b/src/passport-saml/strategy.ts @@ -1,12 +1,10 @@ import { Strategy as PassportStrategy } from "passport-strategy"; -import * as saml from "./saml"; +import { SAML } from "../node-saml"; import * as url from "url"; import { AuthenticateOptions, - AuthorizeOptions, RequestWithUser, SamlConfig, - StrategyOptions, VerifyWithoutRequest, VerifyWithRequest, } from "./types"; @@ -17,7 +15,7 @@ class Strategy extends PassportStrategy { name: string; _verify: VerifyWithRequest | VerifyWithoutRequest; - _saml: saml.SAML | undefined; + _saml: SAML | undefined; _passReqToCallback?: boolean; constructor(options: SamlConfig, verify: VerifyWithRequest); @@ -42,7 +40,7 @@ class Strategy extends PassportStrategy { this._verify = verify; if ((this.constructor as typeof Strategy).newSamlProviderOnConstruct) { - this._saml = new saml.SAML(options); + this._saml = new SAML(options); } this._passReqToCallback = !!options.passReqToCallback; } @@ -67,8 +65,9 @@ class Strategy extends PassportStrategy { throw new Error("Can't get logout response URL without a SAML provider defined."); } - req.samlLogoutRequest = profile; - return this._saml.getLogoutResponseUrl(req, options, redirectIfSuccess); + const RelayState = + (req.query && req.query.RelayState) || (req.body && req.body.RelayState); + return this._saml.getLogoutResponseUrl(profile, RelayState, options, redirectIfSuccess); } return this.pass(); } @@ -128,13 +127,16 @@ class Strategy extends PassportStrategy { throw new Error("Can't process login request without a SAML provider defined."); } + const RelayState = + (req.query && req.query.RelayState) || (req.body && req.body.RelayState); + const host = req.headers && req.headers.host; if (this._saml.options.authnRequestBinding === "HTTP-POST") { - const data = await this._saml.getAuthorizeFormAsync(req); + const data = await this._saml.getAuthorizeFormAsync(RelayState, host); const res = req.res!; res.send(data); } else { // Defaults to HTTP-Redirect - this.redirect(await this._saml.getAuthorizeUrlAsync(req, options)); + this.redirect(await this._saml.getAuthorizeUrlAsync(RelayState, host, options)); } } catch (err) { this.error(err); @@ -146,7 +148,11 @@ class Strategy extends PassportStrategy { } try { - this.redirect(await this._saml.getLogoutUrlAsync(req, options)); + const RelayState = + (req.query && req.query.RelayState) || (req.body && req.body.RelayState); + this.redirect( + await this._saml.getLogoutUrlAsync(req.user as Profile, RelayState, options) + ); } catch (err) { this.error(err); } @@ -165,9 +171,9 @@ class Strategy extends PassportStrategy { if (this._saml == null) { throw new Error("Can't logout without a SAML provider defined."); } - + const RelayState = (req.query && req.query.RelayState) || (req.body && req.body.RelayState); this._saml - .getLogoutUrlAsync(req, {}) + .getLogoutUrlAsync(req.user as Profile, RelayState, {}) .then((url) => callback(null, url)) .catch((err) => callback(err)); } diff --git a/src/passport-saml/types.ts b/src/passport-saml/types.ts index e10a5bf8..b5dbd72d 100644 --- a/src/passport-saml/types.ts +++ b/src/passport-saml/types.ts @@ -1,12 +1,6 @@ import type * as express from "express"; import * as passport from "passport"; -import type { CacheProvider } from "./inmemory-cache-provider"; - -export type CertCallback = ( - callback: (err: Error | null, cert?: string | string[]) => void -) => void; -export type RacComparision = "exact" | "minimum" | "maximum" | "better"; -export type SignatureAlgorithm = "sha1" | "sha256" | "sha512"; +import type { SamlOptions, MandatorySamlOptions, SamlIDPListConfig } from "../node-saml/types"; export interface AuthenticateOptions extends passport.AuthenticateOptions { samlFallback?: "login-request" | "logout-request"; @@ -17,66 +11,6 @@ export interface AuthorizeOptions extends AuthenticateOptions { samlFallback?: "login-request" | "logout-request"; } -export interface SamlSigningOptions { - privateKey?: string | Buffer; - signatureAlgorithm?: SignatureAlgorithm; - xmlSignatureTransforms?: string[]; - digestAlgorithm?: string; -} - -/** - * These are SAML options that must be provided to construct a new SAML Strategy - */ -export interface MandatorySamlOptions { - cert: string | string[] | CertCallback; -} - -/** - * The options required to use a SAML strategy - * These may be provided by means of defaults specified in the constructor - */ -export interface SamlOptions extends SamlSigningOptions, MandatorySamlOptions { - // Core - callbackUrl?: string; - path: string; - protocol?: string; - host: string; - entryPoint?: string; - issuer: string; - decryptionPvk?: string | Buffer; - - // Additional SAML behaviors - additionalParams: Record; - additionalAuthorizeParams: Record; - identifierFormat?: string | null; - acceptedClockSkewMs: number; - attributeConsumingServiceIndex?: string; - disableRequestedAuthnContext: boolean; - authnContext: string[]; - forceAuthn: boolean; - skipRequestCompression: boolean; - authnRequestBinding?: string; - racComparison: RacComparision; - providerName?: string; - passive: boolean; - idpIssuer?: string; - audience?: string; - scoping?: SamlScopingConfig; - wantAssertionsSigned?: boolean; - - // InResponseTo Validation - validateInResponseTo: boolean; - requestIdExpirationPeriodMs: number; - cacheProvider: CacheProvider; - - // Logout - logoutUrl: string; - additionalLogoutParams: Record; - logoutCallbackUrl?: string; - - // extras - disableRequestAcsUrl: boolean; -} export interface StrategyOptions { name?: string; passReqToCallback?: boolean; @@ -87,55 +21,6 @@ export interface StrategyOptions { */ export type SamlConfig = Partial & StrategyOptions & MandatorySamlOptions; -export interface SamlScopingConfig { - idpList?: SamlIDPListConfig[]; - proxyCount?: number; - requesterId?: string[] | string; -} - -export type XMLValue = string | number | boolean | null | XMLObject | XMLValue[]; - -export type XMLObject = { - [key: string]: XMLValue; -}; - -export type XMLInput = XMLObject; - -export interface AuthorizeRequestXML { - "samlp:AuthnRequest": XMLInput; -} - -export interface LogoutRequestXML { - "samlp:LogoutRequest": { - "saml:NameID": XMLInput; - [key: string]: XMLValue; - }; -} - -export interface ServiceMetadataXML { - EntityDescriptor: { - [key: string]: XMLValue; - SPSSODescriptor: XMLObject; - }; -} - -export interface AudienceRestrictionXML { - Audience?: XMLObject[]; -} - -export type XMLOutput = Record; - -export interface SamlIDPListConfig { - entries: SamlIDPEntryConfig[]; - getComplete?: string; -} - -export interface SamlIDPEntryConfig { - providerId: string; - name?: string; - loc?: string; -} - export interface Profile { issuer?: string; sessionIndex?: string; diff --git a/test/saml-post-signing-tests.spec.ts b/test/node-saml/saml-post-signing-tests.spec.ts similarity index 92% rename from test/saml-post-signing-tests.spec.ts rename to test/node-saml/saml-post-signing-tests.spec.ts index 879a5db5..c713aa39 100644 --- a/test/saml-post-signing-tests.spec.ts +++ b/test/node-saml/saml-post-signing-tests.spec.ts @@ -1,8 +1,9 @@ import * as fs from "fs"; -import { signSamlPost, signAuthnRequestPost } from "../src/passport-saml/saml-post-signing"; -import { SamlOptions, SamlSigningOptions } from "../src/passport-saml/types"; +import * as should from "should"; +import { signSamlPost, signAuthnRequestPost } from "../../src/node-saml/saml-post-signing"; +import { SamlSigningOptions } from "../../src/node-saml/types"; -const signingKey = fs.readFileSync(__dirname + "/static/key.pem"); +const signingKey = fs.readFileSync(__dirname + "/../static/key.pem"); describe("SAML POST Signing", function () { it("should sign a simple saml request", function () { diff --git a/test/samlTests.spec.ts b/test/node-saml/samlTests.spec.ts similarity index 74% rename from test/samlTests.spec.ts rename to test/node-saml/samlTests.spec.ts index bd20826e..536f84ac 100644 --- a/test/samlTests.spec.ts +++ b/test/node-saml/samlTests.spec.ts @@ -3,10 +3,15 @@ import * as fs from "fs"; import * as url from "url"; import * as should from "should"; import assert = require("assert"); -import { SAML } from "../src/passport-saml/saml"; -import { RequestWithUser, AuthenticateOptions, AuthorizeOptions } from "../src/passport-saml/types"; -import { assertRequired } from "../src/passport-saml/utility"; -import { FAKE_CERT } from "./types"; +import { SAML } from "../../src/node-saml/saml"; +import { + RequestWithUser, + AuthenticateOptions, + AuthorizeOptions, + Profile, +} from "../../src/passport-saml/types"; +import { assertRequired } from "../../src/node-saml/utility"; +import { FAKE_CERT } from "../types"; describe("SAML.js", function () { describe("get Urls", function () { @@ -41,23 +46,23 @@ describe("SAML.js", function () { describe("getAuthorizeUrl", function () { it("calls callback with right host", async () => { - const target = await saml.getAuthorizeUrlAsync(req, {}); + const target = await saml.getAuthorizeUrlAsync("", req.headers.host, {}); url.parse(target!).host!.should.equal("exampleidp.com"); }); it("calls callback with right protocol", async () => { - const target = await saml.getAuthorizeUrlAsync(req, {}); + const target = await saml.getAuthorizeUrlAsync("", req.headers.host, {}); url.parse(target!).protocol!.should.equal("https:"); }); it("calls callback with right path", async () => { - const target = await saml.getAuthorizeUrlAsync(req, {}); + const target = await saml.getAuthorizeUrlAsync("", req.headers.host, {}); url.parse(target!).pathname!.should.equal("/path"); }); it("calls callback with original query string", async () => { - const target = await saml.getAuthorizeUrlAsync(req, {}); + const target = await saml.getAuthorizeUrlAsync("", req.headers.host, {}); url.parse(target!, true).query["key"]!.should.equal("value"); }); it("calls callback with additional run-time params in query string", async () => { - const target = await saml.getAuthorizeUrlAsync(req, options); + const target = await saml.getAuthorizeUrlAsync("", req.headers.host, options); Object.keys(url.parse(target!, true).query).should.have.length(3); url.parse(target!, true).query["key"]!.should.equal("value"); url.parse(target!, true).query["SAMLRequest"]!.should.not.be.empty(); @@ -65,30 +70,35 @@ describe("SAML.js", function () { }); // NOTE: This test only tests existence of the assertion, not the correctness it("calls callback with saml request object", async () => { - const target = await saml.getAuthorizeUrlAsync(req, {}); + const target = await saml.getAuthorizeUrlAsync("", req.headers.host, {}); should(url.parse(target!, true).query).have.property("SAMLRequest"); }); }); describe("getLogoutUrl", function () { it("calls callback with right host", async () => { - const target = await saml.getLogoutUrlAsync(req, {}); + req.user = assertRequired(req.user); + const target = await saml.getLogoutUrlAsync(req.user, "", {}); url.parse(target!).host!.should.equal("exampleidp.com"); }); it("calls callback with right protocol", async () => { - const target = await saml.getLogoutUrlAsync(req, {}); + req.user = assertRequired(req.user); + const target = await saml.getLogoutUrlAsync(req.user, "", {}); url.parse(target!).protocol!.should.equal("https:"); }); it("calls callback with right path", async () => { - const target = await saml.getLogoutUrlAsync(req, {}); + req.user = assertRequired(req.user); + const target = await saml.getLogoutUrlAsync(req.user, "", {}); url.parse(target!).pathname!.should.equal("/path"); }); it("calls callback with original query string", async () => { - const target = await saml.getLogoutUrlAsync(req, {}); + req.user = assertRequired(req.user); + const target = await saml.getLogoutUrlAsync(req.user, "", {}); url.parse(target!, true).query["key"]!.should.equal("value"); }); it("calls callback with additional run-time params in query string", async () => { - const target = await saml.getLogoutUrlAsync(req, options); + req.user = assertRequired(req.user); + const target = await saml.getLogoutUrlAsync(req.user, "", options); Object.keys(url.parse(target!, true).query).should.have.length(3); url.parse(target!, true).query["key"]!.should.equal("value"); url.parse(target!, true).query["SAMLRequest"]!.should.not.be.empty(); @@ -96,14 +106,15 @@ describe("SAML.js", function () { }); // NOTE: This test only tests existence of the assertion, not the correctness it("calls callback with saml request object", async () => { - const target = await saml.getLogoutUrlAsync(req, {}); + req.user = assertRequired(req.user); + const target = await saml.getLogoutUrlAsync(req.user, "", {}); should(url.parse(target!, true).query).have.property("SAMLRequest"); }); }); describe("getLogoutResponseUrl", function () { it("calls callback with right host", function (done) { - saml.getLogoutResponseUrl(req, {}, function (err, target) { + saml.getLogoutResponseUrl(req.samlLogoutRequest, "", {}, function (err, target) { should.not.exist(err); try { target = assertRequired(target); @@ -116,7 +127,7 @@ describe("SAML.js", function () { }); }); it("calls callback with right protocol", function (done) { - saml.getLogoutResponseUrl(req, {}, function (err, target) { + saml.getLogoutResponseUrl(req.samlLogoutRequest, "", {}, function (err, target) { should.not.exist(err); try { target = assertRequired(target); @@ -129,7 +140,7 @@ describe("SAML.js", function () { }); }); it("calls callback with right path", function (done) { - saml.getLogoutResponseUrl(req, {}, function (err, target) { + saml.getLogoutResponseUrl(req.samlLogoutRequest, "", {}, function (err, target) { should.not.exist(err); try { target = assertRequired(target); @@ -142,7 +153,7 @@ describe("SAML.js", function () { }); }); it("calls callback with original query string", function (done) { - saml.getLogoutResponseUrl(req, {}, function (err, target) { + saml.getLogoutResponseUrl(req.samlLogoutRequest, "", {}, function (err, target) { should.not.exist(err); try { target = assertRequired(target); @@ -155,7 +166,7 @@ describe("SAML.js", function () { }); }); it("calls callback with additional run-time params in query string", function (done) { - saml.getLogoutResponseUrl(req, options, function (err, target) { + saml.getLogoutResponseUrl(req.samlLogoutRequest, "", options, function (err, target) { should.not.exist(err); try { target = assertRequired(target); @@ -171,7 +182,7 @@ describe("SAML.js", function () { }); // NOTE: This test only tests existence of the assertion, not the correctness it("calls callback with saml response object", function (done) { - saml.getLogoutResponseUrl(req, {}, function (err, target) { + saml.getLogoutResponseUrl(req.samlLogoutRequest, "", {}, function (err, target) { should.not.exist(err); try { target = assertRequired(target); @@ -185,27 +196,27 @@ describe("SAML.js", function () { }); }); - describe("keyToPEM", function () { + describe("_keyToPEM", function () { const [regular, singleline] = ["acme_tools_com.key", "singleline_acme_tools_com.key"].map( keyFromFile ); it("formats singleline keys properly", function () { - const result = saml.keyToPEM(singleline); + const result = saml._keyToPEM(singleline); result.should.equal(regular); }); it("passes all other multiline keys", function () { - const result = saml.keyToPEM(regular); + const result = saml._keyToPEM(regular); result.should.equal(regular); }); it("fails with falsy", function () { - assert.throws(() => saml.keyToPEM(null as any)); + assert.throws(() => saml._keyToPEM(null as any)); }); it("does nothing to non strings", function () { - const result = saml.keyToPEM(1 as any); + const result = saml._keyToPEM(1 as any); should.equal(result, 1); }); }); diff --git a/test/test-signatures.spec.ts b/test/node-saml/test-signatures.spec.ts similarity index 93% rename from test/test-signatures.spec.ts rename to test/node-saml/test-signatures.spec.ts index 71926dec..bf066397 100644 --- a/test/test-signatures.spec.ts +++ b/test/node-saml/test-signatures.spec.ts @@ -1,16 +1,16 @@ -import { SAML } from "../src/passport-saml"; +import { SAML } from "../../src/node-saml"; import * as fs from "fs"; import * as sinon from "sinon"; -import { SamlConfig } from "../src/passport-saml/types"; +import { SamlConfig } from "../../src/passport-saml/types"; import assert = require("assert"); -const cert = fs.readFileSync(__dirname + "/static/cert.pem", "ascii"); +const cert = fs.readFileSync(__dirname + "/../static/cert.pem", "ascii"); describe("Signatures", function () { const INVALID_SIGNATURE = "Invalid signature", INVALID_ENCRYPTED_SIGNATURE = "Invalid signature from encrypted assertion", createBody = (pathToXml: string) => ({ - SAMLResponse: fs.readFileSync(__dirname + "/static/signatures" + pathToXml, "base64"), + SAMLResponse: fs.readFileSync(__dirname + "/../static/signatures" + pathToXml, "base64"), }), testOneResponseBody = async ( samlResponseBody: Record, @@ -95,7 +95,7 @@ describe("Signatures", function () { INVALID_ENCRYPTED_SIGNATURE, 2, { - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), wantAssertionsSigned: true, } ) @@ -118,7 +118,7 @@ describe("Signatures", function () { INVALID_ENCRYPTED_SIGNATURE, 2, { - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), wantAssertionsSigned: true, } ) @@ -264,7 +264,7 @@ describe("Signatures", function () { describe("Signature on saml:Response with non-LF line endings", () => { const samlResponseXml = fs .readFileSync( - __dirname + "/static/signatures/valid/response.root-signed.assertion-signed.xml" + __dirname + "/../static/signatures/valid/response.root-signed.assertion-signed.xml" ) .toString(); const makeBody = (str: string) => ({ SAMLResponse: Buffer.from(str).toString("base64") }); diff --git a/test/tests.spec.ts b/test/node-saml/tests.spec.ts similarity index 92% rename from test/tests.spec.ts rename to test/node-saml/tests.spec.ts index 8ab2bd40..f7dfedfd 100644 --- a/test/tests.spec.ts +++ b/test/node-saml/tests.spec.ts @@ -1,23 +1,23 @@ "use strict"; -import * as express from "express"; -import { Strategy as SamlStrategy, SAML } from "../src/passport-saml"; +import { Strategy as SamlStrategy, SAML } from "../../src/passport-saml"; import url = require("url"); import * as querystring from "querystring"; import { parseString } from "xml2js"; import * as fs from "fs"; import * as sinon from "sinon"; -import { RacComparision, RequestWithUser, SamlConfig } from "../src/passport-saml/types.js"; +import { SamlConfig, Profile } from "../../src/passport-saml/types.js"; +import { RacComparision } from "../../src/node-saml/types.js"; import * as should from "should"; import assert = require("assert"); -import { FAKE_CERT, TEST_CERT } from "./types"; -import { signXmlResponse } from "../src/passport-saml/utility"; +import { FAKE_CERT, TEST_CERT } from "../types"; +import { signXmlResponse } from "../../src/node-saml/utility"; export const BAD_TEST_CERT = "MIIEOTCCAyGgAwIBAgIJAKZgJdKdCdL6MA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNVBAYTAkFVMREwDwYDVQQIEwhWaWN0b3JpYTESMBAGA1UEBxMJTWVsYm91cm5lMSEwHwYDVQQKExhUYWJjb3JwIEhvbGRpbmdzIExpbWl0ZWQxFzAVBgNVBAMTDnN0cy50YWIuY29tLmF1MB4XDTE3MDUzMDA4NTQwOFoXDTI3MDUyODA4NTQwOFowcDELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlNZWxib3VybmUxITAfBgNVBAoTGFRhYmNvcnAgSG9sZGluZ3MgTGltaXRlZDEXMBUGA1UEAxMOc3RzLnRhYi5jb20uYXUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0NuMcflq3rtupKYDf4a7lWmsXy66fYe9n8jB2DuLMakEJBlzn9j6B98IZftrilTq21VR7wUXROxG8BkN8IHY+l8X7lATmD28fFdZJj0c8Qk82eoq48faemth4fBMx2YrpnhU00jeXeP8dIIaJTPCHBTNgZltMMhphklN1YEPlzefJs3YD+Ryczy1JHbwETxt+BzO1JdjBe1fUTyl6KxAwWvtsNBURmQRYlDOk4GRgdkQnfxBuCpOMeOpV8wiBAi3h65Lab9C5avu4AJlA9e4qbOmWt6otQmgy5fiJVy6bH/d8uW7FJmSmePX9sqAWa9szhjdn36HHVQsfHC+IUEX7AgMBAAGjgdUwgdIwHQYDVR0OBBYEFN6z6cuxY7FTkg1S/lIjnS4x5ARWMIGiBgNVHSMEgZowgZeAFN6z6cuxY7FTkg1S/lIjnS4x5ARWoXSkcjBwMQswCQYDVQQGEwJBVTERMA8GA1UECBMIVmljdG9yaWExEjAQBgNVBAcTCU1lbGJvdXJuZTEhMB8GA1UEChMYVGFiY29ycCBIb2xkaW5ncyBMaW1pdGVkMRcwFQYDVQQDEw5zdHMudGFiLmNvbS5hdYIJAKZgJdKdCdL6MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAMi5HyvXgRa4+kKz3dk4SwAEXzeZRcsbeDJWVUxdb6a+JQxIoG7L9rSbd6yZvP/Xel5TrcwpCpl5eikzXB02/C0wZKWicNmDEBlOfw0Pc5ngdoh6ntxHIWm5QMlAfjR0dgTlojN4Msw2qk7cP1QEkV96e2BJUaqaNnM3zMvd7cfRjPNfbsbwl6hCCCAdwrALKYtBnjKVrCGPwO+xiw5mUJhZ1n6ZivTOdQEWbl26UO60J9ItiWP8VK0d0aChn326Ovt7qC4S3AgDlaJwcKe5Ifxl/UOWePGRwXj2UUuDWFhjtVmRntMmNZbe5yE8MkEvU+4/c6LqGwTCgDenRbK53Dgg"; export const noop = (): void => undefined; -describe("passport-saml /", function () { +describe("node-saml /", function () { describe("saml.js / ", function () { it("should throw an error if cert property is provided to saml constructor but is empty", function () { should(function () { @@ -26,14 +26,14 @@ describe("passport-saml /", function () { }).throw("cert is required"); }); - it("generateUniqueID should generate 20 char IDs", function () { + it("_generateUniqueID should generate 20 char IDs", function () { const samlObj = new SAML({ entryPoint: "foo", cert: FAKE_CERT }); for (let i = 0; i < 200; i++) { - samlObj.generateUniqueID().length.should.eql(20); + samlObj._generateUniqueID().length.should.eql(20); } }); - it("generateLogoutRequest", function (done) { + it("_generateLogoutRequest", function (done) { try { const expectedRequest = { "samlp:LogoutRequest": { @@ -53,12 +53,10 @@ describe("passport-saml /", function () { }; const samlObj = new SAML({ entryPoint: "foo", cert: FAKE_CERT }); - const logoutRequestPromise = samlObj.generateLogoutRequest({ - user: { - nameIDFormat: "foo", - nameID: "bar", - }, - } as RequestWithUser); + const logoutRequestPromise = samlObj._generateLogoutRequest({ + nameIDFormat: "foo", + nameID: "bar", + }); logoutRequestPromise .then(function (logoutRequest) { @@ -81,7 +79,7 @@ describe("passport-saml /", function () { } }); - it("generateLogoutRequest adds the NameQualifier and SPNameQualifier to the saml request", function (done) { + it("_generateLogoutRequest adds the NameQualifier and SPNameQualifier to the saml request", function (done) { try { const expectedRequest = { "samlp:LogoutRequest": { @@ -110,14 +108,12 @@ describe("passport-saml /", function () { }; const samlObj = new SAML({ entryPoint: "foo", cert: FAKE_CERT }); - const logoutRequestPromise = samlObj.generateLogoutRequest({ - user: { - nameIDFormat: "foo", - nameID: "bar", - nameQualifier: "Identity Provider", - spNameQualifier: "Service Provider", - }, - } as RequestWithUser); + const logoutRequestPromise = samlObj._generateLogoutRequest({ + nameIDFormat: "foo", + nameID: "bar", + nameQualifier: "Identity Provider", + spNameQualifier: "Service Provider", + }); logoutRequestPromise .then(function (logoutRequest) { @@ -140,7 +136,7 @@ describe("passport-saml /", function () { } }); - it("generateLogoutResponse", function (done) { + it("_generateLogoutResponse", function (done) { const expectedResponse = { "samlp:LogoutResponse": { $: { @@ -162,7 +158,7 @@ describe("passport-saml /", function () { }; const samlObj = new SAML({ entryPoint: "foo", cert: FAKE_CERT }); - const logoutRequest = samlObj.generateLogoutResponse({} as express.Request, { ID: "quux" }); + const logoutRequest = samlObj._generateLogoutResponse({ ID: "quux" }); parseString(logoutRequest, function (err, doc) { try { delete doc["samlp:LogoutResponse"]["$"]["ID"]; @@ -175,7 +171,7 @@ describe("passport-saml /", function () { }); }); - it("generateLogoutRequest with session index", function (done) { + it("_generateLogoutRequest with session index", function (done) { try { const expectedRequest = { "samlp:LogoutRequest": { @@ -198,13 +194,11 @@ describe("passport-saml /", function () { }; const samlObj = new SAML({ entryPoint: "foo", cert: FAKE_CERT }); - const logoutRequestPromise = samlObj.generateLogoutRequest({ - user: { - nameIDFormat: "foo", - nameID: "bar", - sessionIndex: "session-id", - }, - } as RequestWithUser); + const logoutRequestPromise = samlObj._generateLogoutRequest({ + nameIDFormat: "foo", + nameID: "bar", + sessionIndex: "session-id", + }); logoutRequestPromise .then(function (logoutRequest) { @@ -227,7 +221,7 @@ describe("passport-saml /", function () { } }); - it("generateLogoutRequest saves id and instant to cache", function (done) { + it("_generateLogoutRequest saves id and instant to cache", function (done) { const expectedRequest = { "samlp:LogoutRequest": { $: { @@ -250,13 +244,11 @@ describe("passport-saml /", function () { const samlObj = new SAML({ entryPoint: "foo", cert: FAKE_CERT }); const cacheSaveSpy = sinon.spy(samlObj.cacheProvider, "saveAsync"); - const logoutRequestPromise = samlObj.generateLogoutRequest({ - user: { - nameIDFormat: "foo", - nameID: "bar", - sessionIndex: "session-id", - }, - } as RequestWithUser); + const logoutRequestPromise = samlObj._generateLogoutRequest({ + nameIDFormat: "foo", + nameID: "bar", + sessionIndex: "session-id", + }); logoutRequestPromise.then(function (logoutRequest) { parseString(logoutRequest, function (err, doc) { @@ -284,7 +276,7 @@ describe("passport-saml /", function () { ) { const samlObj = new SAML(samlConfig); const decryptionCert = fs.readFileSync( - __dirname + "/static/testshib encryption cert.pem", + __dirname + "/../static/testshib encryption cert.pem", "utf-8" ); let metadata = samlObj.generateServiceProviderMetadata(decryptionCert, signingCert); @@ -302,11 +294,11 @@ describe("passport-saml /", function () { issuer: "http://example.serviceprovider.com", callbackUrl: "http://example.serviceprovider.com/saml/callback", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), cert: FAKE_CERT, }; const expectedMetadata = fs.readFileSync( - __dirname + "/static/expected metadata.xml", + __dirname + "/../static/expected metadata.xml", "utf-8" ); @@ -321,7 +313,7 @@ describe("passport-saml /", function () { cert: FAKE_CERT, }; const expectedMetadata = fs.readFileSync( - __dirname + "/static/expected metadata without key.xml", + __dirname + "/../static/expected metadata without key.xml", "utf-8" ); @@ -335,11 +327,11 @@ describe("passport-saml /", function () { host: "example.serviceprovider.com", path: "/saml/callback", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), cert: FAKE_CERT, }; const expectedMetadata = fs.readFileSync( - __dirname + "/static/expected metadata.xml", + __dirname + "/../static/expected metadata.xml", "utf-8" ); @@ -356,7 +348,7 @@ describe("passport-saml /", function () { cert: FAKE_CERT, }; const expectedMetadata = fs.readFileSync( - __dirname + "/static/expected metadata without key.xml", + __dirname + "/../static/expected metadata without key.xml", "utf-8" ); @@ -370,15 +362,17 @@ describe("passport-saml /", function () { host: "example.serviceprovider.com", path: "/saml/callback", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), - privateKey: fs.readFileSync(__dirname + "/static/acme_tools_com.key"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), + privateKey: fs.readFileSync(__dirname + "/../static/acme_tools_com.key"), cert: FAKE_CERT, }; const expectedMetadata = fs.readFileSync( - __dirname + "/static/expectedMetadataWithBothKeys.xml", + __dirname + "/../static/expectedMetadataWithBothKeys.xml", "utf-8" ); - const signingCert = fs.readFileSync(__dirname + "/static/acme_tools_com.cert").toString(); + const signingCert = fs + .readFileSync(__dirname + "/../static/acme_tools_com.cert") + .toString(); testMetadata(samlConfig, expectedMetadata, signingCert); }); @@ -390,15 +384,17 @@ describe("passport-saml /", function () { host: "example.serviceprovider.com", path: "/saml/callback", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), - privateKey: fs.readFileSync(__dirname + "/static/acme_tools_com.key"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), + privateKey: fs.readFileSync(__dirname + "/../static/acme_tools_com.key"), cert: FAKE_CERT, }; const expectedMetadata = fs.readFileSync( - __dirname + "/static/expectedMetadataWithBothKeys.xml", + __dirname + "/../static/expectedMetadataWithBothKeys.xml", "utf-8" ); - const signingCert = fs.readFileSync(__dirname + "/static/acme_tools_com.cert").toString(); + const signingCert = fs + .readFileSync(__dirname + "/../static/acme_tools_com.cert") + .toString(); testMetadata(samlConfig, expectedMetadata, signingCert); }); @@ -409,14 +405,14 @@ describe("passport-saml /", function () { issuer: "http://example.serviceprovider.com", callbackUrl: "http://example.serviceprovider.com/saml/callback", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), logoutCallbackUrl: "http://example.serviceprovider.com/logout", cert: FAKE_CERT, }; const samlObj = new SAML(samlConfig); const decryptionCert = fs.readFileSync( - __dirname + "/static/testshib encryption cert.pem", + __dirname + "/../static/testshib encryption cert.pem", "utf-8" ); const metadata = samlObj.generateServiceProviderMetadata(decryptionCert); @@ -430,27 +426,27 @@ describe("passport-saml /", function () { issuer: "http://example.serviceprovider.com", callbackUrl: "http://example.serviceprovider.com/saml/callback", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), wantAssertionsSigned: true, }; const samlObj = new SAML(samlConfig); const decryptionCert = fs.readFileSync( - __dirname + "/static/testshib encryption cert.pem", + __dirname + "/../static/testshib encryption cert.pem", "utf-8" ); const metadata = samlObj.generateServiceProviderMetadata(decryptionCert); metadata.should.containEql('WantAssertionsSigned="true"'); }); - it("#certToPEM should generate valid certificate", function () { + it("#_certToPEM should generate valid certificate", function () { const samlConfig = { entryPoint: "https://app.onelogin.com/trust/saml2/http-post/sso/371755", cert: "-----BEGIN CERTIFICATE-----" + TEST_CERT + "-----END CERTIFICATE-----", acceptedClockSkewMs: -1, }; const samlObj = new SAML(samlConfig); - const certificate = samlObj.certToPEM(samlConfig.cert); + const certificate = samlObj._certToPEM(samlConfig.cert); if (!(certificate.match(/BEGIN/g)!.length == 1 && certificate.match(/END/g)!.length == 1)) { throw Error("Certificate should have only 1 BEGIN and 1 END block"); @@ -502,11 +498,11 @@ describe("passport-saml /", function () { const container = { SAMLResponse: fs - .readFileSync(__dirname + "/static/response-with-uncomplete-attribute.xml") + .readFileSync(__dirname + "/../static/response-with-uncomplete-attribute.xml") .toString("base64"), }; - const signingCert = fs.readFileSync(__dirname + "/static/cert.pem", "utf-8"); + const signingCert = fs.readFileSync(__dirname + "/../static/cert.pem", "utf-8"); const samlObj = new SAML({ cert: signingCert }); const { profile } = await samlObj.validatePostResponseAsync(container); @@ -815,8 +811,8 @@ describe("passport-saml /", function () { "" + ""; - const signingKey = fs.readFileSync(__dirname + "/static/key.pem"); - const signingCert = fs.readFileSync(__dirname + "/static/cert.pem", "utf-8"); + const signingKey = fs.readFileSync(__dirname + "/../static/key.pem"); + const signingCert = fs.readFileSync(__dirname + "/../static/cert.pem", "utf-8"); const signedXml = signXmlResponse(xml, { privateKey: signingKey }); const base64xml = Buffer.from(signedXml).toString("base64"); @@ -879,8 +875,8 @@ describe("passport-saml /", function () { "" + ""; - const signingKey = fs.readFileSync(__dirname + "/static/key.pem"); - const signingCert = fs.readFileSync(__dirname + "/static/cert.pem", "utf-8"); + const signingKey = fs.readFileSync(__dirname + "/../static/key.pem"); + const signingCert = fs.readFileSync(__dirname + "/../static/cert.pem", "utf-8"); const signedXml = signXmlResponse(xml, { privateKey: signingKey }); const base64xml = Buffer.from(signedXml).toString("base64"); @@ -906,7 +902,7 @@ describe("passport-saml /", function () { entryPoint: "https://adfs.acme_tools.com/adfs/ls/", issuer: "acme_tools_com", callbackUrl: "https://relyingparty/adfs/postResponse", - privateKey: fs.readFileSync(__dirname + "/static/acme_tools_com.key", "utf-8"), + privateKey: fs.readFileSync(__dirname + "/../static/acme_tools_com.key", "utf-8"), authnContext: [ "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password", ], @@ -918,10 +914,10 @@ describe("passport-saml /", function () { cert: FAKE_CERT, }; const samlObj = new SAML(samlConfig); - samlObj.generateUniqueID = function () { + samlObj._generateUniqueID = function () { return "12345678901234567890"; }; - const authorizeUrl = await samlObj.getAuthorizeUrlAsync({} as express.Request, {}); + const authorizeUrl = await samlObj.getAuthorizeUrlAsync("", "", {}); const qry = querystring.parse(url.parse(authorizeUrl).query || ""); qry.SigAlg?.should.match("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); qry.Signature?.should.match( @@ -935,7 +931,7 @@ describe("passport-saml /", function () { entryPoint: "", issuer: "acme_tools_com", callbackUrl: "https://relyingparty/adfs/postResponse", - privateKey: fs.readFileSync(__dirname + "/static/acme_tools_com.key", "utf-8"), + privateKey: fs.readFileSync(__dirname + "/../static/acme_tools_com.key", "utf-8"), authnContext: [ "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password", ], @@ -946,13 +942,13 @@ describe("passport-saml /", function () { cert: FAKE_CERT, }; const samlObj = new SAML(samlConfig); - samlObj.generateUniqueID = function () { + samlObj._generateUniqueID = function () { return "12345678901234567890"; }; const request = 'onelogin_samlurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; - await assert.rejects(samlObj.requestToUrlAsync(request, null, "authorize", {}), { + await assert.rejects(samlObj._requestToUrlAsync(request, null, "authorize", {}), { message: "entryPoint is required", }); }); @@ -961,7 +957,7 @@ describe("passport-saml /", function () { entryPoint: "https://adfs.acme_tools.com/adfs/ls/", issuer: "acme_tools_com", callbackUrl: "https://relyingparty/adfs/postResponse", - privateKey: fs.readFileSync(__dirname + "/static/acme_tools_com.key", "utf-8"), + privateKey: fs.readFileSync(__dirname + "/../static/acme_tools_com.key", "utf-8"), authnContext: [ "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password", ], @@ -969,13 +965,13 @@ describe("passport-saml /", function () { additionalParams: { customQueryStringParam: "CustomQueryStringParamValue", }, - cert: fs.readFileSync(__dirname + "/static/acme_tools_com.cert", "utf-8"), + cert: fs.readFileSync(__dirname + "/../static/acme_tools_com.cert", "utf-8"), }; const samlObj = new SAML(samlConfig); - samlObj.generateUniqueID = function () { + samlObj._generateUniqueID = function () { return "12345678901234567890"; }; - const authorizeUrl = await samlObj.getAuthorizeUrlAsync({} as express.Request, {}); + const authorizeUrl = await samlObj.getAuthorizeUrlAsync("", "", {}); const qry = querystring.parse(url.parse(authorizeUrl).query || ""); qry.SigAlg?.should.match("http://www.w3.org/2000/09/xmldsig#rsa-sha1"); qry.Signature?.should.match( @@ -988,7 +984,7 @@ describe("passport-saml /", function () { entryPoint: "https://adfs.acme_tools.com/adfs/ls/", issuer: "acme_tools_com", callbackUrl: "https://relyingparty/adfs/postResponse", - privateKey: fs.readFileSync(__dirname + "/static/acme_tools_com.key", "utf-8"), + privateKey: fs.readFileSync(__dirname + "/../static/acme_tools_com.key", "utf-8"), authnContext: [ "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password", ], @@ -1000,10 +996,10 @@ describe("passport-saml /", function () { cert: FAKE_CERT, }; const samlObj = new SAML(samlConfig); - samlObj.generateUniqueID = function () { + samlObj._generateUniqueID = function () { return "12345678901234567890"; }; - const authorizeUrl = await samlObj.getAuthorizeUrlAsync({} as express.Request, {}); + const authorizeUrl = await samlObj.getAuthorizeUrlAsync("", "", {}); const qry = querystring.parse(url.parse(authorizeUrl).query || ""); qry.SigAlg?.should.match("http://www.w3.org/2000/09/xmldsig#rsa-sha1"); qry.Signature?.should.match( @@ -1013,7 +1009,7 @@ describe("passport-saml /", function () { }); }); - describe("getAdditionalParams checks /", function () { + describe("_getAdditionalParams checks /", function () { it("should not pass any additional params by default", function () { const samlConfig = { entryPoint: "https://app.onelogin.com/trust/saml2/http-post/sso/371755", @@ -1022,12 +1018,12 @@ describe("passport-saml /", function () { const samlObj = new SAML(samlConfig); ["logout", "authorize"].forEach(function (operation) { - const additionalParams = samlObj.getAdditionalParams({} as express.Request, operation); + const additionalParams = samlObj._getAdditionalParams("", operation); additionalParams.should.be.empty; }); }); - it("should not pass any additional params by default apart from the RelayState in request query", function () { + it("should not pass any additional params by default apart from the RelayState", function () { const samlConfig = { entryPoint: "https://app.onelogin.com/trust/saml2/http-post/sso/371755", cert: FAKE_CERT, @@ -1035,17 +1031,14 @@ describe("passport-saml /", function () { const samlObj = new SAML(samlConfig); ["logout", "authorize"].forEach(function (operation) { - const additionalParams = samlObj.getAdditionalParams( - ({ query: { RelayState: "test" } } as unknown) as express.Request, - operation - ); + const additionalParams = samlObj._getAdditionalParams("test", operation); Object.keys(additionalParams).should.have.length(1); additionalParams.should.containEql({ RelayState: "test" }); }); }); - it("should not pass any additional params by default apart from the RelayState in request body", function () { + it("should only allow RelayState to be a string", function () { const samlConfig = { entryPoint: "https://app.onelogin.com/trust/saml2/http-post/sso/371755", cert: FAKE_CERT, @@ -1053,13 +1046,12 @@ describe("passport-saml /", function () { const samlObj = new SAML(samlConfig); ["logout", "authorize"].forEach(function (operation) { - const additionalParams = samlObj.getAdditionalParams( - { body: { RelayState: "test" } } as express.Request, + const additionalParams = samlObj._getAdditionalParams( + ({ RelayState: "test" } as unknown) as string, operation ); - Object.keys(additionalParams).should.have.length(1); - additionalParams.should.containEql({ RelayState: "test" }); + Object.keys(additionalParams).should.have.length(0); }); }); @@ -1074,7 +1066,7 @@ describe("passport-saml /", function () { const samlObj = new SAML(samlConfig); ["logout", "authorize"].forEach(function (operation) { - const additionalParams = samlObj.getAdditionalParams({} as express.Request, operation); + const additionalParams = samlObj._getAdditionalParams("", operation); Object.keys(additionalParams).should.have.length(1); additionalParams.should.containEql({ queryParam: "queryParamValue" }); }); @@ -1090,14 +1082,11 @@ describe("passport-saml /", function () { }; const samlObj = new SAML(samlConfig); - const additionalAuthorizeParams = samlObj.getAdditionalParams( - {} as express.Request, - "authorize" - ); + const additionalAuthorizeParams = samlObj._getAdditionalParams("", "authorize"); Object.keys(additionalAuthorizeParams).should.have.length(1); additionalAuthorizeParams.should.containEql({ queryParam: "queryParamValue" }); - const additionalLogoutParams = samlObj.getAdditionalParams({} as express.Request, "logout"); + const additionalLogoutParams = samlObj._getAdditionalParams("", "logout"); additionalLogoutParams.should.be.empty; }); @@ -1111,13 +1100,10 @@ describe("passport-saml /", function () { }; const samlObj = new SAML(samlConfig); - const additionalAuthorizeParams = samlObj.getAdditionalParams( - {} as express.Request, - "authorize" - ); + const additionalAuthorizeParams = samlObj._getAdditionalParams("", "authorize"); additionalAuthorizeParams.should.be.empty; - const additionalLogoutParams = samlObj.getAdditionalParams({} as express.Request, "logout"); + const additionalLogoutParams = samlObj._getAdditionalParams("", "logout"); Object.keys(additionalLogoutParams).should.have.length(1); additionalLogoutParams.should.containEql({ queryParam: "queryParamValue" }); }); @@ -1138,17 +1124,14 @@ describe("passport-saml /", function () { }; const samlObj = new SAML(samlConfig); - const additionalAuthorizeParams = samlObj.getAdditionalParams( - {} as express.Request, - "authorize" - ); + const additionalAuthorizeParams = samlObj._getAdditionalParams("", "authorize"); Object.keys(additionalAuthorizeParams).should.have.length(2); additionalAuthorizeParams.should.containEql({ queryParam1: "queryParamValue", queryParam2: "queryParamValueAuthorize", }); - const additionalLogoutParams = samlObj.getAdditionalParams({} as express.Request, "logout"); + const additionalLogoutParams = samlObj._getAdditionalParams("", "logout"); Object.keys(additionalLogoutParams).should.have.length(2); additionalLogoutParams.should.containEql({ queryParam1: "queryParamValue", @@ -1177,8 +1160,8 @@ describe("passport-saml /", function () { }, }; - const additionalAuthorizeParams = samlObj.getAdditionalParams( - {} as express.Request, + const additionalAuthorizeParams = samlObj._getAdditionalParams( + "", "authorize", options.additionalParams ); @@ -1189,8 +1172,8 @@ describe("passport-saml /", function () { queryParam3: "queryParamRuntimeValue", }); - const additionalLogoutParams = samlObj.getAdditionalParams( - {} as express.Request, + const additionalLogoutParams = samlObj._getAdditionalParams( + "", "logout", options.additionalParams ); @@ -1218,14 +1201,11 @@ describe("passport-saml /", function () { }; const samlObj = new SAML(samlConfig); - const additionalAuthorizeParams = samlObj.getAdditionalParams( - {} as express.Request, - "authorize" - ); + const additionalAuthorizeParams = samlObj._getAdditionalParams("", "authorize"); Object.keys(additionalAuthorizeParams).should.have.length(1); additionalAuthorizeParams.should.containEql({ queryParam: "queryParamValueAuthorize" }); - const additionalLogoutParams = samlObj.getAdditionalParams({} as express.Request, "logout"); + const additionalLogoutParams = samlObj._getAdditionalParams("", "logout"); Object.keys(additionalLogoutParams).should.have.length(1); additionalLogoutParams.should.containEql({ queryParam: "queryParamValueLogout" }); }); @@ -1251,16 +1231,16 @@ describe("passport-saml /", function () { }, }; - const additionalAuthorizeParams = samlObj.getAdditionalParams( - {} as express.Request, + const additionalAuthorizeParams = samlObj._getAdditionalParams( + "", "authorize", options.additionalParams ); Object.keys(additionalAuthorizeParams).should.have.length(1); additionalAuthorizeParams.should.containEql({ queryParam: "queryParamRuntimeValue" }); - const additionalLogoutParams = samlObj.getAdditionalParams( - {} as express.Request, + const additionalLogoutParams = samlObj._getAdditionalParams( + "", "logout", options.additionalParams ); @@ -1659,7 +1639,7 @@ describe("passport-saml /", function () { }); it("onelogin xml document with audience and no AudienceRestriction should not pass", async () => { - const signingCert = fs.readFileSync(__dirname + "/static/cert.pem", "utf-8"); + const signingCert = fs.readFileSync(__dirname + "/../static/cert.pem", "utf-8"); const xml = ` https://app.onelogin.com/saml/metadata/371755 @@ -1701,7 +1681,7 @@ describe("passport-saml /", function () { }); it("onelogin xml document with audience not matching AudienceRestriction should not pass", async () => { - const signingCert = fs.readFileSync(__dirname + "/static/cert.pem", "utf-8"); + const signingCert = fs.readFileSync(__dirname + "/../static/cert.pem", "utf-8"); const xml = ` https://app.onelogin.com/saml/metadata/371755 @@ -1747,7 +1727,7 @@ describe("passport-saml /", function () { }); it("onelogin xml document with audience matching AudienceRestriction should pass", async () => { - const signingCert = fs.readFileSync(__dirname + "/static/cert.pem", "utf-8"); + const signingCert = fs.readFileSync(__dirname + "/../static/cert.pem", "utf-8"); const xml = ` https://app.onelogin.com/saml/metadata/371755 @@ -1797,7 +1777,7 @@ describe("passport-saml /", function () { let samlObj: SAML; beforeEach(function () { samlObj = new SAML({ - cert: fs.readFileSync(__dirname + "/static/cert.pem", "ascii"), + cert: fs.readFileSync(__dirname + "/../static/cert.pem", "ascii"), }); }); @@ -1812,7 +1792,7 @@ describe("passport-saml /", function () { it("errors if bad signature", async () => { const body = { SAMLRequest: fs.readFileSync( - __dirname + "/static/logout_request_with_bad_signature.xml", + __dirname + "/../static/logout_request_with_bad_signature.xml", "base64" ), }; @@ -1823,7 +1803,7 @@ describe("passport-saml /", function () { it("returns profile for valid signature", async () => { const body = { SAMLRequest: fs.readFileSync( - __dirname + "/static/logout_request_with_good_signature.xml", + __dirname + "/../static/logout_request_with_good_signature.xml", "base64" ), }; @@ -1838,7 +1818,7 @@ describe("passport-saml /", function () { it("returns profile for valid signature including session index", async () => { const body = { SAMLRequest: fs.readFileSync( - __dirname + "/static/logout_request_with_session_index.xml", + __dirname + "/../static/logout_request_with_session_index.xml", "base64" ), }; @@ -1853,12 +1833,12 @@ describe("passport-saml /", function () { }); it("returns profile for valid signature with encrypted nameID", async () => { const samlObj = new SAML({ - cert: fs.readFileSync(__dirname + "/static/cert.pem", "ascii"), - decryptionPvk: fs.readFileSync(__dirname + "/static/key.pem", "ascii"), + cert: fs.readFileSync(__dirname + "/../static/cert.pem", "ascii"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/key.pem", "ascii"), }); const body = { SAMLRequest: fs.readFileSync( - __dirname + "/static/logout_request_with_encrypted_name_id.xml", + __dirname + "/../static/logout_request_with_encrypted_name_id.xml", "base64" ), }; @@ -1874,12 +1854,12 @@ describe("passport-saml /", function () { }); it("validatePostRequest errors for encrypted nameID with wrong decryptionPvk", async () => { const samlObj = new SAML({ - cert: fs.readFileSync(__dirname + "/static/cert.pem", "ascii"), - decryptionPvk: fs.readFileSync(__dirname + "/static/acme_tools_com.key", "ascii"), + cert: fs.readFileSync(__dirname + "/../static/cert.pem", "ascii"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/acme_tools_com.key", "ascii"), }); const body = { SAMLRequest: fs.readFileSync( - __dirname + "/static/logout_request_with_encrypted_name_id.xml", + __dirname + "/../static/logout_request_with_encrypted_name_id.xml", "base64" ), }; @@ -1890,7 +1870,7 @@ describe("passport-saml /", function () { it("errors if bad privateKey to requestToURL", async () => { const samlObj = new SAML({ - entryPoint: "foo", + entryPoint: "http://localhost", privateKey: "-----BEGIN CERTIFICATE-----\n" + "8mvhvrcCOiJ3mjgKNN1F31jOBJuZNmq0U7n9v+Z+3NfyU/0E9jkrnFvm5ks+p8kl\n" + @@ -1922,153 +1902,153 @@ describe("passport-saml /", function () { }); const request = 'onelogin_samlurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; - await assert.rejects(samlObj.requestToUrlAsync(request, null, "authorize", {}), { + await assert.rejects(samlObj._requestToUrlAsync(request, null, "authorize", {}), { message: /no start line/, }); }); -}); -describe("validateRedirect()", function () { - describe("idp slo", function () { - let samlObj: SAML; - let fakeClock: sinon.SinonFakeTimers; - beforeEach(function () { - samlObj = new SAML({ - cert: fs.readFileSync(__dirname + "/static/acme_tools_com.cert", "ascii"), - idpIssuer: "http://localhost:20000/saml2/idp/metadata.php", + describe("validateRedirect()", function () { + describe("idp slo", function () { + let samlObj: SAML; + let fakeClock: sinon.SinonFakeTimers; + beforeEach(function () { + samlObj = new SAML({ + cert: fs.readFileSync(__dirname + "/../static/acme_tools_com.cert", "ascii"), + idpIssuer: "http://localhost:20000/saml2/idp/metadata.php", + }); + this.request = Object.assign( + {}, + JSON.parse(fs.readFileSync(__dirname + "/../static/idp_slo_redirect.json", "utf8")) + ); + fakeClock = sinon.useFakeTimers(Date.parse("2018-04-11T14:08:00Z")); }); - this.request = Object.assign( - {}, - JSON.parse(fs.readFileSync(__dirname + "/static/idp_slo_redirect.json", "utf8")) - ); - fakeClock = sinon.useFakeTimers(Date.parse("2018-04-11T14:08:00Z")); - }); - afterEach(function () { - fakeClock.restore(); - }); - it("errors if bad xml", async function () { - const body = { - SAMLRequest: "asdf", - }; - await assert.rejects(samlObj.validateRedirectAsync(body, this.request.originalQuery)); - }); - it("errors if idpIssuer is set and issuer is wrong", async function () { - samlObj.options.idpIssuer = "foo"; - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { - message: - "Unknown SAML issuer. Expected: foo Received: http://localhost:20000/saml2/idp/metadata.php", - } - ); - }); - it("errors if request has expired", async function () { - fakeClock.restore(); - fakeClock = sinon.useFakeTimers(Date.parse("2100-04-11T14:08:00Z")); + afterEach(function () { + fakeClock.restore(); + }); + it("errors if bad xml", async function () { + const body = { + SAMLRequest: "asdf", + }; + await assert.rejects(samlObj.validateRedirectAsync(body, this.request.originalQuery)); + }); + it("errors if idpIssuer is set and issuer is wrong", async function () { + samlObj.options.idpIssuer = "foo"; + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { + message: + "Unknown SAML issuer. Expected: foo Received: http://localhost:20000/saml2/idp/metadata.php", + } + ); + }); + it("errors if request has expired", async function () { + fakeClock.restore(); + fakeClock = sinon.useFakeTimers(Date.parse("2100-04-11T14:08:00Z")); - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { message: "SAML assertion expired" } - ); - }); - it("errors if request has a bad signature", async function () { - this.request.Signature = "foo"; - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { message: "Invalid query signature" } - ); - }); - it("returns profile for valid signature including session index", async function () { - const { profile } = await samlObj.validateRedirectAsync( - this.request, - this.request.originalQuery - ); - profile!.should.eql({ - ID: "_8f0effde308adfb6ae7f1e29b414957fc320f5636f", - issuer: "http://localhost:20000/saml2/idp/metadata.php", - nameID: "stavros@workable.com", - nameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - sessionIndex: "_00bf7b2d5d9d3c970217eecefb1194bef3362a618e", + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { message: "SAML assertion expired" } + ); + }); + it("errors if request has a bad signature", async function () { + this.request.Signature = "foo"; + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { message: "Invalid query signature" } + ); + }); + it("returns profile for valid signature including session index", async function () { + const { profile } = await samlObj.validateRedirectAsync( + this.request, + this.request.originalQuery + ); + profile!.should.eql({ + ID: "_8f0effde308adfb6ae7f1e29b414957fc320f5636f", + issuer: "http://localhost:20000/saml2/idp/metadata.php", + nameID: "stavros@workable.com", + nameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + sessionIndex: "_00bf7b2d5d9d3c970217eecefb1194bef3362a618e", + }); }); }); - }); - describe("sp slo", function () { - let samlObj: SAML; + describe("sp slo", function () { + let samlObj: SAML; - beforeEach(function () { - samlObj = new SAML({ - cert: fs.readFileSync(__dirname + "/static/acme_tools_com.cert", "ascii"), - idpIssuer: "http://localhost:20000/saml2/idp/metadata.php", - validateInResponseTo: true, + beforeEach(function () { + samlObj = new SAML({ + cert: fs.readFileSync(__dirname + "/../static/acme_tools_com.cert", "ascii"), + idpIssuer: "http://localhost:20000/saml2/idp/metadata.php", + validateInResponseTo: true, + }); + this.request = Object.assign( + {}, + JSON.parse(fs.readFileSync(__dirname + "/../static/sp_slo_redirect.json", "utf8")) + ); + }); + afterEach(async function () { + await samlObj.cacheProvider.removeAsync("_79db1e7ad12ca1d63e5b"); + }); + it("errors if bad xml", async function () { + const body = { + SAMLRequest: "asdf", + }; + await assert.rejects(samlObj.validateRedirectAsync(body, null)); + }); + it("errors if idpIssuer is set and wrong issuer", async function () { + samlObj.options.idpIssuer = "foo"; + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { + message: + "Unknown SAML issuer. Expected: foo Received: http://localhost:20000/saml2/idp/metadata.php", + } + ); + }); + it("errors if unsuccessful", async function () { + this.request = JSON.parse( + fs.readFileSync(__dirname + "/../static/sp_slo_redirect_failure.json", "utf8") + ); + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { message: "Bad status code: urn:oasis:names:tc:SAML:2.0:status:Requester" } + ); + }); + it("errors if InResponseTo is not found", async function () { + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { message: "InResponseTo is not valid" } + ); + }); + it("errors if bad signature", async function () { + await samlObj.cacheProvider.saveAsync("_79db1e7ad12ca1d63e5b", new Date().toISOString()); + this.request.Signature = "foo"; + await assert.rejects( + samlObj.validateRedirectAsync(this.request, this.request.originalQuery), + { message: "Invalid query signature" } + ); }); - this.request = Object.assign( - {}, - JSON.parse(fs.readFileSync(__dirname + "/static/sp_slo_redirect.json", "utf8")) - ); - }); - afterEach(async function () { - await samlObj.cacheProvider.removeAsync("_79db1e7ad12ca1d63e5b"); - }); - it("errors if bad xml", async function () { - const body = { - SAMLRequest: "asdf", - }; - await assert.rejects(samlObj.validateRedirectAsync(body, null)); - }); - it("errors if idpIssuer is set and wrong issuer", async function () { - samlObj.options.idpIssuer = "foo"; - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { - message: - "Unknown SAML issuer. Expected: foo Received: http://localhost:20000/saml2/idp/metadata.php", - } - ); - }); - it("errors if unsuccessful", async function () { - this.request = JSON.parse( - fs.readFileSync(__dirname + "/static/sp_slo_redirect_failure.json", "utf8") - ); - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { message: "Bad status code: urn:oasis:names:tc:SAML:2.0:status:Requester" } - ); - }); - it("errors if InResponseTo is not found", async function () { - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { message: "InResponseTo is not valid" } - ); - }); - it("errors if bad signature", async function () { - await samlObj.cacheProvider.saveAsync("_79db1e7ad12ca1d63e5b", new Date().toISOString()); - this.request.Signature = "foo"; - await assert.rejects( - samlObj.validateRedirectAsync(this.request, this.request.originalQuery), - { message: "Invalid query signature" } - ); - }); - it("returns true for valid signature", async function () { - await samlObj.cacheProvider.saveAsync("_79db1e7ad12ca1d63e5b", new Date().toISOString()); - const { loggedOut } = await samlObj.validateRedirectAsync( - this.request, - this.request.originalQuery - ); - loggedOut!.should.eql(true); - }); + it("returns true for valid signature", async function () { + await samlObj.cacheProvider.saveAsync("_79db1e7ad12ca1d63e5b", new Date().toISOString()); + const { loggedOut } = await samlObj.validateRedirectAsync( + this.request, + this.request.originalQuery + ); + loggedOut!.should.eql(true); + }); - it("accepts cert without header and footer line", async function () { - samlObj.options.cert = fs.readFileSync( - __dirname + "/static/acme_tools_com_without_header_and_footer.cert", - "ascii" - ); - await samlObj.cacheProvider.saveAsync("_79db1e7ad12ca1d63e5b", new Date().toISOString()); - const { loggedOut } = await samlObj.validateRedirectAsync( - this.request, - this.request.originalQuery - ); - loggedOut!.should.eql(true); + it("accepts cert without header and footer line", async function () { + samlObj.options.cert = fs.readFileSync( + __dirname + "/../static/acme_tools_com_without_header_and_footer.cert", + "ascii" + ); + await samlObj.cacheProvider.saveAsync("_79db1e7ad12ca1d63e5b", new Date().toISOString()); + const { loggedOut } = await samlObj.validateRedirectAsync( + this.request, + this.request.originalQuery + ); + loggedOut!.should.eql(true); + }); }); }); }); diff --git a/test/capturedSamlRequests.spec.ts b/test/passport-saml/capturedSamlRequests.spec.ts similarity index 99% rename from test/capturedSamlRequests.spec.ts rename to test/passport-saml/capturedSamlRequests.spec.ts index 6865c9f8..0df0f9ff 100644 --- a/test/capturedSamlRequests.spec.ts +++ b/test/passport-saml/capturedSamlRequests.spec.ts @@ -2,16 +2,16 @@ import * as express from "express"; import * as bodyParser from "body-parser"; import * as passport from "passport"; -import { Strategy as SamlStrategy } from "../src/passport-saml"; +import { Strategy as SamlStrategy } from "../../src/passport-saml"; import request = require("request"); import * as zlib from "zlib"; import * as querystring from "querystring"; import { parseString } from "xml2js"; import * as fs from "fs"; -import { AuthenticateOptions, Profile, VerifiedCallback } from "../src/passport-saml/types.js"; +import { AuthenticateOptions, Profile, VerifiedCallback } from "../../src/passport-saml/types.js"; import * as should from "should"; import { Server } from "http"; -import { CapturedCheck, FAKE_CERT, SamlCheck } from "./types"; +import { CapturedCheck, FAKE_CERT, SamlCheck } from "../types"; const capturedSamlRequestChecks: SamlCheck[] = [ { @@ -868,12 +868,12 @@ export const logoutChecks: CapturedCheck[] = [ config: { skipRequestCompression: true, entryPoint: "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", - cert: fs.readFileSync(__dirname + "/static/cert.pem", "ascii"), + cert: fs.readFileSync(__dirname + "/../static/cert.pem", "ascii"), identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", }, samlRequest: { SAMLRequest: fs.readFileSync( - __dirname + "/static/logout_request_with_good_signature.xml", + __dirname + "/../static/logout_request_with_good_signature.xml", "base64" ), }, diff --git a/test/capturedSamlResponses.spec.ts b/test/passport-saml/capturedSamlResponses.spec.ts similarity index 99% rename from test/capturedSamlResponses.spec.ts rename to test/passport-saml/capturedSamlResponses.spec.ts index 63f63f6e..bb48447c 100644 --- a/test/capturedSamlResponses.spec.ts +++ b/test/passport-saml/capturedSamlResponses.spec.ts @@ -2,7 +2,7 @@ import * as express from "express"; import * as bodyParser from "body-parser"; import * as passport from "passport"; -import { Strategy as SamlStrategy } from "../src/passport-saml"; +import { Strategy as SamlStrategy } from "../../src/passport-saml"; import request = require("request"); import * as fs from "fs"; import * as sinon from "sinon"; @@ -11,10 +11,10 @@ import { SamlConfig, StrategyOptions, VerifiedCallback, -} from "../src/passport-saml/types.js"; +} from "../../src/passport-saml/types.js"; import * as should from "should"; import { Server } from "http"; -import { CapturedCheck, TEST_CERT } from "./types"; +import { CapturedCheck, TEST_CERT } from "../types"; export const capturedSamlResponseChecks: CapturedCheck[] = [ { @@ -46,7 +46,7 @@ export const capturedSamlResponseChecks: CapturedCheck[] = [ "https://frontapp.oktapreview.com/app/frontdev584714_front_1/exk7xdi6axPfombzx0h7/sso/saml", cert: "MIIDoDCCAoigAwIBAgIGAVaZ04POMA0GCSqGSIb3DQEBBQUAMIGQMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxETAPBgNVBAMMCGZyb250YXBwMRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMB4XDTE2MDgxNzE4NDUzMVoXDTI2MDgxNzE4NDYzMVowgZAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARPa3RhMRQwEgYDVQQLDAtTU09Qcm92aWRlcjERMA8GA1UEAwwIZnJvbnRhcHAxHDAaBgkqhkiG9w0BCQEWDWluZm9Ab2t0YS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvplQONVwknRy1iBnaoZtsOz28A7XW2tRpFW+0La7RJexbziIwEy1bPZENhfwjPZA1oHHZqi5l315BxXKWJqmmNmbDCFDo+/FYFCoHXliiLm9vqDbR1br6ByqeY0GfxyTPKHZxb2FSes30TffDknpMQd/8kA9YWaW5xDlu2ivWJI+sfcOJOMd6t+gcfXj58a5fP8Mwm6Y220KeZSvrVpEV2KDp9hln7fhhoxHZ7K/BYbidqdwLzeUQXpb6LIrxtKdug2FofS+ONs6yLIQRmrbCB7SVX1QA8JInMn+fzrGtZmFiHR0aFbyhiO78v/ufDa6S+XpYyp2b6D4SnzeggnobAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAJ2wcFVffFHSd9pj6RgoNHXZBsWp0HUZrNekiSbgomr4tSDefWtKb04nFIlRytfVs/k74wmbNiRCE8nDVBrBDFA/+Tv/3PowZXHjXKBofUuScTP4/Tw1N/ywf7V+XY5kV3VmLBL6ax+ULJauR/YGIIMsIc/rS2D04aAcScU9pqVh2ML7nTH7gFqYrxypavmVk6K94vLjs0ggF2TGp7tXCRjeOlPPJS+MOJHJhTBWYFWvBLclU3zcri3ws7GqJMpeiHa7rMoHV0onxWsZTZW57ybaIWKLt1goAooC7hq0rx7oNlOvrys5lllhBySYYC3ycqca/D0+GxXLcEr9QwP7TVw=", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), }, expectedStatusCode: 200, expectedNameIDStartsWith: "xavier", @@ -92,7 +92,7 @@ export const capturedSamlResponseChecks: CapturedCheck[] = [ cert: "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYDVQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRlc3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7CyVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aTNPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWHgWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0GA1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ869nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBlbm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNoaWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRLI4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4/SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAjGeka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==", identifierFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - decryptionPvk: fs.readFileSync(__dirname + "/static/testshib encryption pvk.pem"), + decryptionPvk: fs.readFileSync(__dirname + "/../static/testshib encryption pvk.pem"), }, expectedStatusCode: 200, mockDate: "2014-06-02T17:48:56.820Z", diff --git a/test/multiSamlStrategy.spec.ts b/test/passport-saml/multiSamlStrategy.spec.ts similarity index 98% rename from test/multiSamlStrategy.spec.ts rename to test/passport-saml/multiSamlStrategy.spec.ts index 4718541d..0ab47684 100644 --- a/test/multiSamlStrategy.spec.ts +++ b/test/passport-saml/multiSamlStrategy.spec.ts @@ -2,15 +2,15 @@ import * as express from "express"; import * as sinon from "sinon"; import * as should from "should"; -import { Strategy as SamlStrategy, MultiSamlStrategy, SAML } from "../src/passport-saml"; +import { Strategy as SamlStrategy, MultiSamlStrategy, SAML } from "../../src/passport-saml"; import { MultiSamlConfig, SamlOptionsCallback, RequestWithUser, SamlConfig, -} from "../src/passport-saml/types"; +} from "../../src/passport-saml/types"; import assert = require("assert"); -import { FAKE_CERT } from "./types"; +import { FAKE_CERT } from "../types"; const noop = () => undefined; diff --git a/test/strategy.spec.ts b/test/passport-saml/strategy.spec.ts similarity index 88% rename from test/strategy.spec.ts rename to test/passport-saml/strategy.spec.ts index 0f64a454..2946836a 100644 --- a/test/strategy.spec.ts +++ b/test/passport-saml/strategy.spec.ts @@ -1,9 +1,9 @@ "use strict"; import * as sinon from "sinon"; -import { SAML, Strategy as SamlStrategy } from "../src/passport-saml"; -import { RequestWithUser } from "../src/passport-saml/types"; -import { FAKE_CERT } from "./types"; +import { SAML, Strategy as SamlStrategy } from "../../src/passport-saml"; +import { RequestWithUser } from "../../src/passport-saml/types"; +import { FAKE_CERT } from "../types"; const noop = () => undefined;