Skip to content

Commit 20e0b7e

Browse files
Add AES flow to sample (#3)
* * added eslint * added ability to save identityId * updated apiurl to /0 * added upload document capabiity and get download link at the end * moved frontend to it's own folder * added integrated backend into the project * general improved ui and messaging
1 parent 8699451 commit 20e0b7e

20 files changed

+5338
-1248
lines changed

.gitignore

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1 @@
1-
# Logs
2-
logs
3-
*.log
4-
npm-debug.log*
5-
yarn-debug.log*
6-
yarn-error.log*
7-
pnpm-debug.log*
8-
lerna-debug.log*
9-
10-
node_modules
11-
dist
12-
dist-ssr
13-
*.local
14-
15-
# Editor directories and files
16-
.vscode/*
17-
!.vscode/extensions.json
18-
.idea
191
.DS_Store
20-
*.suo
21-
*.ntvs*
22-
*.njsproj
23-
*.sln
24-
*.sw?

backend/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.env
2+
node_modules

backend/README.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Backend Server using NodeJS
2+
3+
Backend Server that implements all the endpoints needed to run our Frontend Samples
4+
5+
## Endpoints
6+
7+
- GET `/start`: Call Incode's `/omni/start` API to create an Incode session which will include a `token` in the JSON response. This token can be shared with Incode SDK client apps to do token based initialization, which is a best practice.
8+
9+
It also performs basic storage of sessions in the `sessions` directory to help implement `renderRedirectToMobile`in frontend.
10+
11+
At session generation it will generate an `uniqueId` and save the session in `session/<uniqueId>.json`, later if you call `/start` again passing a valid `uniqueId` it will retrieve the stored session instead of creating a new one.
12+
13+
- GET `/onboarding-url`: Calls incodes `/omni/start` and then with the token calls `/0/omni/onboarding-url` to retrieve the unique onboarding-url for the newly created session.
14+
15+
- GET `/onboarding-status`: Calls incodes `/omni/get/onboarding/status` API and return the onboarding status.
16+
17+
Expects `interviewId` as query param.
18+
19+
- GET `/fetch-score`: Calls incodes `/omni/get/score` API and return the score.
20+
21+
Expects `interviewId` as query param.
22+
23+
- POST `/auth`: Receives the information about a faceMatch attempt and verifies if it was correct and has not been tampered.
24+
25+
- POST `/webhook`: Example webhook that reads the json data and return it back a response, from here you could fetch scores or OCR data when the status is ONBOARDING_FINISHED
26+
27+
- POST `/approve`: Example webhook that reads the json data and if the status is ONBOARDING_FINISHED goes ahead and creates the identity using the `/omni/process/approve` endpoint.
28+
29+
- POST `/finish`: Finishes the session, receives the token as a body parameter
30+
31+
## Secure Credential Handling
32+
We highly recommend to follow the 0 rule for your implementations, where all sensitive calls to incode's endpoints are done in the backend, keeping your apikey protected and just returning a `token` with the user session to the frontend.
33+
34+
Within this sample you will find the only calls to a `/omni/` endpoints we recommend for you to have, it requires the usage of the `apikey`, all further calls must be done using only the generated `token` and be addresed to the `/0/omni` endpoints.
35+
36+
## Prerequisites
37+
This sample uses the global fetch API so you must use [Node 18](https://nodejs.org/en) or higher.
38+
39+
## Local Development
40+
41+
### Environment
42+
Rename `sample.env` file to `.env` adding your subscription information:
43+
44+
```env
45+
API_URL=https://demo-api.incodesmile.com
46+
API_KEY=you-api-key
47+
FLOW_ID=Flow or Workflow Id from your Incode dashboard.
48+
ADMIN_TOKEN=Needed for the webhooks to be able to fetch Scores and auto-approve
49+
```
50+
51+
### Using NPM
52+
Install the depencies with `npm install`
53+
```bash
54+
npm install
55+
```
56+
57+
Then start the local server with the nodemon script, it will keep an eye on file changes and restart the local server if needed.
58+
```bash
59+
npm run nodemon
60+
```
61+
62+
The server will accept petitions on `http://localhost:3000/`
63+
64+
### Using Docker
65+
66+
```bash
67+
docker-compose build
68+
docker-compose --env-file ./.env up
69+
```
70+
71+
The server will accept petitions on `http://localhost:3000/`
72+
73+
### Frontend development
74+
75+
For development most of our frontend samples have a reverse proxy configured to serve `http://localhost:3000/` on `https://<your-ip>:5731/api`
76+
77+
That way you avoid all problems related to CORS.
78+
79+
### Webhook development
80+
81+
For our systems to reach your server, you will need to expose the server to the internet with ngrok
82+
83+
For your frontend to properly work in tandem with this server on your mobile phone for testing, you will need a public url with proper SSL configured, by far the easiest way to acchieve this with an ngrok account properly configured on your computer. You can visit `https://ngrok.com` to make a free account and do a quick setup.
84+
85+
Then simply run the nodemon script, it will start the server in port 3000 and restart whenever a file is changed, leave it running.
86+
87+
```bash
88+
npm run nodemon
89+
```
90+
91+
In another shell expose the server to internet through your computer ngrok account:
92+
93+
```bash
94+
ngrok http 3000
95+
```
96+
97+
Open the `Forwarding` adress in a web browser. The URL should look similar to this: `https://466c-47-152-68-211.ngrok-free.app`.
98+
99+
Now you should be able to visit the following routes to receive the associated payloads:
100+
1. `https://yourforwardingurl.app/start`
101+
2. `https://yourforwardingurl.app/start?uniqueId=0e810732-6e7e-4512-aaa5-1ae2e1f8df46`
102+
3. `https://yourforwardingurl.app/onboarding-url`
103+
4. `https://yourforwardingurl.app/onboarding-url?redirectionUrl=https%3A%2F%2Fexample.com%2F`
104+
105+
## Post Endpoints
106+
107+
### Auth
108+
Receives the information about a faceMatch attempt and verifies if it was correct and has not been tampered.
109+
110+
All the parameters needed come as the result of execution of the [Render Login](https://docs.incode.com/docs/web/integration-guide/sdk-methods#renderlogin) component,
111+
you can see a full example of it's usage in [Face Login Sample](https://github.com/Incode-Technologies-Example-Repos/javascript-samples/tree/main/face-login)
112+
113+
```bash
114+
curl --location 'https://yourforwardingurl.app/auth' \
115+
--header 'Content-Type: application/json' \
116+
--data '{
117+
"transactionId": "Transaction Id obtained at face login",
118+
"token": "Token obtained at face login ",
119+
"interviewToken": "Interview token obtained at face login",
120+
}'
121+
```
122+
123+
### Finish
124+
Finishes a session, is the matching endpoint of `/start`
125+
126+
```bash
127+
curl --location 'https://yourforwardingurl.app/finish' \
128+
--header 'Content-Type: application/json' \
129+
--data '{
130+
131+
"token": "Token obtained at the /start endpoint ",
132+
133+
}'
134+
```
135+
136+
## Webhooks
137+
138+
### Simplified Webhook
139+
`https://yourforwardingurl.app/webhook`
140+
We provide an example on how to read the data we send in the webhook calls, from here you could
141+
fetch scores and OCR data, what you do with that is up to you.
142+
143+
### Auto approve on PASS
144+
`https://yourforwardingurl.app/approve`
145+
We provide a more complex example where we fetch the scores and if the status is `OK` we then
146+
approve the user to create his identity for face-login
147+
148+
### Admin Token
149+
For the approval and fetching of scores to work you will need an Admin Token, Admin tokens
150+
require an executive user-password and have a 24 hour expiration, thus need a
151+
more involved strategy to be generated, renewed, securely saved and shared to the app.
152+
153+
For this simple test just use the following cURl, and add the generated token to the `.env` file,
154+
you will need to refresh it after 24 hours.
155+
156+
```bash
157+
curl --location 'https://demo-api.incodesmile.com/executive/log-in' \
158+
--header 'Content-Type: application/json' \
159+
--header 'api-version: 1.0' \
160+
--header 'x-api-key: <your-apikey>' \
161+
--data '{
162+
"email": "••••••",
163+
"password": "••••••"
164+
}'
165+
```
166+
167+
### How to test your code
168+
To recreate the call and the format of the data sent by Incode you can use the following script:
169+
170+
```bash
171+
curl --location 'https://yourforwardingurl.app/webhook' \
172+
--header 'Content-Type: application/json' \
173+
--data '{
174+
"interviewId": "<interviewId>",
175+
"onboardingStatus": "ONBOARDING_FINISHED",
176+
"clientId": "<clientId>",
177+
"flowId": "<flowId>"
178+
}'
179+
```
180+
181+
## Dependencies
182+
183+
* **nodejs18+**: JavaScript runtime built on Chrome's V8 JavaScript engine.
184+
* **express**: Web server framework.
185+
* **dotenv**: Used to access environment variables.
186+
* **ngrok**: Unified ingress platform used to expose your local server to the internet.

backend/index.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
const express = require('express');
2+
const dotenv = require('dotenv');
3+
const cors = require('cors');
4+
const fs = require('node:fs');
5+
6+
const app = express();
7+
dotenv.config();
8+
9+
app.use(cors())
10+
// Middleware to handle raw body data
11+
app.use(express.raw({ type: '*/*' }));
12+
13+
const defaultHeader = {
14+
'Content-Type': "application/json",
15+
'x-api-key': process.env.API_KEY,
16+
'api-version': '1.0'
17+
};
18+
19+
// Admin Token + ApiKey are needed for approving
20+
const adminHeader = {
21+
'Content-Type': "application/json",
22+
'x-api-key': process.env.API_KEY,
23+
'X-Incode-Hardware-Id': process.env.ADMIN_TOKEN,
24+
'api-version': '1.0'
25+
};
26+
27+
// Receives the information about a faceMatch attempt and verifies
28+
// if it was correct and has not been tampered.
29+
app.post('/verify', async (req, res) => {
30+
/** Get parameters from body */
31+
const faceMatchData = JSON.parse(req.body.toString());
32+
const {transactionId, token, interviewToken} = faceMatchData;
33+
const verificationParams = { transactionId, token, interviewToken };
34+
35+
let response={};
36+
try{
37+
/** Run Call against incode API **/
38+
const verifyAttemptUrl = `${process.env.API_URL}/omni/authentication/verify`;
39+
response = await doPost(verifyAttemptUrl, verificationParams, adminHeader);
40+
} catch(e) {
41+
console.log(e.message);
42+
res.status(500).send({success:false, error: e.message});
43+
return;
44+
}
45+
log = {
46+
timestamp: new Date().toISOString().slice(0, 19).replace('T', ' '),
47+
data: {verificationParams, response}
48+
}
49+
res.status(200).send(response);
50+
51+
// Write to a log so you can debug it.
52+
console.log(log);
53+
});
54+
55+
// if it was correct and has not been tampered.
56+
app.post('/sign', async (req, res) => {
57+
/** Receive contract and token as parameters */
58+
const signParamsData = JSON.parse(req.body.toString());
59+
const { interviewToken, base64Contract } = signParamsData;
60+
61+
/** Prepare the authorization header that will be used in calls to incode */
62+
sessionHeader = {...defaultHeader};
63+
sessionHeader['X-Incode-Hardware-Id'] = interviewToken;
64+
65+
let response = {};
66+
try{
67+
/** Get URL where to upload the contract */
68+
const generateDocumentUploadUrl = `${process.env.API_URL}/omni/es/generateDocumentUploadUrl`;
69+
const documentURLData = await doPost(generateDocumentUploadUrl, { token:interviewToken }, sessionHeader);
70+
const {referenceId, preSignedUrl} = documentURLData
71+
72+
/** Upload contract to AWS presigned url */
73+
const binary = Buffer.from(base64Contract, 'base64');
74+
const uploadResponse = await fetch(preSignedUrl, {
75+
method: "PUT",
76+
headers: {"Content-Type": "application/pdf"},
77+
body: binary,
78+
})
79+
if (!uploadResponse.ok) {
80+
throw new Error('Uploading contract failed with code ' + uploadResponse.status)
81+
}
82+
83+
/** Sign the document */
84+
const signURL = `${process.env.API_URL}/omni/es/process/sign`;
85+
const signData = await doPost(signURL,
86+
{
87+
"documentRef": referenceId,
88+
"userConsented": true
89+
},
90+
sessionHeader
91+
);
92+
const {success} = signData
93+
if (!success) { throw new Error('Sign failed');}
94+
95+
/** Fetch all signed document references */
96+
const documentsSignedURL = `${process.env.API_URL}/omni/es/documents/signed`;
97+
const documentsSignedData = await doGet(documentsSignedURL, {}, sessionHeader);
98+
const {documents} = documentsSignedData
99+
100+
/** This endpoint returns all documents, find the one just signed matching by referenceId*/
101+
const justSigned = documents.find(document => document.documentRef=== referenceId)
102+
103+
/** Return referenceId and documentUrl */
104+
const {documentRef, documentUrl} = justSigned
105+
response = {referenceId, documentUrl}
106+
107+
} catch(e) {
108+
console.log(e.message);
109+
res.status(500).send({success:false, error: e.message});
110+
return;
111+
}
112+
log = {
113+
timestamp: new Date().toISOString().slice(0, 19).replace('T', ' '),
114+
data: {signParamsData, response}
115+
}
116+
res.status(200).send(response);
117+
// Write to a log so you can debug it.
118+
console.log(log);
119+
});
120+
121+
122+
app.get('*', function(req, res){
123+
res.status(404).json({error: `Cannot GET ${req.url}`});
124+
});
125+
126+
app.post('*', function(req, res){
127+
res.status(404).json({error: `Cannot POST ${req.url}`});
128+
});
129+
130+
// Utility functions
131+
const doPost = async (url, bodyparams, headers) => {
132+
try {
133+
const response = await fetch(url, { method: 'POST', body: JSON.stringify(bodyparams), headers});
134+
if (!response.ok) {
135+
//console.log(response.json());
136+
throw new Error('Request failed with code ' + response.status)
137+
}
138+
return response.json();
139+
} catch(e) {
140+
console.log({url, bodyparams, headers})
141+
throw new Error('HTTP Post Error: ' + e.message)
142+
}
143+
}
144+
145+
const doGet = async (url, params, headers) => {
146+
try {
147+
const response = await fetch(`${url}?` + new URLSearchParams(params), {method: 'GET', headers});
148+
if (!response.ok) {
149+
//console.log(await response.json());
150+
throw new Error('Request failed with code ' + response.status)
151+
}
152+
return response.json();
153+
} catch(e) {
154+
console.log({url, params, headers})
155+
throw new Error('HTTP Get Error: ' + e.message)
156+
}
157+
}
158+
159+
160+
// Listen for HTTP
161+
const httpPort = 3000;
162+
163+
app.listen(httpPort, () => {
164+
console.log(`HTTP listening on: http://localhost:${httpPort}/`);
165+
});
166+
167+
module.exports = app;

0 commit comments

Comments
 (0)