Skip to content

Commit

Permalink
Face match and liveness implementation (#15)
Browse files Browse the repository at this point in the history
* add: rekognito and amplify implementation

* add: liveness and face match implementation

* fix: optional payload

* add: `disableInstructionScreen` props to component
  • Loading branch information
marluanespiritusanto authored May 5, 2023
1 parent 620f234 commit 29037bb
Show file tree
Hide file tree
Showing 24 changed files with 5,614 additions and 425 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/dev-auth-registry-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ jobs:
tags: ${{ env.GAR_BASE }}/${{env.ARTIFACT_REGISTRY_NAME}}/${{ env.GITHUB_REPOSITORY_NAME_PART_SLUG }}:${{ env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
build-args: |
NODE_ENV=${{ env.NODE_ENV }}
REACT_APP_SOCKET_FACIAL_URL=${{ secrets.REACT_APP_SOCKET_FACIAL_URL }}
NEXT_PUBLIC_IAM_API=${{ secrets.NEXT_PUBLIC_IAM_API }}
NEXT_PUBLIC_CEDULA_API=${{ secrets.NEXT_PUBLIC_CEDULA_API }}
NEXT_PUBLIC_CEDULA_API_KEY=${{ secrets.NEXT_PUBLIC_CEDULA_API_KEY }}
NEXT_PUBLIC_SITE_KEY=${{ secrets.NEXT_PUBLIC_SITE_KEY }}
NEXT_PUBLIC_PHOTO_API=${{ secrets.NEXT_PUBLIC_PHOTO_API }}
NEXT_PUBLIC_PHOTO_API_KEY=${{ secrets.NEXT_PUBLIC_PHOTO_API_KEY }}
push: true
cache-from: type=registry,ref=${{ env.GAR_BASE }}/${{ env.GITHUB_REPOSITORY_NAME_PART_SLUG }}:${{ env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
cache-to: type=inline
Expand Down Expand Up @@ -111,11 +112,12 @@ jobs:
region: ${{ secrets.GCP_REGION }}
env_vars: |
NODE_ENV=${{ env.NODE_ENV }},
REACT_APP_SOCKET_FACIAL_URL=${{ secrets.REACT_APP_SOCKET_FACIAL_URL }},
NEXT_PUBLIC_IAM_API=${{ secrets.NEXT_PUBLIC_IAM_API }},
NEXT_PUBLIC_CEDULA_API=${{ secrets.NEXT_PUBLIC_CEDULA_API }},
NEXT_PUBLIC_CEDULA_API_KEY=${{ secrets.NEXT_PUBLIC_CEDULA_API_KEY }},
NEXT_PUBLIC_SITE_KEY=${{ secrets.NEXT_PUBLIC_SITE_KEY }},
NEXT_PUBLIC_PHOTO_API=${{ secrets.NEXT_PUBLIC_PHOTO_API }},
NEXT_PUBLIC_PHOTO_API_KEY=${{ secrets.NEXT_PUBLIC_PHOTO_API_KEY }},
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v1
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/prod-auth-registry-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ jobs:
tags: ${{ env.GAR_BASE }}/${{env.ARTIFACT_REGISTRY_NAME}}/${{ env.GITHUB_REPOSITORY_NAME_PART_SLUG }}:${{ env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
build-args: |
NODE_ENV=${{ env.NODE_ENV }}
REACT_APP_SOCKET_FACIAL_URL=${{ secrets.REACT_APP_SOCKET_FACIAL_URL }}
NEXT_PUBLIC_IAM_API=${{ secrets.NEXT_PUBLIC_IAM_API }}
NEXT_PUBLIC_CEDULA_API=${{ secrets.NEXT_PUBLIC_CEDULA_API }}
NEXT_PUBLIC_CEDULA_API_KEY=${{ secrets.NEXT_PUBLIC_CEDULA_API_KEY }}
NEXT_PUBLIC_SITE_KEY=${{ secrets.NEXT_PUBLIC_SITE_KEY }}
NEXT_PUBLIC_PHOTO_API=${{ secrets.NEXT_PUBLIC_PHOTO_API }}
NEXT_PUBLIC_PHOTO_API_KEY=${{ secrets.NEXT_PUBLIC_PHOTO_API_KEY }}
push: true
cache-from: type=registry,ref=${{ env.GAR_BASE }}/${{ env.GITHUB_REPOSITORY_NAME_PART_SLUG }}:${{ env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
cache-to: type=inline
Expand Down Expand Up @@ -102,11 +103,12 @@ jobs:
region: ${{ secrets.GCP_REGION }}
env_vars: |
NODE_ENV=${{ env.NODE_ENV }},
REACT_APP_SOCKET_FACIAL_URL=${{ secrets.REACT_APP_SOCKET_FACIAL_URL }},
NEXT_PUBLIC_IAM_API=${{ secrets.NEXT_PUBLIC_IAM_API }},
NEXT_PUBLIC_CEDULA_API=${{ secrets.NEXT_PUBLIC_CEDULA_API }},
NEXT_PUBLIC_CEDULA_API_KEY=${{ secrets.NEXT_PUBLIC_CEDULA_API_KEY }},
NEXT_PUBLIC_SITE_KEY=${{ secrets.NEXT_PUBLIC_SITE_KEY }},
NEXT_PUBLIC_PHOTO_API=${{ secrets.NEXT_PUBLIC_PHOTO_API }},
NEXT_PUBLIC_PHOTO_API_KEY=${{ secrets.NEXT_PUBLIC_PHOTO_API_KEY }},
- name: Testing Service with curl
run: curl "${{ steps.deploy.outputs.url }}"
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# AWS Amplify
amplify/\#current-cloud-backend
amplify/.config/local-*
amplify/mock-data
amplify/backend/amplify-meta.json
amplify/backend/awscloudformation

# env
*.env

Expand Down
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ RUN yarn install --frozen-lockfile

COPY . .

ARG REACT_APP_SOCKET_FACIAL_URL
ENV REACT_APP_SOCKET_FACIAL_URL=${REACT_APP_SOCKET_FACIAL_URL}
ARG NEXT_PUBLIC_PHOTO_API
ENV NEXT_PUBLIC_PHOTO_API=${NEXT_PUBLIC_PHOTO_API}

ARG NEXT_PUBLIC_PHOTO_API_KEY
ENV NEXT_PUBLIC_PHOTO_API_KEY=${NEXT_PUBLIC_PHOTO_API_KEY}

ARG NEXT_PUBLIC_IAM_API
ENV NEXT_PUBLIC_IAM_API=${NEXT_PUBLIC_IAM_API}
Expand Down
3 changes: 2 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
publicRuntimeConfig: {
REACT_APP_SOCKET_FACIAL_URL: process.env.REACT_APP_SOCKET_FACIAL_URL,
NEXT_PUBLIC_PHOTO_API: process.env.NEXT_PUBLIC_PHOTO_API,
NEXT_PUBLIC_PHOTO_API_KEY: process.env.NEXT_PUBLIC_PHOTO_API_KEY,
NEXT_PUBLIC_IAM_API: process.env.NEXT_PUBLIC_IAM_API,
NEXT_PUBLIC_CEDULA_API: process.env.NEXT_PUBLIC_CEDULA_API,
NEXT_PUBLIC_CEDULA_API_KEY: process.env.NEXT_PUBLIC_CEDULA_API_KEY,
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "auth-registry-frontend",
"description": "Registry App for IAM users",
"version": "0.0.1",
"version": "0.0.2",
"private": true,
"author": "OGTIC",
"scripts": {
Expand All @@ -11,12 +11,15 @@
"lint": "next lint"
},
"dependencies": {
"@aws-amplify/ui-react-liveness": "^1.0.0",
"@aws-sdk/client-rekognition": "^3.326.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/roboto": "^4.5.8",
"@hookform/resolvers": "2.9.7",
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"aws-amplify": "^5.1.4",
"axios": "^1.4.0",
"eslint": "8.35.0",
"eslint-config-next": "13.2.3",
Expand All @@ -26,7 +29,6 @@
"react-google-recaptcha": "^2.1.0",
"react-hook-form": "7.34.0",
"sharp": "^0.32.1",
"socket.io-client": "^4.6.1",
"sweetalert2": "^11.7.3",
"typescript": "4.9.5",
"yup": "0.32.11"
Expand All @@ -36,7 +38,6 @@
"@types/node": "^18.16.3",
"@types/react": "^18.2.1",
"@types/react-dom": "^18.2.2",
"@types/react-google-recaptcha": "^2.1.5",
"@types/socket.io-client": "^3.0.0"
"@types/react-google-recaptcha": "^2.1.5"
}
}
}
50 changes: 50 additions & 0 deletions src/components/biometric/face-liveness-detector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { FaceLivenessDetector } from "@aws-amplify/ui-react-liveness";
import { Loader, ThemeProvider } from "@aws-amplify/ui-react";
import React from "react";

export function LivenessQuickStartReact({ handleNextForm, cedula }: any) {
const next = handleNextForm;
const id = cedula;
const [loading, setLoading] = React.useState<boolean>(true);
const [sessionId, setSessionId] = React.useState<string>("");

React.useEffect(() => {
const fetchCreateLiveness = async () => {
const response = await fetch(`/api/biometric`, { method: "POST" });
const { sessionId } = await response.json();

setSessionId(sessionId);
setLoading(false);
};

fetchCreateLiveness();
}, []);

const handleAnalysisComplete = async () => {
const response = await fetch(
`/api/biometric?sessionId=${sessionId}&cedula=${id}`
);
const data = await response.json();

if (data.match) {
next();
} else {
alert("No se ha podido validar correctamente la identidad.");
}
};

return (
<ThemeProvider>
{loading ? (
<Loader />
) : (
<FaceLivenessDetector
sessionId={sessionId}
region="us-east-1"
onAnalysisComplete={handleAnalysisComplete}
disableInstructionScreen={true}
/>
)}
</ThemeProvider>
);
}
2 changes: 1 addition & 1 deletion src/components/elements/cardAuth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const CardAuth = ({
}}
>
<div style={{ marginRight: "15px", marginTop: "-3px" }}>
<Image src={LogoDedo.src} alt="Logo" width="40" />
<Image src={LogoDedo.src} alt="Logo" width="40" height="20" />
</div>
<Typography
color="primary"
Expand Down
3 changes: 2 additions & 1 deletion src/components/layout/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function Index() {
<GridContainer spacing={4}>
<GridItem md={12} lg={3}>
<div style={{ display: "flex" }}>
<Image src={logoGOB.src} alt="logo" width="200" />
<Image src={logoGOB.src} alt="logo" width="200" height="100" />
</div>
</GridItem>
<GridItem md={12} lg={9}>
Expand Down Expand Up @@ -88,6 +88,7 @@ export default function Index() {
src={logoOGTIC.src}
alt="logo ogtic"
width="50"
height="25"
/>
</div>
</GridItem>
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/navBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function Index() {
<Toolbar>
<div style={{ flexGrow: 1, paddingTop: "8px" }}>
<Link href={routes.auth.home}>
<Image src={Logo.src} alt="logo" width="200" />
<Image src={Logo.src} alt="logo" width="200" height="10" />
</Link>
</div>
<AppsIcon fontSize="large" />
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { validateSameSiteRequest } from "./same-site-validator";
export { getRekognitionClient } from "./rekognition";
21 changes: 21 additions & 0 deletions src/helpers/rekognition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Rekognition } from "@aws-sdk/client-rekognition";
import { Amplify, withSSRContext } from "aws-amplify";
import { NextApiRequest } from "next/types";

import awsExports from "../aws-exports";

Amplify.configure({ ...awsExports, ssr: true });

export async function getRekognitionClient(
req: NextApiRequest
): Promise<Rekognition> {
const { Credentials } = withSSRContext({ req });
const credentials = await Credentials.get();
const endpoint = "https://rekognition.us-east-1.amazonaws.com";

return new Rekognition({
region: "us-east-1",
credentials,
endpoint,
});
}
10 changes: 10 additions & 0 deletions src/helpers/same-site-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IncomingHttpHeaders } from "node:http";

export function validateSameSiteRequest(headers: IncomingHttpHeaders) {
const fetchHeaderKey = "sec-fetch-site";
const fetchHeaderValue = "same-origin";

return (
headers[fetchHeaderKey] && headers[fetchHeaderKey] === fetchHeaderValue
);
}
5 changes: 5 additions & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material/styles";
import type { AppProps } from "next/app";
import { Amplify } from "aws-amplify";
import Head from "next/head";

import Layout from "../components/layout";
import awsExports from "../aws-exports";
import { theme } from "../themes";

import "@aws-amplify/ui-react/styles.css";
import "@/styles/globals.css";

Amplify.configure(awsExports);

export default function App({ Component, pageProps }: AppProps) {
return (
<>
Expand Down
72 changes: 72 additions & 0 deletions src/pages/api/biometric/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextApiRequest, NextApiResponse } from "next/types";
import axios from "axios";

import { getRekognitionClient, validateSameSiteRequest } from "@/helpers";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<any | void>
): Promise<any> {
const isValidRequest = validateSameSiteRequest(req.headers);

if (!isValidRequest) {
return res.status(401).send(null);
}

const http = axios.create({
baseURL: process.env.NEXT_PUBLIC_PHOTO_API,
});

const rekognition = await getRekognitionClient(req);

if (req.method === "GET") {
const { sessionId, cedula } = req.query;
const SessionId = sessionId as string;

const response = await rekognition.getFaceLivenessSessionResults({
SessionId,
});

let isLive: any;
let base64Image: string = "";
let result: any;

if (response.Confidence) {
isLive = response.Confidence > 90;
}

if (response.ReferenceImage && response.ReferenceImage.Bytes) {
base64Image = response.ReferenceImage.Bytes.toString();

const { data } = await http.get(`/${cedula}/photo`, {
params: {
"api-key": process.env.NEXT_PUBLIC_PHOTO_API_KEY,
},
responseType: "arraybuffer",
});

result = await rekognition.compareFaces({
SimilarityThreshold: 80,
TargetImage: {
Bytes: data,
},
SourceImage: {
Bytes: response.ReferenceImage.Bytes,
},
});
}

const { FaceMatches } = result;
const isFaceMatched =
FaceMatches && FaceMatches.length && FaceMatches[0].Similarity > 90;

return res.status(200).json({
match: isFaceMatched,
});
} else if (req.method === "POST") {
const { SessionId: sessionId } =
await rekognition.createFaceLivenessSession({});

return res.status(201).json({ sessionId });
}
}
9 changes: 8 additions & 1 deletion src/pages/api/citizens/[cedula].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { NextApiRequest, NextApiResponse } from "next/types";
import axios from "axios";

import { CitizensBasicInformationResponse } from "../types";
import { validateSameSiteRequest } from "@/helpers";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<CitizensBasicInformationResponse>
res: NextApiResponse<CitizensBasicInformationResponse | void>
): Promise<void> {
const isValidRequest = validateSameSiteRequest(req.headers);

if (!isValidRequest) {
return res.status(401).send();
}

const http = axios.create({
baseURL: process.env.NEXT_PUBLIC_CEDULA_API,
});
Expand Down
7 changes: 7 additions & 0 deletions src/pages/api/iam/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { NextApiRequest, NextApiResponse } from "next/types";
import axios from "axios";

import { validateSameSiteRequest } from "@/helpers";
import { VerifyIamUserResponse } from "../types";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<any>
): Promise<any> {
const isValidRequest = validateSameSiteRequest(req.headers);

if (!isValidRequest) {
return res.status(401).send(null);
}

const http = axios.create({
baseURL: process.env.NEXT_PUBLIC_IAM_API,
});
Expand Down
1 change: 0 additions & 1 deletion src/pages/register/confirmation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export default function Index() {
});

const onSubmit = (data: IFormInputs) => {
console.log(data);
router.push(routes.register.registered);
};

Expand Down
Loading

0 comments on commit 29037bb

Please sign in to comment.