Skip to content

Commit

Permalink
Chore: Migrate credential stuffing to app INTER-911 (#158)
Browse files Browse the repository at this point in the history
* chore: move credential stuffing client to `app`

* chore: move credential stuffing server to `app`
  • Loading branch information
JuroUhlar authored Sep 19, 2024
1 parent 2009679 commit c2dd3ff
Show file tree
Hide file tree
Showing 8 changed files with 43 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { useState } from 'react';
import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper';
import React from 'react';
Expand All @@ -12,10 +14,10 @@ import shownIcon from './iconShown.svg';
import Image from 'next/image';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import { TEST_IDS } from '../../client/testIDs';
import { LoginPayload, LoginResponse } from '../api/credential-stuffing/authenticate';
import { useMutation } from 'react-query';
import { LoginPayload, LoginResponse } from './api/authenticate/route';

export default function Index() {
export function CredentialStuffing() {
const { getData: getVisitorData } = useVisitorData(
{ ignoreCache: true },
{
Expand All @@ -32,7 +34,7 @@ export default function Index() {
mutationKey: ['login attempt'],
mutationFn: async ({ username, password }) => {
const { requestId, visitorId } = await getVisitorData({ ignoreCache: true });
const response = await fetch('/api/credential-stuffing/authenticate', {
const response = await fetch('/credential-stuffing/api/authenticate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Op } from 'sequelize';
import { isValidPostRequest, sequelize } from '../../../server/server';
import { Severity, getAndValidateFingerprintResult } from '../../../server/checks';
import { NextApiRequest, NextApiResponse } from 'next';
import { CREDENTIAL_STUFFING_COPY } from '../../../server/credentialStuffing/copy';
import { env } from '../../../env';
import { LoginAttemptDbModel, LoginAttemptResult } from '../../../server/credentialStuffing/database';
import { NextResponse } from 'next/server';
import { env } from 'process';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { CREDENTIAL_STUFFING_COPY } from '../../../../server/credentialStuffing/copy';
import { LoginAttemptResult, LoginAttemptDbModel } from '../../../../server/credentialStuffing/database';
import { sequelize } from '../../../../server/server';

export type LoginPayload = {
username: string;
Expand Down Expand Up @@ -32,30 +32,21 @@ function getKnownVisitorIds() {
return visitorIdsFromEnv ? [...defaultVisitorIds, ...visitorIdsFromEnv] : defaultVisitorIds;
}

export default async function loginHandler(req: NextApiRequest, res: NextApiResponse) {
// This API route accepts only POST requests.
const reqValidation = isValidPostRequest(req);
if (!reqValidation.okay) {
res.status(405).send({ severity: 'error', message: reqValidation.error });
return;
}

const { requestId, username, password, visitorId: clientVisitorId } = req.body as LoginPayload;
export async function POST(req: Request): Promise<NextResponse<LoginResponse>> {
const { requestId, username, password, visitorId: clientVisitorId } = (await req.json()) as LoginPayload;

// Get the full Identification result from Fingerprint Server API and validate its authenticity
const fingerprintResult = await getAndValidateFingerprintResult({ requestId, req });
if (!fingerprintResult.okay) {
logLoginAttempt(clientVisitorId, username, 'RequestIdValidationFailed');
res.status(403).send({ severity: 'error', message: fingerprintResult.error });
return;
return NextResponse.json({ message: fingerprintResult.error, severity: 'error' }, { status: 403 });
}

// Get visitorId from the Server API Identification event
const visitorId = fingerprintResult.data.products?.identification?.data?.visitorId;
if (!visitorId) {
logLoginAttempt(clientVisitorId, username, 'RequestIdValidationFailed');
res.status(403).send({ severity: 'error', message: 'Visitor ID not found.' });
return;
return NextResponse.json({ message: 'Visitor ID not found.', severity: 'error' }, { status: 403 });
}

// If the visitor ID performed 5 unsuccessful login attempts during the last 24 hours we do not perform the login.
Expand All @@ -73,28 +64,31 @@ export default async function loginHandler(req: NextApiRequest, res: NextApiResp
});
if (failedLoginsToday.count >= 5) {
logLoginAttempt(visitorId, username, 'TooManyLoginAttempts');
res.status(403).send({ severity: 'error', message: CREDENTIAL_STUFFING_COPY.tooManyAttempts });
return;
return NextResponse.json({ message: CREDENTIAL_STUFFING_COPY.tooManyAttempts, severity: 'error' }, { status: 403 });
}

// If the provided credentials are incorrect, we return an error.
if (!credentialsAreCorrect(username, password)) {
logLoginAttempt(visitorId, username, 'IncorrectCredentials');
res.status(403).send({ severity: 'error', message: CREDENTIAL_STUFFING_COPY.invalidCredentials });
return;
return NextResponse.json(
{ message: CREDENTIAL_STUFFING_COPY.invalidCredentials, severity: 'error' },
{ status: 403 },
);
}

// If the provided credentials are correct but the user never logged in using this browser,
// we force the user to use multi-factor authentication (text message, email, authenticator app, etc.)
if (!mockedUser.knownVisitorIds.includes(visitorId)) {
logLoginAttempt(visitorId, username, 'UnknownBrowserEnforceMFA');
res.status(403).send({ severity: 'warning', message: CREDENTIAL_STUFFING_COPY.differentVisitorIdUseMFA });
return;
return NextResponse.json(
{ message: CREDENTIAL_STUFFING_COPY.differentVisitorIdUseMFA, severity: 'warning' },
{ status: 403 },
);
}

// If the provided credentials are correct and we recognize the browser, we log the user in
logLoginAttempt(visitorId, username, 'Success');
res.status(200).send({ severity: 'success', message: CREDENTIAL_STUFFING_COPY.success });
return NextResponse.json({ message: CREDENTIAL_STUFFING_COPY.success, severity: 'success' });
}

// Dummy action simulating authentication.
Expand Down
9 changes: 9 additions & 0 deletions src/app/credential-stuffing/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { USE_CASES } from '../../../client/components/common/content';
import { generateUseCaseMetadata } from '../../../client/components/common/seo';
import { CredentialStuffing } from '.././CredentialStuffing';

export const metadata = generateUseCaseMetadata(USE_CASES.credentialStuffing);

export default function CredentialStuffingPage() {
return <CredentialStuffing />;
}
File renamed without changes
File renamed without changes
9 changes: 9 additions & 0 deletions src/app/credential-stuffing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { USE_CASES } from '../../client/components/common/content';
import { generateUseCaseMetadata } from '../../client/components/common/seo';
import { CredentialStuffing } from './CredentialStuffing';

export const metadata = generateUseCaseMetadata(USE_CASES.credentialStuffing);

export default function CredentialStuffingPage() {
return <CredentialStuffing />;
}
13 changes: 0 additions & 13 deletions src/pages/credential-stuffing/embed.tsx

This file was deleted.

0 comments on commit c2dd3ff

Please sign in to comment.