Skip to content

Commit a4f664d

Browse files
Updated diagram and workings to final approved version of handling auth (#5)
* diagram added * 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 * updated diagram * correctly sending identityId to backend * update diagram * updated diagram with alts to clarify conditionals and actions on errors
1 parent f730ccf commit a4f664d

File tree

4 files changed

+160
-92
lines changed

4 files changed

+160
-92
lines changed

README.md

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

4-
- **User hint input** for authentication (customer ID, email, or phone)
5+
- **User hint input** for authentication (customerId, email, or phone)
56
- **Face authentication** using Incode's renderAuthFace SDK
67
- **Session management** with IndexedDB to prevent reuse
78
- **Backend validation** to verify authentication integrity by:
8-
- Matching candidate ID from the SDK with identity ID from the score API
9+
- Matching candidate from the SDK with identityId from the score API
910
- Validating overall authentication status
1011
- Preventing token tampering and session replay attacks
1112
- Marking sessions as used to prevent reuse
@@ -21,47 +22,71 @@ sequenceDiagram
2122
participant IncodeAPI
2223
participant IndexedDB
2324
24-
Note over Frontend: Enter hint:<br> email/phone/identityId
25-
Frontend->>Backend: Start Session in Backend
25+
Note over Frontend: Enter hint:<br> identityId
26+
Note over Frontend: WebSDK: create()
27+
Frontend->>Backend: Start Session in Backend<br>{identityId}
2628
Backend->>IncodeAPI: Create new session<br>{configurationId, apikey}
2729
Note over IncodeAPI: /omni/start
2830
IncodeAPI-->>Backend: Returns Session<br>{token, interviewId}
29-
Backend->>IndexedDB: Store session<br>{key: interviewId, backToken: token, used: false)
31+
Backend->>IndexedDB: Store session<br>{key: interviewId, backToken: token, status: pending, identityId)
3032
Backend-->>Frontend: Return Session<br>{token, interviewId}
31-
32-
Note over Frontend: renderAuthFace(token, hint)
33+
34+
Note over Frontend: WebSDK: renderAuthFace(token, hint)
3335
Note over Frontend: User completes face authentication
34-
Note over Frontend:Returns:<br>{candidateId}
35-
36-
37-
Frontend->>Backend: Mark Session as Completed<br>{token}
38-
Note over IncodeAPI: /0/omni/finish-status
39-
Backend->>IncodeAPI: Get finish status
40-
IncodeAPI-->>Backend: Return:<br>{redirectionUrl, action}//Unused
41-
42-
Frontend->>Backend: Validate Authentication<br>{interviewId, token, candidateId}
36+
Note over Frontend:Returns:<br>{candidate}
37+
38+
Frontend->>Backend: Validate Authentication<br>{interviewId, token, candidate}
4339
Backend->>IndexedDB: Get Session Info:<br>{key:interviewId}
44-
IndexedDB-->>Backend: {backToken, used}
45-
Note over Backend: Validate interviewId exists in DB
46-
Note over Backend: Validate Session wasn't Used<br>used != True
47-
Note over Backend: Validate tokens match<br>token === backToken
40+
IndexedDB-->>Backend: {backToken, status}
41+
alt interviewId doesn't exist in DB
42+
Backend->>Frontend: {"interviewId doesn't exists", valid:false}
43+
end
44+
alt status != pending
45+
Backend->>Frontend: { "Session was already verified", valid:false}
46+
end
47+
alt candidate != session.identityId
48+
Backend->>IndexedDB: Mark session as Rejected<br>{interviewId, status:rejected}
49+
Backend->>Frontend: {"Stored identityId doesn't match candidate", valid:false}
50+
end
51+
alt token != backToken
52+
Backend->>IndexedDB: Mark session as Rejected<br>{interviewId, status:rejected}
53+
Backend->>Frontend: {"Stored token doesn't match token", valid:false}
54+
end
55+
56+
Backend->>IncodeAPI: Mark session as completed
57+
Note over IncodeAPI: /0/omni/finish-status
58+
IncodeAPI-->>Backend: Return:<br>{redirectionUrl, action}//Unused
59+
4860
Backend->>IncodeAPI: Get Authentication Score<br>{token:backToken}
4961
Note over IncodeAPI: /0/omni/get/score
5062
IncodeAPI-->>Backend: {status, identityId}
51-
Note over Backend: Validate candidateId matches identityId<br> candidateId === identityId
52-
Note over Backend: Validate Score is OK:<br>status === "OK"
53-
Backend->>IndexedDB: Mark session as used<br>{interviewId, used:true}
54-
Backend-->>Frontend: Return validation result<br>{message, valid, identityId}
63+
alt identityId != candidate
64+
Backend->>IndexedDB: Mark session as Rejected<br>{interviewId, status:rejected}
65+
Backend->>Frontend: {"candidate doesn't matches score identityId", valid:false}
66+
end
67+
alt score.status != "OK"
68+
Backend->>IndexedDB: Mark session as Rejected<br>{interviewId, status:rejected}
69+
Backend->>Frontend: {"Score for this session is not OK", valid:false}
70+
end
71+
72+
Note over Backend: Success
73+
Backend->>IndexedDB: Mark session as approved<br>{interviewId, status:approved}
74+
Backend-->>Frontend: Return validation result<br>{"Succesful validation", valid:true, identityId}
5575
Note over Frontend: Show validation results
5676
```
5777

5878
# Requirements
79+
5980
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.
6081

6182
# Install
83+
6284
Run `npm install`
85+
6386
# Config
87+
6488
Copy `.env.example` to `.env.local` and add your local values
89+
6590
```
6691
VITE_API_URL=https://demo-api.incodesmile.com/0
6792
VITE_SDK_URL=https://sdk.incode.com/sdk/onBoarding-1.80.1.js
@@ -71,11 +96,13 @@ VITE_FAKE_BACKEND_APIURL=https://demo-api.incodesmile.com
7196
VITE_FAKE_BACKEND_APIKEY=
7297
VITE_FAKE_BACKEND_FLOW_ID=
7398
```
99+
74100
Remember the Flow holds the backend counter part of the process, some configurations there might affect the behavior of the WebSDK here.
75101

76102
# Fake Backend Server
103+
77104
Starting and finishing the session must be done in the backend. To simplify development, this
78-
sample includes a `fake_backend.js` file that handles backend operations in the frontend.
105+
sample includes a `fake_backend.js` file that handles backend operations in the frontend.
79106

80107
**Important:** Replace this with a proper backend for production. The API key should NEVER be exposed in the frontend.
81108

@@ -87,26 +114,28 @@ sample includes a `fake_backend.js` file that handles backend operations in the
87114
- `fakeBackendValidateAuthentication()` - Validates the authentication by:
88115
- Checking if the session exists and hasn't been used
89116
- Verifying the token matches the stored token
90-
- Comparing candidate ID with identity ID from the score
117+
- Comparing candidate with identityId from the score
91118
- Ensuring overall status is "OK"
92119
- Marking the session as used to prevent reuse
93120

94121
# Run
122+
95123
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.
96124

97125
run `npm run dev`
98126

99127
A new server will be exposed, the data will be in the terminal
100128

101129
# Build
130+
102131
run `npm run build`
103132

104133
A new build will be created in `/dist` you can serve that build everywhere just remember to serve with https.
105134

106135
# Testing especific versions of the webSDK locally
136+
107137
You can save the specific version needed under `/public` and change the `VITE_SDK_URL` variable on `.env.local` to something like:
108138

109139
```
110140
VITE_SDK_URL=/name-of-the-js-file.js
111141
```
112-

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: "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>

0 commit comments

Comments
 (0)