Skip to content

Commit b5437be

Browse files
committed
Working Example
0 parents  commit b5437be

File tree

10 files changed

+2472
-0
lines changed

10 files changed

+2472
-0
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
VITE_API_URL=https://demo-api.incodesmile.com/0
2+
VITE_SDK_URL=https://sdk.incode.com/sdk/onBoarding-1.85.0.js
3+
4+
# HERE ONLY FOR DEMO PURPOSES, THE APIKEY AND THE FLOW_ID SHOULD NEVER BE IN THE FRONTEND.
5+
VITE_FAKE_BACKEND_APIURL=https://demo-api.incodesmile.com
6+
VITE_FAKE_BACKEND_APIKEY=
7+
VITE_FAKE_BACKEND_FLOWID=

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.cert/*.pem
6+
/.pnp
7+
.pnp.js
8+
.env
9+
10+
# testing
11+
/coverage
12+
13+
# production
14+
/dist
15+
16+
# misc
17+
.DS_Store
18+
.env.local
19+
.env.development.local
20+
.env.test.local
21+
.env.production.local
22+
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Face Authentication Validation Example
2+
This project demonstrates a secure face authentication flow using Incode's WebSDK with proper validation and session management. The application implements:
3+
4+
- **User hint input** for authentication (customer ID, email, or phone)
5+
- **Face authentication** using Incode's renderAuthFace SDK
6+
- **Session management** with IndexedDB to prevent reuse
7+
- **Backend validation** to verify authentication integrity by:
8+
- Matching candidate ID from the SDK with identity ID from the score API
9+
- Validating overall authentication status
10+
- Preventing token tampering and session replay attacks
11+
- Marking sessions as used to prevent reuse
12+
13+
This example showcases best practices for implementing face authentication in a web application with proper security measures.
14+
15+
# Requirements
16+
Vite requires Node.js version 14.18+, 16+. some templates require a higher Node.js version to work, please upgrade if your package manager warns about it.
17+
18+
# Install
19+
Run `npm install`
20+
# Config
21+
Copy `.env.example` to `.env.local` and add your local values
22+
```
23+
VITE_API_URL=https://demo-api.incodesmile.com/0
24+
VITE_SDK_URL=https://sdk.incode.com/sdk/onBoarding-1.80.1.js
25+
26+
# HERE ONLY FOR DEMO PURPOSES, THE APIKEY AND THE FLOW_ID SHOULD NEVER BE IN THE FRONTEND.
27+
VITE_FAKE_BACKEND_APIURL=https://demo-api.incodesmile.com
28+
VITE_FAKE_BACKEND_APIKEY=
29+
VITE_FAKE_BACKEND_FLOW_ID=
30+
```
31+
Remember the Flow holds the backend counter part of the process, some configurations there might affect the behavior of the WebSDK here.
32+
33+
# Fake Backend Server
34+
Starting and finishing the session must be done in the backend. To simplify development, this
35+
sample includes a `fake_backend.js` file that handles backend operations in the frontend.
36+
37+
**Important:** Replace this with a proper backend for production. The API key should NEVER be exposed in the frontend.
38+
39+
## Key Backend Functions
40+
41+
- `fakeBackendStart()` - Creates a new session and stores it in IndexedDB with `used: false`
42+
- `fakeBackendFinish()` - Retrieves the finish status from the API
43+
- `fakeBackendGetScore()` - Gets the authentication score from the API
44+
- `fakeBackendValidateAuthentication()` - Validates the authentication by:
45+
- Checking if the session exists and hasn't been used
46+
- Verifying the token matches the stored token
47+
- Comparing candidate ID with identity ID from the score
48+
- Ensuring overall status is "OK"
49+
- Marking the session as used to prevent reuse
50+
51+
# Run
52+
Vite is configured to serve the project using https and and expose him self, so you can easily test with your mobile phone on the local network.
53+
54+
run `npm run dev`
55+
56+
A new server will be exposed, the data will be in the terminal
57+
58+
# Build
59+
run `npm run build`
60+
61+
A new build will be created in `/dist` you can serve that build everywhere just remember to serve with https.
62+
63+
# Testing especific versions of the webSDK locally
64+
You can save the specific version needed under `/public` and change the `VITE_SDK_URL` variable on `.env.local` to something like:
65+
66+
```
67+
VITE_SDK_URL=/name-of-the-js-file.js
68+
```
69+

fake_backend.js

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
const apiurl = import.meta.env.VITE_FAKE_BACKEND_APIURL;
2+
const flowid = import.meta.env.VITE_FAKE_BACKEND_FLOWID;
3+
const apikey = import.meta.env.VITE_FAKE_BACKEND_APIKEY;
4+
5+
const defaultHeader = {
6+
"Content-Type": "application/json",
7+
"x-api-key": apikey,
8+
"api-version": "1.0",
9+
};
10+
11+
// Call Incode's `omni/start` API to create an Incode session which will include a
12+
// token in the response.
13+
const fakeBackendStart = async function () {
14+
const url = `${apiurl}/omni/start`;
15+
const params = {
16+
configurationId: flowid,
17+
// language: "en-US",
18+
// redirectionUrl: "https://example.com?custom_parameter=some+value",
19+
// externalCustomerId: "the id of the customer in your system",
20+
};
21+
22+
let response;
23+
try {
24+
response = await fetch(url, { method: "POST", body: JSON.stringify(params), headers: defaultHeader });
25+
if (!response.ok) {
26+
throw new Error("Request failed with code " + response.status);
27+
}
28+
} catch (e) {
29+
throw new Error("HTTP Post Error: " + e.message);
30+
}
31+
32+
// The session response has many values, but you should only pass the token to the frontend.
33+
const responseData = await response.json();
34+
const { token, interviewId } = responseData;
35+
36+
// Store session in local DB, session will be created as used: false.
37+
await addSession(interviewId, token);
38+
39+
return { token, interviewId };
40+
};
41+
42+
// Finishes the session started at /start
43+
const fakeBackendFinish = async function (token) {
44+
const url = `${apiurl}/omni/finish-status`;
45+
46+
let sessionHeaders = { ...defaultHeader };
47+
sessionHeaders["X-Incode-Hardware-Id"] = token;
48+
49+
let response;
50+
try {
51+
response = await fetch(url, { method: "GET", headers: sessionHeaders });
52+
if (!response.ok) {
53+
throw new Error("Request failed with code " + response.status);
54+
}
55+
} catch (e) {
56+
throw new Error("HTTP Post Error: " + e.message);
57+
}
58+
const { redirectionUrl, action } = await response.json();
59+
return { redirectionUrl, action };
60+
};
61+
62+
const fakeBackendValidateAuthentication = async function (interviewId, token, candidateId) {
63+
const session = await getSession(interviewId);
64+
65+
if (!session) {
66+
return {
67+
message: "No session found for interviewId " + interviewId,
68+
valid: false,
69+
};
70+
}
71+
// Prevents reuse of the same session.
72+
if (session.used) {
73+
return {
74+
message: "Session already used for interviewId " + interviewId,
75+
valid: false,
76+
};
77+
}
78+
79+
// Prevents usage of token from another interviewId.
80+
if (session.token !== token) {
81+
return {
82+
message: "Token mismatch for interviewId " + interviewId,
83+
valid: false,
84+
};
85+
}
86+
87+
let identityId, scoreStatus;
88+
try {
89+
// At this point we already verified that the token matches, but
90+
// to be clear about our intentions, we use the token stored in the
91+
// database to get the identityId and compare it with the candidateId.
92+
const scoreResponse = await fakeBackendGetScore(session.token);
93+
identityId = scoreResponse.authentication.identityId;
94+
scoreStatus = scoreResponse.overall.status;
95+
} catch (e) {
96+
// If there is an error communicating with API, we consider validation failed.
97+
return {
98+
message: "Error validating authentication for interviewId " + interviewId + ": " + e.message,
99+
valid: false,
100+
};
101+
}
102+
103+
// renderFaceAuth returns candidateId, which should match identityId from score,
104+
// this prevents tampering of the identityId in the frontend.
105+
if (identityId !== candidateId) {
106+
return {
107+
message: "Session data doesn't match for interviewId " + interviewId,
108+
valid: false,
109+
};
110+
}
111+
112+
// If backend score overall status is not OK, validation fails.
113+
if (scoreStatus !== "OK") {
114+
return {
115+
message: "Face Validation failed for interviewId " + interviewId,
116+
valid: false,
117+
};
118+
}
119+
120+
// Mark session as used so it can't be used again
121+
await markSessionAsUsed(interviewId);
122+
123+
// Only valid if all checks passed, we return the identityId that was validated.
124+
return {
125+
message: "Face Validation succeeded for interviewId " + interviewId,
126+
valid: true,
127+
identityId: identityId,
128+
};
129+
};
130+
131+
// Finishes the session started at /start
132+
const fakeBackendGetScore = async function (token) {
133+
const url = `${apiurl}/omni/get/score`;
134+
135+
let sessionHeaders = { ...defaultHeader };
136+
sessionHeaders["X-Incode-Hardware-Id"] = token;
137+
138+
let response;
139+
try {
140+
response = await fetch(url, { method: "GET", headers: sessionHeaders });
141+
if (!response.ok) {
142+
throw new Error("Request failed with code " + response.status);
143+
}
144+
} catch (e) {
145+
throw new Error("HTTP Post Error: " + e.message);
146+
}
147+
const score = await response.json();
148+
/* Example score
149+
{
150+
"authentication": {
151+
"overall": {
152+
"value": "89.2",
153+
"status": "OK"
154+
},
155+
"identityId": "68c851bedd5176f7d8bf4758"
156+
},
157+
"liveness": {
158+
"physicalAttack": {
159+
"value": "100.0",
160+
"status": "OK"
161+
},
162+
"spoofDetectionMethod": "SF",
163+
"overall": {
164+
"value": "100.0",
165+
"status": "OK"
166+
},
167+
"livenessScore": {
168+
"value": "100.0",
169+
"status": "OK"
170+
}
171+
},
172+
"deviceRisk": {
173+
"overall": {
174+
"status": "UNKNOWN"
175+
}
176+
},
177+
"behavioralRisk": {
178+
"overall": {
179+
"status": "UNKNOWN"
180+
}
181+
},
182+
"retryInfo": {},
183+
"documentOnEdgeInfo": {},
184+
"sessionRecording": {
185+
"mergedRecordingQualityChecks": {}
186+
},
187+
"reasonMsg": "This session passed because it passed all of Incode's tests: Liveness Detection",
188+
"overall": {
189+
"value": "94.6",
190+
"status": "OK"
191+
}
192+
}
193+
*/
194+
return score;
195+
};
196+
197+
/** Helper functions for sessions saving and retrieval from IndexedDB,
198+
* in production this should be handled with your backend or secure storage */
199+
200+
// Local database helper functions using IndexedDB
201+
const DB_NAME = "AuthenticationDB";
202+
const DB_VERSION = 1;
203+
const STORE_NAME = "sessions";
204+
205+
// Initialize IndexedDB
206+
function initDB() {
207+
return new Promise((resolve, reject) => {
208+
const request = indexedDB.open(DB_NAME, DB_VERSION);
209+
210+
request.onerror = () => reject(request.error);
211+
request.onsuccess = () => resolve(request.result);
212+
213+
request.onupgradeneeded = (event) => {
214+
const db = event.target.result;
215+
if (!db.objectStoreNames.contains(STORE_NAME)) {
216+
const objectStore = db.createObjectStore(STORE_NAME, { keyPath: "interviewId" });
217+
objectStore.createIndex("interviewId", "interviewId", { unique: true });
218+
}
219+
};
220+
});
221+
}
222+
223+
// Read a specific session from IndexedDB by interviewId
224+
async function getSession(interviewId) {
225+
const db = await initDB();
226+
return new Promise((resolve, reject) => {
227+
const transaction = db.transaction([STORE_NAME], "readonly");
228+
const objectStore = transaction.objectStore(STORE_NAME);
229+
const request = objectStore.get(interviewId);
230+
231+
request.onsuccess = () => resolve(request.result);
232+
request.onerror = () => reject(request.error);
233+
});
234+
}
235+
236+
// Add a new session to the database
237+
async function addSession(interviewId, token) {
238+
const db = await initDB();
239+
return new Promise((resolve, reject) => {
240+
const transaction = db.transaction([STORE_NAME], "readwrite");
241+
const objectStore = transaction.objectStore(STORE_NAME);
242+
const session = {
243+
interviewId,
244+
token,
245+
used: false,
246+
timestamp: new Date().toISOString(),
247+
};
248+
const request = objectStore.add(session);
249+
250+
request.onsuccess = () => resolve(session);
251+
request.onerror = () => reject(request.error);
252+
});
253+
}
254+
255+
// Update validation status for a session
256+
async function markSessionAsUsed(interviewId) {
257+
const db = await initDB();
258+
return new Promise((resolve, reject) => {
259+
const transaction = db.transaction([STORE_NAME], "readwrite");
260+
const objectStore = transaction.objectStore(STORE_NAME);
261+
const getRequest = objectStore.get(interviewId);
262+
263+
getRequest.onsuccess = () => {
264+
const session = getRequest.result;
265+
if (session) {
266+
session.used = true;
267+
const updateRequest = objectStore.put(session);
268+
updateRequest.onsuccess = () => resolve(session);
269+
updateRequest.onerror = () => reject(updateRequest.error);
270+
} else {
271+
resolve(null);
272+
}
273+
};
274+
getRequest.onerror = () => reject(getRequest.error);
275+
});
276+
}
277+
278+
export { fakeBackendStart, fakeBackendFinish, fakeBackendGetScore, fakeBackendValidateAuthentication };

0 commit comments

Comments
 (0)