Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API-10892 techdebt passport selection fix #468

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7b081fe
decoded saml response examples
crolarlibertyva Sep 2, 2023
3e13353
samlresponse examples
crolarlibertyva Sep 2, 2023
599f6cd
test items
crolarlibertyva Sep 2, 2023
0656db9
selectPassportStrategyKey
crolarlibertyva Sep 2, 2023
18b8377
test items
crolarlibertyva Sep 2, 2023
3c81d9c
okta idp items
crolarlibertyva Sep 2, 2023
e498c0b
selectPassportStrategyKey
crolarlibertyva Sep 2, 2023
992bdae
test items
crolarlibertyva Sep 2, 2023
10888e4
test items
crolarlibertyva Sep 2, 2023
4f1d265
selectPassportStrategyKey
crolarlibertyva Sep 2, 2023
21923fb
selectPassportStrategyKey
crolarlibertyva Sep 2, 2023
3ecacd8
test items
crolarlibertyva Sep 2, 2023
335bfed
test item
crolarlibertyva Sep 3, 2023
b1fcbfc
selectPassportStrategyKey
crolarlibertyva Sep 3, 2023
bbb7ab0
selectPassportStrategyKey
crolarlibertyva Sep 3, 2023
1ef04e2
test items, origin header not used
crolarlibertyva Sep 3, 2023
54377be
Merge branch 'master' into API-10892-techdebt-passport-selection-fix
crolarlibertyva Sep 6, 2023
f719a3c
fake idps
crolarlibertyva Sep 7, 2023
d84a84b
test items
crolarlibertyva Sep 7, 2023
41cc472
selectPassportStrategyKey
crolarlibertyva Sep 7, 2023
8b6ecb0
selectPassportStrategyKey
crolarlibertyva Sep 7, 2023
6dc6276
selectPassportStrategyKey
crolarlibertyva Sep 7, 2023
c89f9be
selectPassportStrategyKey
crolarlibertyva Sep 7, 2023
c4084db
selectPassportStrategyKey
crolarlibertyva Sep 7, 2023
c4e53ce
selectPassportStrategyKey
crolarlibertyva Sep 8, 2023
4e9d10c
selectPassportStrategyKey, simplify expression
crolarlibertyva Sep 11, 2023
2bb0ac4
config override on idp matching
crolarlibertyva Sep 11, 2023
cc1463b
config override on idp matching
crolarlibertyva Sep 11, 2023
83a6b7c
Merge branch 'API-10892-techdebt-passport-selection-fix-match-overrid…
crolarlibertyva Sep 11, 2023
8bc80b5
selectPassportStrategyKey, simplify expression
crolarlibertyva Sep 12, 2023
dd1a2e1
selectPassportStrategyKey, simplify expression
crolarlibertyva Sep 12, 2023
18d94cf
test items
crolarlibertyva Sep 28, 2023
d6c22d6
test items
crolarlibertyva Sep 28, 2023
497d962
test items
crolarlibertyva Sep 28, 2023
e75fbe7
test items
crolarlibertyva Sep 28, 2023
481af44
Merge branch 'API-10892-techdebt-passport-selection-fix-test-update' …
crolarlibertyva Sep 28, 2023
0c4b175
lint and idp default
crolarlibertyva Sep 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SPConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class SPConfig {
this.idpCert = argv.spIdpCert;
this.idpThumbprint = argv.spIdpThumbprint;
this.idpMetaUrl = argv.spIdpMetaUrl;
this.idpIssuerMatchOverride = argv.spIdpIssuerMatchOverride;
this.audience = argv.spAudience;
this.providerName = argv.spProviderName;
this.signAuthnRequests = argv.spSignAuthnRequests;
Expand Down
6 changes: 6 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ export function processArgs() {
string: true,
default: "samlp",
},
spIdpCategory: {
description: "The IDP category for the default upstream IDP",
required: true,
string: true,
default: "id_me",
},
spIdpIssuer: {
description: "IdP Issuer URI",
required: false,
Expand Down
7 changes: 2 additions & 5 deletions src/routes/acsHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import { VsoClient } from "../VsoClient";
import { MVIRequestMetrics } from "../metrics";
import { TestCache } from "./types";
import {
buildSamlResponseFunction,
defaultMockRequest,
b64encodedDataFromFile,
} from "../../test/testUtils";
import { idpConfig } from "../../test/testServer";
import { IDME_USER } from "../../test/testUsers";
import { accessiblePhoneNumber } from "../utils";
import samlp from "samlp";
jest.mock("passport");
Expand Down Expand Up @@ -593,7 +591,6 @@ describe("buildPassportLoginHandler", () => {
let req: any;
let mockResponse: any;
let mockNext: any;
const buildSamlResponse = buildSamlResponseFunction(1);
beforeEach(async () => {
req = defaultMockRequest;
mockResponse = {
Expand All @@ -608,7 +605,7 @@ describe("buildPassportLoginHandler", () => {
});

it("happy path", () => {
req.query.SAMLResponse = buildSamlResponse(IDME_USER, "3", idpConfig);
req.query.SAMLResponse = b64encodedDataFromFile("idp1_example.xml");
handlers.buildPassportLoginHandler("http://example.com/acs")(
req,
mockResponse,
Expand Down
34 changes: 27 additions & 7 deletions src/routes/passport.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import passport from "passport";
import { Strategy } from "passport-wsfed-saml2";
import omit from "lodash.omit";
import { IDPProfileMapper } from "../IDPProfileMapper";
import { issuerFromSamlResponse } from "../utils";

/**
* Creates the passport strategy using the response params
Expand Down Expand Up @@ -74,12 +75,31 @@ export function preparePassport(strategy) {
* @returns {*} A string with the correct spIdp key
*/
export const selectPassportStrategyKey = (req) => {
const origin = req.headers.origin;
let passportKey = "id_me";
Object.entries(req.sps.options).forEach((spIdpEntry) => {
if (spIdpEntry[1].idpSsoUrl.startsWith(origin)) {
passportKey = spIdpEntry[0];
}
const samlResponse = req.body?.SAMLResponse || req.query?.SAMLResponse;
const issuer = issuerFromSamlResponse(samlResponse);
const spIdpKeys = Object.keys(req.sps.options);
const foundSpIdpKey = spIdpKeys.find((spIdpKey) => {
const spIdpOption = req.sps.options[spIdpKey];
const domain = spIdpHostDomain(spIdpOption);
return spIdpOption.idpIssuerMatchOverride
? issuer.includes(spIdpOption.idpIssuerMatchOverride)
: issuer.includes(domain);
});
return passportKey;
return foundSpIdpKey ? foundSpIdpKey : spIdpKeys[0];
johnmweisz marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* Returns the domain that cooresponds to the host of the metadata url.
*
* @param {spConfig} spIdpOption The SP Config Option
* @returns {string} The domain name
*/
function spIdpHostDomain(spIdpOption) {
crolarlibertyva marked this conversation as resolved.
Show resolved Hide resolved
const url = new URL(spIdpOption.idpMetaUrl);
const domain_parts = url.host.split(".");
const domain =
domain_parts.length > 1
? domain_parts[domain_parts.length - 2]
: domain_parts[0];
return domain;
}
40 changes: 34 additions & 6 deletions src/routes/passport.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable jsdoc/require-returns */
import "jest";
import { selectPassportStrategyKey } from "./passport";
import { b64encodedDataFromFile } from "../../test/testUtils";

const mockReq = {
headers: {
Expand All @@ -8,24 +10,50 @@ const mockReq = {
sps: {
options: {
idp1: {
idpSsoUrl: "http://login.example1.com/saml/sso",
category: "idp1",
idpMetaUrl: "https://api.idp1.com/saml/metadata/provider",
},
idp2: {
idpSsoUrl: "http://login.example2.com/saml/sso",
category: "idp2",
idpMetaUrl: "https://idp.int.idp2.org/api/saml/metadata2023",
},
idp3: {
category: "idp3",
idpMetaUrl: "https://idp3:18443/realms/xxxx/protocol/saml/descriptor",
},
idp4: {
category: "idp4",
idpMetaUrl:
"https://deptyyy.idp4preview.com/app/yyyy/sso/saml/metadata",
idpIssuerMatchOverride: "idp4.com",
},
},
},
};
describe("selectPassportStrategyKey", () => {
test("selectPassportStrategyKey idp1", () => {
mockReq.body = { SAMLResponse: b64encodedDataFromFile("idp1_example.xml") };
expect(selectPassportStrategyKey(mockReq)).toBe("idp1");
});
test("selectPassportStrategyKey idp2", () => {
mockReq.headers.origin = "http://login.example2.com";
mockReq.body = { SAMLResponse: b64encodedDataFromFile("idp2_example.xml") };
expect(selectPassportStrategyKey(mockReq)).toBe("idp2");
});
test("selectPassportStrategyKey default 'id_me'", () => {
mockReq.headers.origin = "http://login.example0.com";
expect(selectPassportStrategyKey(mockReq)).toBe("id_me");

test("selectPassportStrategyKey idp3", () => {
mockReq.body = { SAMLResponse: b64encodedDataFromFile("idp3_example.xml") };
expect(selectPassportStrategyKey(mockReq)).toBe("idp3");
});

test("selectPassportStrategyKey idp4", () => {
mockReq.body = { SAMLResponse: b64encodedDataFromFile("idp4_example.xml") };
expect(selectPassportStrategyKey(mockReq)).toBe("idp4");
});

test("selectPassportStrategyKey default 'idp1'", () => {
mockReq.body = {
SAMLResponse: b64encodedDataFromFile("unmatched_example.xml"),
};
expect(selectPassportStrategyKey(mockReq)).toBe("idp1");
});
});
23 changes: 23 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,29 @@ function getInResponseToFromSAML(samlResponse) {
}
}

/**
* Retrieves the issuer from a b64 encoded SAMLResponse
*
* @param {string} samlResponse the raw samlResponse
* @returns {*} a string if Issuer is present
*/
export function issuerFromSamlResponse(samlResponse) {
try {
const decoded = Buffer.from(samlResponse, "base64").toString("ascii");
const parser = new DOMParser();
const issuerElems = parser
.parseFromString(decoded)
.documentElement.getElementsByTagNameNS(
"urn:oasis:names:tc:SAML:2.0:assertion",
"Issuer"
crolarlibertyva marked this conversation as resolved.
Show resolved Hide resolved
);
const issuer = issuerElems[0].textContent.trim();
return issuer;
} catch (err) {
logger.error("decodedSamlResponse failed: ", err);
}
}

/**
* Retrieves ID assertion from SAMLRequest
*
Expand Down
7 changes: 7 additions & 0 deletions test/samlResponses/decoded/idp1_example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_7bb1775fba384475ae3593649740af7d"
Version="2.0" IssueInstant="2023-09-01T22:00:12Z"
Destination="http://localhost:7000/samlproxy/sp/saml/sso"
InResponseTo="id32154644961507252952328722">
<saml:Issuer>api.idp1.com</saml:Issuer>
</samlp:Response>
7 changes: 7 additions & 0 deletions test/samlResponses/decoded/idp2_example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<samlp:Response ID="_03af590d-49cf-4360-9da8-cef9ce89986e" Version="2.0"
IssueInstant="2023-09-01T22:33:07Z" Destination="https://localhost:9001/samlproxy/sp/saml/sso"
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
InResponseTo="id771166074202739869068838" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
https://idp2.org/api/saml</Issuer>
</samlp:Response>
7 changes: 7 additions & 0 deletions test/samlResponses/decoded/idp3_example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
Destination="http://localhost:7000/samlproxy/sp/saml/sso"
ID="ID_218901a1-603f-4688-87e0-2072c87bf36c" InResponseTo="id31972267796844074658910450"
IssueInstant="2023-08-30T19:54:56.160Z" Version="2.0">
<saml:Issuer>https://idp3:18443/idp</saml:Issuer>
</samlp:Response>
8 changes: 8 additions & 0 deletions test/samlResponses/decoded/idp4_example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response Destination="http://localhost:7000/samlproxy/sp/saml/sso"
ID="id62198144964841931471458631" InResponseTo="id58204955972369371611730360"
IssueInstant="2023-09-02T04:30:28.137Z" Version="2.0"
xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.idp4.com/yyyyyy</saml2:Issuer>
</saml2p:Response>
10 changes: 10 additions & 0 deletions test/samlResponses/decoded/unmatched_example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
Destination="http://localhost:7000/samlproxy/sp/saml/sso"
ID="ID_218901a1-603f-4688-87e0-2072c87bf36c" InResponseTo="id31972267796844074658910450"
IssueInstant="2023-08-30T19:54:56.160Z" Version="2.0">
<saml:Issuer>http://wontfind/xxxx</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
</samlp:Response>
20 changes: 17 additions & 3 deletions test/testUtils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import btoa from "btoa";
import { getSamlResponse } from "samlp";
import { getUser } from "./testUsers";
const fs = require("fs");
const path = require("path");

/**
* This test function builds the saml response function using session index
Expand Down Expand Up @@ -33,9 +35,6 @@ export let defaultMockRequest = {
query: {
relayState: "relay",
},
headers: {
origin: "https://idp.example.com",
},
body: {
RelayState: "relay",
SAMLResponse: null
Expand All @@ -45,6 +44,7 @@ export let defaultMockRequest = {
id_me: {
getResponseParams: jest.fn(() => {}),
idpSsoUrl: "https://idp.example.com/saml/sso",
idpMetaUrl: "https://api.idmelabs.com/metadata",
},
},
},
Expand Down Expand Up @@ -72,3 +72,17 @@ export let defaultMockRequest = {
originalUrl: "http://original.example.com",
x_fowarded_host: "fowarded.example.com",
};

/**
* Loads test data into a string.
*
* @param {*} fname The file with test data
*/
export function b64encodedDataFromFile(fname) {
const file = path.join("./test/samlResponses/decoded", fname);
const samlResponse = fs.readFileSync(file, "utf8", function (err, data) {
return data;
});
const encoded = Buffer.from(samlResponse, "ascii").toString("base64");
return encoded;
}