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-41014 Fraud Identity Theft #509

Merged
merged 5 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion dev-config.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 12 additions & 6 deletions src/MpiUserClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ beforeEach(() => {
icn: "fakeICN",
first_name: "Edward",
last_name: "Paget",
id_theft_indicator: true,
crolarlibertyva marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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);
});
});
23 changes: 20 additions & 3 deletions src/MpiUserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = {
idp_uuid: user.uuid,
dslogon_edipi: user.edipi || null,
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/MpiUserClientConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
3 changes: 2 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
83 changes: 82 additions & 1 deletion src/routes/acsHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
15 changes: 14 additions & 1 deletion src/routes/acsHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> => {
return req.mpiUserClient.getMpiTraitsForLoa3User(req.user.claims);
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions test/testServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down