diff --git a/src/config.ts b/src/config.ts index f9f5af1..89e76bc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -187,7 +187,7 @@ export default { shuftipro: { clientId: KYC_SHUFTIPRO_CLIENT_ID, secretKey: KYC_SHUFTIPRO_SECRET_KEY, - baseUrl: 'https://api.shuftipro.com', + baseUrl: 'https://shuftipro.com', callbackUrl: KYC_SHUFTIPRO_CALLBACK_URL, redirectUrl: KYC_SHUFTIPRO_REDIRECT_URL, allowRecreateSession: (KYC_SHUFTIPRO_ALLOW_RECREATE_SESSION === 'true') || false diff --git a/src/entities/shuftipro.kyc.result.ts b/src/entities/shuftipro.kyc.result.ts index ba3abea..54b0a9d 100644 --- a/src/entities/shuftipro.kyc.result.ts +++ b/src/entities/shuftipro.kyc.result.ts @@ -6,16 +6,25 @@ export class ShuftiproKycResult { id: ObjectID; @Column() - statusCode?: string; + event?: string; @Column() - message: string; + reference?: string; @Column() - reference?: string; + token?: string; + + @Column() + verificationUrl?: string; + + @Column() + verificationResult?: string | null; + + @Column() + verificationData?: any; @Column() - signature?: string; + declinedReason?: any; @Column() error?: boolean; @@ -28,11 +37,14 @@ export class ShuftiproKycResult { static createShuftiproKycResult(data: ShuftiproInitResult): ShuftiproKycResult { const kycResult = new ShuftiproKycResult(); - kycResult.statusCode = data.status_code; - kycResult.message = data.message; + kycResult.event = data.event; + kycResult.token = data.token; kycResult.reference = data.reference; - kycResult.signature = data.signature; + kycResult.verificationData = data.verification_data; + kycResult.verificationResult = data.verification_result; + kycResult.verificationUrl = data.verification_url; kycResult.timestamp = data.timestamp; + kycResult.declinedReason = data.declined_reason; if (data.error) { kycResult.error = data.error; diff --git a/src/index.d.ts b/src/index.d.ts index 86d017f..f6c7953 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -257,11 +257,53 @@ declare interface JumioInitResult extends KycInitResult { } declare interface ShuftiproInitResult extends KycInitResult { - status_code?: string; - message: string; + event?: string; reference?: string; - signature?: string; error?: boolean; + token?: string; + verification_url?: string; + verification_result?: any; + verification_data?: any; + declined_reason?: any; +} + +declare interface ShuftiProName { + first_name: string; + last_name: string; +} + +declare interface ShuftiProDocument { + supported_types: string[]; + name: ShuftiProName; + dob: string; +} + +declare interface ShuftiProBackgroundChecks { + name: ShuftiProName; + dob: string; +} + +declare interface ShuftiProVerificationData { + reference: string; + country: string; + callback_url: string; + document: ShuftiProDocument; + background_checks: ShuftiProBackgroundChecks; +} + +declare interface ShuftiProVerificationResponse { + reference: string; + event: string; + error: any; + token: string; + verification_url: string; + verification_result: number | null; + verification_data: any; +} + +declare interface ShuftiProStatusResponse { + reference: string; + event: string; } declare interface KycScanStatus { diff --git a/src/ioc.container.ts b/src/ioc.container.ts index cecbf97..df1bcda 100644 --- a/src/ioc.container.ts +++ b/src/ioc.container.ts @@ -25,6 +25,7 @@ import { GatewayController } from './controllers/gateway.controller'; import { EmailTemplateService, EmailTemplateServiceType } from './services/email.template.service'; import { JumioProvider } from './providers/kyc/jumio.provider'; import { ShuftiproProvider } from './providers/kyc/shuftipro.provider'; +import { ShuftiProClientInterface, ShuftiProClientType, ShuftiProClient } from './services/shuftipro.client'; let container = new Container(); @@ -60,6 +61,10 @@ container.bind(CoinpaymentsClientType).to(Coinpayme container.bind(PaymentsServiceType).to(PaymentsService).inSingletonScope(); container.bind(IPNServiceType).to(IPNService).inSingletonScope(); container.bind(EmailTemplateServiceType).to(EmailTemplateService).inSingletonScope(); +container.bind(ShuftiProClientType).toConstantValue(new ShuftiProClient( + config.kyc.shuftipro.clientId, + config.kyc.shuftipro.secretKey +)); const auth = new Auth(container.get(AuthClientType)); // middlewares diff --git a/src/providers/kyc/shuftipro.provider.ts b/src/providers/kyc/shuftipro.provider.ts index 0d1e969..3c7fa9b 100644 --- a/src/providers/kyc/shuftipro.provider.ts +++ b/src/providers/kyc/shuftipro.provider.ts @@ -6,29 +6,34 @@ import * as crypto from 'crypto'; import * as qs from 'querystring'; import * as uuid from 'node-uuid'; import { AuthorizedRequest } from '../../requests/authorized.request'; -import { KYC_STATUS_VERIFIED, KYC_STATUS_FAILED, KYC_STATUS_PENDING, Investor, KYC_STATUS_NOT_VERIFIED } from '../../entities/investor'; +import { KYC_STATUS_VERIFIED, KYC_STATUS_FAILED, KYC_STATUS_PENDING, Investor } from '../../entities/investor'; import { KycAlreadyVerifiedError, KycFailedError, KycPendingError, KycShuftiProInvalidSignature } from '../../exceptions/exceptions'; import { getConnection } from 'typeorm'; import { ShuftiproKycResult } from '../../entities/shuftipro.kyc.result'; import { Web3ClientInterface, Web3ClientType } from '../../services/web3.client'; +import { ShuftiProClientType, ShuftiProClientInterface } from '../../services/shuftipro.client'; const mongo = require('mongodb'); +const LOCAL_INIT = 'local.init'; +const REQUEST_PENDING = 'request.pending'; +const REQUEST_INVALID = 'request.invalid'; +const REQUEST_CANCELLED = 'request.cancelled'; +const REQUEST_TIMEOUT = 'request.timeout'; +const REQUEST_UNAUTORIZED = 'request.unautorized'; +const VERIFICATION_ACCEPTED = 'verification.accepted'; +const VERIFICATION_DECLINED = 'verification.declined'; + @injectable() export class ShuftiproProvider implements KycProviderInterface { private logger = Logger.getInstance('SHUFTIPRO_KYC_CLIENT'); - clientId: string; - secretKey: string; - baseUrl: string; kycEnabled: boolean; constructor( - @inject(Web3ClientType) private web3Client: Web3ClientInterface + @inject(Web3ClientType) private web3Client: Web3ClientInterface, + @inject(ShuftiProClientType) private shuftiClient: ShuftiProClientInterface ) { - this.clientId = config.kyc.shuftipro.clientId; - this.secretKey = config.kyc.shuftipro.secretKey; - this.baseUrl = config.kyc.shuftipro.baseUrl; this.kycEnabled = config.kyc.enabled; request.defaults({ @@ -36,69 +41,48 @@ export class ShuftiproProvider implements KycProviderInterface { }); } - async init(investor: Investor): Promise { - const logger = this.logger.sub({ email: investor.email }).addPrefix('[init] '); + async init(investor: Investor): Promise { + const logger = this.logger.sub({ email: investor.email }).addPrefix('[init]'); - try { - logger.debug('Prepare investor for identification'); - - if (this.kycEnabled) { - const postData = this.preparePostData(investor); - - await this.localInitKycProcess(investor, postData.reference); - - const options = { - 'method': 'POST', - 'headers': { - 'content-type': 'application/x-www-form-urlencoded' - }, - 'path': '/', - 'body': qs.stringify(postData) - }; - - const kycInitResponse = await request.json(this.baseUrl, options); - - if (!kycInitResponse.error) { - const signature = this.signature(kycInitResponse.status_code + kycInitResponse.message + kycInitResponse.reference); - if (signature === kycInitResponse.signature) { - await this.saveKycInitResult(investor, kycInitResponse); - this.logger.info('Successful init'); - return { ...kycInitResponse, timestamp: (new Date()).toISOString() } as ShuftiproInitResult; - } - - this.logger.exception('Invalid signature'); - throw new KycShuftiProInvalidSignature('Invalid signature'); - } + if (this.kycEnabled) { + const verificationData = this.preparePostData(investor); + await this.localInitKycProcess(investor, verificationData.reference); - this.logger.exception(kycInitResponse.message); - throw new Error(kycInitResponse.message); - } else { - return { - message: 'KYC disabled', - status_code: 'SP1' - } as ShuftiproInitResult; - } - } catch (error) { - logger.exception({ error }); + const verificationResponse = await this.shuftiClient.init(verificationData); - throw error; + if (!verificationResponse.error) { + await this.saveKycInitResult(investor, verificationResponse); + this.logger.info('Successful init'); + return verificationResponse; + } + logger.exception(verificationResponse.error); + throw new Error(verificationResponse.error); } + + return { + event: VERIFICATION_ACCEPTED, + timestamp: (new Date()).toISOString() + } as ShuftiproInitResult; } async getInitStatus(req: AuthorizedRequest, res: any, next: any): Promise { res.json(await this.processKycStatus(req.user)); } - successUpload(req: any, res: any, next: any) { + public successUpload(req: any, res: any, next: any) { throw new Error('Method not supported.'); } async callback(req: any, res: any, next: any): Promise { - await this.processCallback(req.body); + await this.processCallback(req.body, req.headers['sp_signature']); res.status(200).send(); } - async processCallback(kycResultRequest: ShuftiproInitResult): Promise { + async processCallback(kycResultRequest: ShuftiproInitResult, spSignature: string): Promise { + const logger = this.logger.sub({ reference: kycResultRequest.reference }).addPrefix('[callback]'); + + logger.debug('process callback request'); + const kycResultRepo = getConnection().getMongoRepository(ShuftiproKycResult); const investorRepo = getConnection().getMongoRepository(Investor); const storedKycResult = await kycResultRepo.findOne({ where: {'reference': kycResultRequest.reference} }); @@ -115,15 +99,18 @@ export class ShuftiproProvider implements KycProviderInterface { return; } - const signature = this.signature(kycResult.statusCode + kycResult.message + kycResult.reference); - if (signature === kycResult.signature) { - switch (kycResult.statusCode) { - case 'SP1': + const signature = this.spSignature(JSON.stringify(kycResultRequest)); + if (signature === spSignature) { + switch (kycResult.event) { + case VERIFICATION_ACCEPTED: investor.kycStatus = KYC_STATUS_VERIFIED; investor.kycInitResult = kycResult; + + logger.debug('add investor into whitelist', investor.email); await this.web3Client.addAddressToWhiteList(investor.ethWallet.address); break; - case 'SP0': + case VERIFICATION_DECLINED: + logger.debug('verification declined', investor.email, kycResult); investor.kycStatus = KYC_STATUS_FAILED; investor.kycInitResult = kycResult; break; @@ -149,7 +136,7 @@ export class ShuftiproProvider implements KycProviderInterface { throw new KycPendingError('Your account verification is pending. Please wait for status update'); } - if ((user.kycInitResult as ShuftiproKycResult).statusCode === 'SP1') { + if ((user.kycInitResult as ShuftiproKycResult).event === VERIFICATION_ACCEPTED) { return user.kycInitResult as ShuftiproInitResult; } @@ -164,64 +151,30 @@ export class ShuftiproProvider implements KycProviderInterface { try { const currentStatus = await this.getKycStatus(user); if (currentStatus.error - || (currentStatus.status_code !== 'SP2' - && currentStatus.status_code !== 'SP1' - && currentStatus.status_code !== 'SP26' + || (currentStatus.event !== REQUEST_PENDING + && currentStatus.event !== VERIFICATION_ACCEPTED )) { return await this.createNewKycProcess(user); } return user.kycInitResult as ShuftiproInitResult; } catch (error) { - if (error.constructor === KycShuftiProInvalidSignature) { - return await this.createNewKycProcess(user); - } - throw error; + return await this.createNewKycProcess(user); } } - private async getKycStatus(user: Investor): Promise { - const kycInitResult = user.kycInitResult as ShuftiproInitResult; - const postData = { - client_id: this.clientId, - reference: kycInitResult.reference, - signature: this.signature(this.clientId + kycInitResult.reference) - }; - - const options = { - 'method': 'POST', - 'headers': { - 'content-type': 'application/x-www-form-urlencoded' - }, - 'body': qs.stringify(postData) - }; - - const response = await request.post(this.baseUrl + '/status', options); - if (response.content.length > 0) { - const result = JSON.parse(response.content); - if (!result.error) { - const signature = this.signature(result.status_code + result.message + result.reference); - if (signature === result.signature) { - return { ...result, timestamp: (new Date()).toISOString() } as ShuftiproInitResult; - } - throw new KycShuftiProInvalidSignature('Invalid signature'); - } - } - - return { - error: true, - message: 'Empty response' - } as ShuftiproInitResult; - } + private async getKycStatus(investor: Investor): Promise { + this.logger.debug('get kyc status'); + const kycInitResult = investor.kycInitResult as ShuftiproInitResult; - private signature(data: string): string { - return crypto.createHash('sha256').update(data + this.secretKey, 'utf8').digest('hex'); + const statusResponse = await this.shuftiClient.status(kycInitResult.reference); + return { ...statusResponse, timestamp: (new Date()).toISOString() } as ShuftiproInitResult; } private async localInitKycProcess(user: Investor, reference: string): Promise { const shuftiproKycResultRepo = getConnection().getMongoRepository(ShuftiproKycResult); const localInitKyc = new ShuftiproKycResult(); - localInitKyc.message = 'Local init'; + localInitKyc.event = LOCAL_INIT; localInitKyc.reference = reference; localInitKyc.timestamp = (new Date()).toISOString(); localInitKyc.user = user.id; @@ -229,39 +182,43 @@ export class ShuftiproProvider implements KycProviderInterface { await shuftiproKycResultRepo.save(localInitKyc); } - private async saveKycInitResult(user: Investor, kycInitResponse: ShuftiproInitResult): Promise { + private async saveKycInitResult(user: Investor, kycInitResponse: ShuftiProVerificationResponse): Promise { const shuftiproKycResultRepo = getConnection().getMongoRepository(ShuftiproKycResult); - const kycInitResult = ShuftiproKycResult.createShuftiproKycResult({ ...kycInitResponse, timestamp: (new Date()).toISOString() }); + const kycInitResult = ShuftiproKycResult.createShuftiproKycResult({ + error: kycInitResponse.error, + event: kycInitResponse.event, + reference: kycInitResponse.reference, + verification_url: kycInitResponse.verification_url, + verification_result: kycInitResponse.verification_result, + timestamp: (new Date()).toISOString() + }); await shuftiproKycResultRepo.save(shuftiproKycResultRepo.create({ ...kycInitResult, user: user.id })); } - private preparePostData(user: Investor): any { - const verificationServices = { - first_name: user.firstName, - last_name: user.lastName, - dob: user.dob, - background_check: '0' - }; - const postData = { - client_id: this.clientId, + private preparePostData(user: Investor): ShuftiProVerificationData { + const verificationData = { + callback_url: config.kyc.shuftipro.callbackUrl, reference: uuid.v4(), - email: user.email, - phone_number: user.phone, country: user.country, - lang: 'en', - callback_url: config.kyc.shuftipro.callbackUrl, - redirect_url: config.kyc.shuftipro.redirectUrl, - verification_services: JSON.stringify(verificationServices) - }; - - let rawData: string = ''; - Object.keys(postData).sort().forEach(function(value) { - rawData += postData[value]; - }); - postData['signature'] = this.signature(rawData); + document: { + supported_types: ['passport', 'id_card', 'driving_license'], + name: { + first_name: user.firstName, + last_name: user.lastName + }, + dob: user.dob + }, + background_checks: { + name: { + first_name: user.firstName, + last_name: user.lastName + }, + dob: user.dob + } + } as ShuftiProVerificationData; - return postData; + return verificationData; } private async createNewKycProcess(user: Investor): Promise { @@ -271,4 +228,8 @@ export class ShuftiproProvider implements KycProviderInterface { await investorRepo.save(user); return kycInitResult; } + + private spSignature(data: any): string { + return crypto.createHash('sha256').update(data + config.kyc.shuftipro.secretKey).digest('hex'); + } } diff --git a/src/services/shuftipro.client.ts b/src/services/shuftipro.client.ts new file mode 100644 index 0000000..3d71d21 --- /dev/null +++ b/src/services/shuftipro.client.ts @@ -0,0 +1,83 @@ +import * as request from 'web-request'; +import * as crypto from 'crypto'; +import { injectable } from 'inversify'; + +export class InvalidSignature extends Error {} +export class InvalidRequest extends Error {} + +export interface ShuftiProClientInterface { + init(verificationData: ShuftiProVerificationData): Promise; + status(reference: string): Promise; +} + +@injectable() +export class ShuftiProClient implements ShuftiProClientInterface { + private baseUrlApi: string; + private rootPath = '/'; + private statusPath = '/status'; + private clientId: string; + private secretKey: string; + + constructor(clientId: string, secretKey: string) { + this.baseUrlApi = 'https://shuftipro.com/api'; + this.clientId = clientId; + this.secretKey = secretKey; + + request.defaults({ throwResponseError: false }); + } + + public async init(verificationData: ShuftiProVerificationData): Promise { + const options = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': this.basicAuth() + } + }; + + try { + const response = await request.post(this.baseUrlApi + this.rootPath, options, JSON.stringify(verificationData)); + + if (response.headers.sp_signature === this.spSignature(response.content)) { + return JSON.parse(response.content) as ShuftiProVerificationResponse; + } + throw new InvalidSignature('Invalid signature'); + } catch (error) { + throw new Error(error); + } + } + + public async status(reference: string): Promise { + const options = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': this.basicAuth() + } + }; + + try { + const response = await request.post(this.baseUrlApi + this.statusPath, options, JSON.stringify({ reference })); + + if (response.statusCode === 404) { + throw new InvalidRequest('Invalid Request'); + } + + if (response.headers.sp_signature === this.spSignature(response.content)) { + return JSON.parse(response.content) as ShuftiProStatusResponse; + } + throw new InvalidSignature('Invalid signature'); + } catch (error) { + throw new Error(error); + } + } + + private basicAuth(): string { + return 'Basic ' + Buffer.from(`${this.clientId}:${this.secretKey}`).toString('base64'); + } + + private spSignature(data: any): string { + return crypto.createHash('sha256').update(data + this.secretKey).digest('hex'); + } +} + +const ShuftiProClientType = Symbol('ShuftiProClientInterface'); +export { ShuftiProClientType };