diff --git a/dev-config.base.json b/dev-config.base.json index 60692467..4c2b80aa 100644 --- a/dev-config.base.json +++ b/dev-config.base.json @@ -29,7 +29,8 @@ "spIdpSsoBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", "idpSamlLoginsEnabled": true, "logStyleElementsEnabled": true, - "idpSamlLogins": + "fraudBlockEnabled": true, + "idpSamlLogins": [ { "category": "example2SamlIdp", diff --git a/src/MpiUserClient.test.ts b/src/MpiUserClient.test.ts index e9d5cd0d..cc397912 100644 --- a/src/MpiUserClient.test.ts +++ b/src/MpiUserClient.test.ts @@ -50,6 +50,7 @@ beforeEach(() => { icn: "fakeICN", first_name: "Edward", last_name: "Paget", + id_theft_indicator: true, }, }, }, @@ -62,7 +63,8 @@ describe("getMVITraitsForLoa3User", () => { const client = new MpiUserClient( "faketoken", "https://example.gov/mvi-user", - "faketoken" + "faketoken", + true ); await client.getMpiTraitsForLoa3User(samlTraitsEDIPI); expect(axios.post).toHaveBeenCalledWith( @@ -89,7 +91,8 @@ describe("getMVITraitsForLoa3User", () => { const client = new MpiUserClient( "faketoken", "https://example.gov/mpi-user", - "faketoken" + "faketoken", + true ); await client.getMpiTraitsForLoa3User(samlTraitsICN); expect(axios.post).toHaveBeenCalledWith( @@ -111,7 +114,8 @@ describe("getMVITraitsForLoa3User", () => { const client = new MpiUserClient( "faketoken", "https://example.gov/mpi-user", - "faketoken" + "faketoken", + true ); await client.getMpiTraitsForLoa3User(samlTraits); expect(axios.post).toHaveBeenCalledWith( @@ -138,9 +142,11 @@ describe("getMVITraitsForLoa3User", () => { const client = new MpiUserClient( "faketoken", "https://example.gov", - "faketoken" + "faketoken", + false ); - const { icn } = await client.getMpiTraitsForLoa3User(samlTraits); - expect(icn).toEqual("fakeICN"); + const mpiTraits = await client.getMpiTraitsForLoa3User(samlTraits); + expect(mpiTraits.icn).toEqual("fakeICN"); + expect(mpiTraits.idTheftIndicator).toEqual(false); }); }); diff --git a/src/MpiUserClient.ts b/src/MpiUserClient.ts index 4a42e5a4..c96be8c6 100644 --- a/src/MpiUserClient.ts +++ b/src/MpiUserClient.ts @@ -4,18 +4,30 @@ import axios from "axios"; export class MpiUserClient { mpiUserEndpoint: string; headers: object; + fraudBlockEnabled: boolean; - constructor(apiKey: string, mpiUserEndpoint: string, accessKey: string) { + constructor( + apiKey: string, + mpiUserEndpoint: string, + accessKey: string, + fraudBlockEnabled: boolean + ) { this.mpiUserEndpoint = mpiUserEndpoint; this.headers = { apiKey: apiKey, accesskey: accessKey, }; + this.fraudBlockEnabled = fraudBlockEnabled; } public async getMpiTraitsForLoa3User( user: SAMLUser - ): Promise<{ icn: string; first_name: string; last_name: string }> { + ): Promise<{ + icn: string; + first_name: string; + last_name: string; + idTheftIndicator: boolean; + }> { const body: Record = { idp_uuid: user.uuid, dslogon_edipi: user.edipi || null, @@ -38,7 +50,12 @@ export class MpiUserClient { }) .then((response) => { const data = response.data.data; - return data.attributes; + return { + icn: data.attributes.icn, + first_name: data.attributes.first_name, + last_name: data.attributes.last_name, + idTheftIndicator: data.id_theft_indicator || false, + }; }) .catch(() => { throw { diff --git a/src/MpiUserClientConfig.js b/src/MpiUserClientConfig.js index 5226b43c..50c57198 100644 --- a/src/MpiUserClientConfig.js +++ b/src/MpiUserClientConfig.js @@ -3,5 +3,6 @@ export default class MpiUserClientConfig { this.mpiUserEndpoint = argv.mpiUserEndpoint; this.accessKey = argv.accessKey; this.apiKey = argv.vetsAPIToken; + this.fraudBlockEnabled = argv.fraudBlockEnabled || false; } } diff --git a/src/app.js b/src/app.js index 7b22ab80..fe0223f0 100644 --- a/src/app.js +++ b/src/app.js @@ -101,7 +101,8 @@ function runServer(argv) { const mpiUserClient = new MpiUserClient( mpiUserClientConfig.apiKey, mpiUserClientConfig.mpiUserEndpoint, - mpiUserClientConfig.accessKey + mpiUserClientConfig.accessKey, + mpiUserClientConfig.fraudBlockEnabled ); const vsoClient = new VsoClient( vsoConfig.token, diff --git a/src/cli/index.js b/src/cli/index.js index d6f3c573..a18ba0ca 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -41,6 +41,13 @@ export function processArgs() { KEY_CERT_HELP_TEXT ), }, + fraudBlockEnabled: { + description: + "Enable or disable blocking logins based on the fraud identity indicator", + required: false, + boolean: true, + default: false, + }, idpKey: { description: "IdP Signature PrivateKey Certificate", required: true, diff --git a/src/routes/acsHandlers.test.ts b/src/routes/acsHandlers.test.ts index bf6bff94..097be238 100644 --- a/src/routes/acsHandlers.test.ts +++ b/src/routes/acsHandlers.test.ts @@ -25,7 +25,8 @@ const vsoClient = new VsoClient("fakeToken", "https://example.gov"); const mpiUserClient = new MpiUserClient( "fakeToken", "http://example.com/mpiuser", - "fakekey" + "fakekey", + true ); // Technically Doesn't TypeCheck, but typechecking is off for test files @@ -219,6 +220,86 @@ describe("loadICN", () => { vsoClient.getVSOSearch.mockReset(); }); + it("should block login when fraudBlockEnabled is true and idTheftIndicator is true", async () => { + const nextFn = jest.fn(); + const renderMock = jest.fn(); + const req: any = { + mpiUserClient: { ...mpiUserClient, fraudBlockEnabled: true }, + vsoClient: vsoClient, + user: { + claims: { ...claimsWithICN }, + }, + }; + + req.mpiUserClient.getMpiTraitsForLoa3User.mockResolvedValueOnce({ + icn: "anICN", + first_name: "Edward", + last_name: "Paget", + idTheftIndicator: true, + }); + + const response: any = { render: renderMock }; + await handlers.loadICN(req, response, nextFn); + + expect(req.mpiUserClient.getMpiTraitsForLoa3User).toHaveBeenCalled(); + expect(renderMock).toHaveBeenCalledWith("layout", { + body: "sensitive_error", + }); + expect(nextFn).not.toHaveBeenCalled(); + }); + + it("should not block login when fraudBlockEnabled is true and idTheftIndicator is false", async () => { + const nextFn = jest.fn(); + const renderMock = jest.fn(); + const req: any = { + mpiUserClient: { ...mpiUserClient, fraudBlockEnabled: true }, + vsoClient: vsoClient, + user: { + claims: { ...claimsWithICN }, + }, + }; + + req.mpiUserClient.getMpiTraitsForLoa3User.mockResolvedValueOnce({ + icn: "anICN", + first_name: "Edward", + last_name: "Paget", + idTheftIndicator: false, + }); + + const response: any = { render: renderMock }; + await handlers.loadICN(req, response, nextFn); + + expect(renderMock).not.toHaveBeenCalled(); + expect(nextFn).toHaveBeenCalled(); + expect(req.user.claims.icn).toEqual("anICN"); + }); + + it("should not block login when fraudBlockEnabled is false and idTheftIndicator is true", async () => { + const nextFn = jest.fn(); + const renderMock = jest.fn(); + const req: any = { + mpiUserClient: { ...mpiUserClient, fraudBlockEnabled: false }, + vsoClient: vsoClient, + user: { + claims: { ...claimsWithICN }, + }, + }; + + req.mpiUserClient.getMpiTraitsForLoa3User.mockResolvedValueOnce({ + icn: "anICN", + first_name: "Edward", + last_name: "Paget", + idTheftIndicator: true, + }); + + const response: any = { render: renderMock }; + await handlers.loadICN(req, response, nextFn); + + expect(renderMock).not.toHaveBeenCalled(); + expect(nextFn).toHaveBeenCalled(); + expect(req.user.claims.icn).toEqual("anICN"); + }); + it("should call getMVITraits... calls when ICN Exists", async () => { const nextFn = jest.fn(); const req: any = { diff --git a/src/routes/acsHandlers.ts b/src/routes/acsHandlers.ts index 5542cecc..fd046a18 100644 --- a/src/routes/acsHandlers.ts +++ b/src/routes/acsHandlers.ts @@ -123,7 +123,12 @@ export const loadICN = async ( const action = "loadICN"; try { - const { icn, first_name, last_name } = await requestWithMetrics( + const { + icn, + first_name, + last_name, + idTheftIndicator, + } = await requestWithMetrics( MVIRequestMetrics, (): Promise => { return req.mpiUserClient.getMpiTraitsForLoa3User(req.user.claims); @@ -135,6 +140,14 @@ export const loadICN = async ( action, result: "success", }); + + if (req.mpiUserClient.fraudBlockEnabled && idTheftIndicator) { + logger.warn("Fradulent identity detected, blocking login."); + return res.render("layout", { + body: "sensitive_error", + request_id: rTracer.id(), + }); + } req.user.claims.icn = icn; if (first_name) { req.user.claims.firstName = first_name; diff --git a/test/testServer.js b/test/testServer.js index 3fbe943f..ffba133a 100644 --- a/test/testServer.js +++ b/test/testServer.js @@ -36,6 +36,7 @@ const defaultTestingConfig = { spValidateNameIDFormat: true, spNameIDFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", spRequestAuthnContext: true, + fraudBlockEnabled: true, }; export const idpConfig = new IDPConfig(defaultTestingConfig);