Skip to content

Commit be7211d

Browse files
committed
Implemented review suggestions:
* warning on how to respond errors * changed from fakebackend to exampleBackend * moved finishing of the session to the verify endpoint * moved from "used" to "status", and now we reject or approve
1 parent 0df58f5 commit be7211d

File tree

4 files changed

+109
-70
lines changed

4 files changed

+109
-70
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Face Authentication Validation Example
22
This project demonstrates a secure face authentication flow using Incode's WebSDK with proper validation and session management. The application implements:
33

4-
- **User hint input** for authentication (customer ID, email, or phone)
4+
- **User hint input** for authentication (customerId, email, or phone)
55
- **Face authentication** using Incode's renderAuthFace SDK
66
- **Session management** with IndexedDB to prevent reuse
77
- **Backend validation** to verify authentication integrity by:
8-
- Matching candidate ID from the SDK with identity ID from the score API
8+
- Matching candidate from the SDK with identityId from the score API
99
- Validating overall authentication status
1010
- Preventing token tampering and session replay attacks
1111
- Marking sessions as used to prevent reuse
@@ -22,24 +22,25 @@ sequenceDiagram
2222
participant IndexedDB
2323
2424
Note over Frontend: Enter hint:<br> email/phone/identityId
25+
Note over Frontend: WebSDK: create()
2526
Frontend->>Backend: Start Session in Backend
2627
Backend->>IncodeAPI: Create new session<br>{configurationId, apikey}
2728
Note over IncodeAPI: /omni/start
2829
IncodeAPI-->>Backend: Returns Session<br>{token, interviewId}
2930
Backend->>IndexedDB: Store session<br>{key: interviewId, backToken: token, used: false)
3031
Backend-->>Frontend: Return Session<br>{token, interviewId}
3132
32-
Note over Frontend: renderAuthFace(token, hint)
33+
Note over Frontend: WebSDK: renderAuthFace(token, hint)
3334
Note over Frontend: User completes face authentication
34-
Note over Frontend:Returns:<br>{candidateId}
35+
Note over Frontend:Returns:<br>{candidate}
3536
3637
3738
Frontend->>Backend: Mark Session as Completed<br>{token}
3839
Note over IncodeAPI: /0/omni/finish-status
3940
Backend->>IncodeAPI: Get finish status
4041
IncodeAPI-->>Backend: Return:<br>{redirectionUrl, action}//Unused
4142
42-
Frontend->>Backend: Validate Authentication<br>{interviewId, token, candidateId}
43+
Frontend->>Backend: Validate Authentication<br>{interviewId, token, candidate}
4344
Backend->>IndexedDB: Get Session Info:<br>{key:interviewId}
4445
IndexedDB-->>Backend: {backToken, used}
4546
Note over Backend: Validate interviewId exists in DB
@@ -48,7 +49,7 @@ sequenceDiagram
4849
Backend->>IncodeAPI: Get Authentication Score<br>{token:backToken}
4950
Note over IncodeAPI: /0/omni/get/score
5051
IncodeAPI-->>Backend: {status, identityId}
51-
Note over Backend: Validate candidateId matches identityId<br> candidateId === identityId
52+
Note over Backend: Validate candidate matches identityId<br> candidate === identityId
5253
Note over Backend: Validate Score is OK:<br>status === "OK"
5354
Backend->>IndexedDB: Mark session as used<br>{interviewId, used:true}
5455
Backend-->>Frontend: Return validation result<br>{message, valid, identityId}
@@ -87,7 +88,7 @@ sample includes a `fake_backend.js` file that handles backend operations in the
8788
- `fakeBackendValidateAuthentication()` - Validates the authentication by:
8889
- Checking if the session exists and hasn't been used
8990
- Verifying the token matches the stored token
90-
- Comparing candidate ID with identity ID from the score
91+
- Comparing candidate with identityId from the score
9192
- Ensuring overall status is "OK"
9293
- Marking the session as used to prevent reuse
9394

fake_backend.js renamed to example_backend.js

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ const defaultHeader = {
88
"api-version": "1.0",
99
};
1010

