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