11-
// Call Incode's `omni/start` API to create an Incode session which will include a
11+
// Public: Call Incode's `omni/start` API to create an Incode session which will include a
1212
// token in the response.
13-
const fakeBackendStart = async function () {
13+
const start = async function (identityId) {
1414
const url = `${apiurl}/omni/start`;
1515
const params = {
1616
configurationId: flowid,
@@ -34,102 +34,134 @@ const fakeBackendStart = async function () {
3434
const { token, interviewId } = responseData;
3535

3636
// Store session in local DB, session will be created as used: false.
37-
await addSession(interviewId, token);
37+
await addSession(interviewId, token, identityId);
3838

3939
return { token, interviewId };
4040
};
4141

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) {
42+
// Public: Verify the authentication by checking the score and session data
43+
const verifyAuthentication = async function (interviewId, token, candidate) {
6344
const session = await getSession(interviewId);
6445

46+
// Prevents usage of session that doesn't exist.
6547
if (!session) {
6648
return {
49+
// Detailed debug message, in production you might want to avoid exposing internal details.
6750
message: "No session found for interviewId " + interviewId,
6851
valid: false,
6952
};
7053
}
54+
7155
// Prevents reuse of the same session.
72-
if (session.used) {
56+
if (session.status !== "pending") {
7357
return {
58+
// Detailed debug message, in production you might want to avoid exposing internal details.
7459
message: "Session already used for interviewId " + interviewId,
7560
valid: false,
7661
};
7762
}
7863

7964
// Prevents usage of token from another interviewId.
8065
if (session.token !== token) {
66+
// Mark the session as rejected.
67+
await updateSession(interviewId, "rejected");
8168
return {
69+
// Detailed debug message, in production you might want to avoid exposing internal details.
8270
message: "Token mismatch for interviewId " + interviewId,
8371
valid: false,
8472
};
8573
}
74+
75+
// Prevents usage of candidate that doesn't match the identityId stored in session.
76+
if (session.identityId !== candidate) {
77+
// Mark the session as rejected.
78+
await updateSession(interviewId, "rejected");
79+
return {
80+
// Detailed debug message, in production you might want to avoid exposing internal details.
81+
message: "Token identityId and candidate mismatch for interviewId " + interviewId,
82+
valid: false,
83+
};
84+
}
85+
86+
// Finishing the session stop it from being changed further and triggers score calculation and business rules.
87+
await finish(token); // Mark session as finished in Incode backend
8688

8789
let identityId, scoreStatus;
8890
try {
8991
// At this point we already verified that the token matches, but
9092
// 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+
// database to get the identityId and compare it with the candidate.
94+
const scoreResponse = await getScore(session.token);
9395
identityId = scoreResponse.authentication.identityId;
9496
scoreStatus = scoreResponse.overall.status;
9597
} catch (e) {
98+
// Mark the session as rejected.
99+
await updateSession(interviewId, "rejected");
96100
// If there is an error communicating with API, we consider validation failed.
97101
return {
102+
// Detailed debug message, in production you might want to avoid exposing internal details.
98103
message: "Error validating authentication for interviewId " + interviewId + ": " + e.message,
99104
valid: false,
100105
};
101106
}
102107

103-
// renderFaceAuth returns candidateId, which should match identityId from score,
108+
// renderFaceAuth returns candidate, which should match identityId from score,
104109
// this prevents tampering of the identityId in the frontend.
105-
if (identityId !== candidateId) {
110+
if (identityId !== candidate) {
111+
// Mark the session as rejected.
112+
await updateSession(interviewId, "rejected");
106113
return {
114+
// Detailed debug message, in production you might want to avoid exposing internal details.
107115
message: "Session data doesn't match for interviewId " + interviewId,
108116
valid: false,
109117
};
110118
}
111119

112120
// If backend score overall status is not OK, validation fails.
113121
if (scoreStatus !== "OK") {
122+
// Mark the session as rejected.
123+
await updateSession(interviewId, "rejected");
114124
return {
125+
// Detailed debug message, in production you might want to avoid exposing internal details.
115126
message: "Face Validation failed for interviewId " + interviewId,
116127
valid: false,
117128
};
118129
}
119130

120-
// Mark session as used so it can't be used again
121-
await markSessionAsUsed(interviewId);
131+
// Mark the session as approved since all checks passed.
132+
await updateSession(interviewId, "approved");
122133

123134
// Only valid if all checks passed, we return the identityId that was validated.
124135
return {
136+
// Detailed debug message, in production you might want to avoid exposing internal details.
125137
message: "Face Validation succeeded for interviewId " + interviewId,
126138
valid: true,
127139
identityId: identityId,
128140
};
129141
};
130142

131-
// Finishes the session started at /start
132-
const fakeBackendGetScore = async function (token) {
143+
// Private: Calls Incode's `omni/finish-status` API mark the session as finished
144+
const finish = async function (token) {
145+
const url = `${apiurl}/omni/finish-status`;
146+
147+
let sessionHeaders = { ...defaultHeader };
148+
sessionHeaders["X-Incode-Hardware-Id"] = token;
149+
150+
let response;
151+
try {
152+
response = await fetch(url, { method: "GET", headers: sessionHeaders });
153+
if (!response.ok) {
154+
throw new Error("Request failed with code " + response.status);
155+
}
156+
} catch (e) {
157+
throw new Error("HTTP Post Error: " + e.message);
158+
}
159+
const { redirectionUrl, action } = await response.json();
160+
return { redirectionUrl, action };
161+
};
162+
163+
// Private: Call Incode's `omni/get/score` API to retrieve the score for the session
164+
const getScore = async function (token) {
133165
const url = `${apiurl}/omni/get/score`;
134166

135167
let sessionHeaders = { ...defaultHeader };
@@ -234,15 +266,16 @@ async function getSession(interviewId) {
234266
}
235267

236268
// Add a new session to the database
237-
async function addSession(interviewId, token) {
269+
async function addSession(interviewId, token, identityId) {
238270
const db = await initDB();
239271
return new Promise((resolve, reject) => {
240272
const transaction = db.transaction([STORE_NAME], "readwrite");
241273
const objectStore = transaction.objectStore(STORE_NAME);
242274
const session = {
243275
interviewId,
244276
token,
245-
used: false,
277+
identityId,
278+
status: "pending",
246279
timestamp: new Date().toISOString(),
247280
};
248281
const request = objectStore.add(session);
@@ -253,7 +286,12 @@ async function addSession(interviewId, token) {
253286
}
254287

255288
// Update validation status for a session
256-
async function markSessionAsUsed(interviewId) {
289+
async function updateSession(interviewId, status) {
290+
291+
if (status !== "rejected" && status !== "approved") {
292+
throw new Error("Invalid status. Must be 'rejected' or 'approved'.");
293+
}
294+
257295
const db = await initDB();
258296
return new Promise((resolve, reject) => {
259297
const transaction = db.transaction([STORE_NAME], "readwrite");
@@ -263,7 +301,7 @@ async function markSessionAsUsed(interviewId) {
263301
getRequest.onsuccess = () => {
264302
const session = getRequest.result;
265303
if (session) {
266-
session.used = true;
304+
session.status = status;
267305
const updateRequest = objectStore.put(session);
268306
updateRequest.onsuccess = () => resolve(session);
269307
updateRequest.onerror = () => reject(updateRequest.error);
@@ -275,4 +313,5 @@ async function markSessionAsUsed(interviewId) {
275313
});
276314
}
277315

278-
export { fakeBackendStart, fakeBackendFinish, fakeBackendGetScore, fakeBackendValidateAuthentication };
316+
const exampleBackend = { start, verifyAuthentication }
317+
export default exampleBackend;

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
<body>
1212
<main id="app">
1313
<div id="user-hint-container">
14-
<label for="user-hint-input">User Hint (Customer ID, Email, or Phone):</label>
15-
<input type="text" id="user-hint-input" placeholder="Enter customer ID, email, or phone" />
14+
<label for="user-hint-input">User Hint (identityId):</label>
15+
<input type="text" id="user-hint-input" placeholder="Enter identityId" />
1616
<button id="continue-btn">Continue</button>
1717
</div>
1818
<div id="camera-container"></div>

main.js

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { fakeBackendStart, fakeBackendFinish, fakeBackendValidateAuthentication } from "./fake_backend";
1+
/* main.js */
2+
3+
/** Example Backend, this functions should be replaced with your actual backend implementation **/
4+
import exampleBackend from "./example_backend";
25

36
let incodeSDKInstance;
47
let incodeSession;
5-
let candidateId;
8+
let candidate;
69

710
const cameraContainer = document.getElementById("camera-container");
811

@@ -22,7 +25,7 @@ async function authenticate() {
2225
const userHintDiv = document.getElementById("user-hint-container");
2326
userHintDiv.style.display = "none";
2427

25-
incodeSession = await fakeBackendStart();
28+
incodeSession = await exampleBackend.start();
2629
renderAuthentication(hintInput.value);
2730
}
2831

@@ -50,43 +53,39 @@ function finishAuthentication(response) {
5053
}
5154
*/
5255

53-
candidateId = response.candidate; // Store candidate globally
56+
candidate = response.candidate; // Store candidate globally
5457

55-
fakeBackendFinish(incodeSession.token)
56-
.then((backendResponse) => {
57-
console.log(backendResponse);
58-
const container = document.getElementById("finish-container");
59-
container.innerHTML = `
60-
<h1>Authentication Finished</h1>
61-
<p><strong>Overall Status:</strong> ${response.overallStatus}</p>
62-
<p><strong>Candidate:</strong> ${response.candidate}</p>
58+
const container = document.getElementById("finish-container");
59+
container.innerHTML = `
60+
<h1>Authentication Process Finished</h1>
61+
<p><strong>Candidate:</strong> ${candidate}</p>
6362
<button id="verify-authentication-btn">Verify Authentication</button>
6463
`;
65-
document.getElementById("verify-authentication-btn").addEventListener("click", verifyAuthentication);
66-
})
67-
.catch((e) => {
68-
showError(e);
69-
});
64+
document.getElementById("verify-authentication-btn").addEventListener("click", verifyAuthentication);
7065
}
7166

7267
// 4.- Verify the authentication against the score
7368
async function verifyAuthentication() {
74-
console.log("Verifying authentication for candidateId:", candidateId);
69+
console.log("Verifying authentication for candidate:", candidate);
7570
try {
76-
const validationResult = await fakeBackendValidateAuthentication(incodeSession.interviewId, incodeSession.token, candidateId);
71+
const validationResult = await exampleBackend.verifyAuthentication(
72+
incodeSession.interviewId,
73+
incodeSession.token,
74+
candidate,
75+
);
7776
console.log("Validation result:", validationResult);
7877

7978
const container = document.getElementById("finish-container");
8079
container.innerHTML += `
8180
<hr>
8281
<h2>Authentication Verification</h2>
8382
<p><strong>Interview ID:</strong> ${incodeSession.interviewId}</p>
84-
<p><strong>Candidate ID:</strong> ${candidateId}</p>
85-
<p><strong>Identity ID:</strong> ${validationResult.identityId || 'N/A'}</p>
83+
<p><strong>Candidate:</strong> ${candidate}</p>
84+
<p><strong>Identity ID:</strong> ${validationResult.identityId || "N/A"}</p>
8685
<p><strong>Message:</strong> ${validationResult.message}</p>
8786
<p><strong>Authentication Valid:</strong> <span style="color: ${validationResult.valid ? "green" : "red"}; font-weight: bold;">${
88-
validationResult.valid ? "✓ VALID" : "✗ INVALID"
89-
}</span></p>
87+
validationResult.valid ? "✓ VALID" : "✗ INVALID"
88+
}</span></p>
9089
`;
9190
document.getElementById("verify-authentication-btn").addEventListener("click", verifyAuthentication);
9291
} catch (e) {

0 commit comments

Comments
 (0)