/** * POST /v1/authenticate — Autentisering/bedrägeri * Kombinerar face verification + liveness + risk scoring */ import { Router } from 'express'; import * as ort from 'onnxruntime-node'; import sharp from 'sharp'; import { fetchImage, hashInput, saveResult, genReqId, requireAuth } from './utils.mjs'; const router = Router(); const FACE_DET_MODEL = '/opt/amos/data/kyc-service/models/face_detection_yunet_2023mar.onnx'; const LIVENESS_MODEL = '/opt/amos/data/kyc-service/models/2.7_80x80_MiniFASNetV2.onnx'; const FACE_REC_MODEL = '/opt/amos/data/kyc-service/models/face_recognition_sface_2021dec.onnx'; let detSession = null, liveSession = null, recSession = null; async function getDetSession() { if (!detSession) detSession = await ort.InferenceSession.create(FACE_DET_MODEL); return detSession; } async function getLiveSession() { if (!liveSession) liveSession = await ort.InferenceSession.create(LIVENESS_MODEL); return liveSession; } async function getRecSession() { if (!recSession) recSession = await ort.InferenceSession.create(FACE_REC_MODEL); return recSession; } async function detectFace(buffer) { const raw = await sharp(buffer).resize(640, 640).raw().toBuffer({ resolveWithObject: true }); const { data, info } = raw; const h = info.height, w = info.width; const floatData = new Float32Array(1 * 3 * h * w); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const idx = (y * w + x) * 3; floatData[0 * h * w + y * w + x] = data[idx] / 255.0; floatData[1 * h * w + y * w + x] = data[idx + 1] / 255.0; floatData[2 * h * w + y * w + x] = data[idx + 2] / 255.0; } } const tensor = new ort.Tensor('float32', floatData, [1, 3, h, w]); const sess = await getDetSession(); const feeds = {}; feeds[sess.inputNames[0]] = tensor; const out = await sess.run(feeds); const outTensor = out[sess.outputNames[0]]; const outData = outTensor.data; const dims = outTensor.dims; const stride = dims[dims.length - 1]; let bestScore = 0, bestBox = null; for (let i = 0; i < dims[0]; i++) { const row = Array.from(outData.slice(i * stride, (i + 1) * stride)); if (row[2] > bestScore) { bestScore = row[2]; bestBox = row; } } if (!bestBox || bestScore < 0.5) return null; return { box: bestBox, score: bestScore }; } async function checkLiveness(buffer, box) { const orig = await sharp(buffer).raw().toBuffer({ resolveWithObject: true }); const ow = orig.info.width, oh = orig.info.height; const x1 = Math.max(0, Math.round(box[3] * ow)); const y1 = Math.max(0, Math.round(box[4] * oh)); const x2 = Math.min(ow, Math.round(box[5] * ow)); const y2 = Math.min(oh, Math.round(box[6] * oh)); const faceBuf = await sharp(buffer) .extract({ left: x1, top: y1, width: x2 - x1, height: y2 - y1 }) .resize(80, 80) .raw() .toBuffer(); const liveFloat = new Float32Array(1 * 3 * 80 * 80); for (let i = 0; i < 80 * 80; i++) { liveFloat[0 * 6400 + i] = faceBuf[i * 3] / 255.0; liveFloat[1 * 6400 + i] = faceBuf[i * 3 + 1] / 255.0; liveFloat[2 * 6400 + i] = faceBuf[i * 3 + 2] / 255.0; } const liveTensor = new ort.Tensor('float32', liveFloat, [1, 3, 80, 80]); const liveSess = await getLiveSession(); const liveFeeds = {}; liveFeeds[liveSess.inputNames[0]] = liveTensor; const liveOut = await liveSess.run(liveFeeds); const liveData = liveOut[liveSess.outputNames[0]].data; const realScore = liveData[0]; const fakeScore = liveData[1]; const score = realScore / (realScore + fakeScore + 1e-6); return { score: parseFloat(score.toFixed(4)), label: score > 0.7 ? 'live' : score > 0.4 ? 'uncertain' : 'spoof' }; } async function getEmbedding(buffer, box) { const orig = await sharp(buffer).raw().toBuffer({ resolveWithObject: true }); const ow = orig.info.width, oh = orig.info.height; const x1 = Math.max(0, Math.round(box[3] * ow)); const y1 = Math.max(0, Math.round(box[4] * oh)); const x2 = Math.min(ow, Math.round(box[5] * ow)); const y2 = Math.min(oh, Math.round(box[6] * oh)); const faceBuf = await sharp(buffer) .extract({ left: x1, top: y1, width: x2 - x1, height: y2 - y1 }) .resize(112, 112) .raw() .toBuffer(); const floatData = new Float32Array(1 * 3 * 112 * 112); for (let i = 0; i < 112 * 112; i++) { floatData[0 * 12544 + i] = faceBuf[i * 3] / 255.0; floatData[1 * 12544 + i] = faceBuf[i * 3 + 1] / 255.0; floatData[2 * 12544 + i] = faceBuf[i * 3 + 2] / 255.0; } const tensor = new ort.Tensor('float32', floatData, [1, 3, 112, 112]); const sess = await getRecSession(); const feeds = {}; feeds[sess.inputNames[0]] = tensor; const out = await sess.run(feeds); return out[sess.outputNames[0]].data; } function cosineSimilarity(a, b) { let dot = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-6); } router.post('/', requireAuth, async (req, res) => { const requestId = genReqId(); const start = Date.now(); try { const { image_url, image_base64, reference_image_url, reference_image_base64, user_id } = req.body || {}; const img = await fetchImage({ image_url, image_base64 }); const inputHash = hashInput(img.buffer); // Step 1: Detect face const face = await detectFace(img.buffer); if (!face) { return res.status(400).json({ ok: false, error: 'No face detected', request_id: requestId }); } // Step 2: Liveness check const liveness = await checkLiveness(img.buffer, face.box); // Step 3: Compare with reference if provided let identityMatch = null; if (reference_image_url || reference_image_base64) { const refImg = await fetchImage({ image_url: reference_image_url, image_base64: reference_image_base64 }); const refFace = await detectFace(refImg.buffer); if (refFace) { const emb1 = await getEmbedding(img.buffer, face.box); const emb2 = await getEmbedding(refImg.buffer, refFace.box); const sim = cosineSimilarity(emb1, emb2); identityMatch = { similarity: parseFloat(sim.toFixed(4)), match: sim > 0.6 }; } } // Step 4: Risk scoring let riskScore = 0; if (liveness.label === 'spoof') riskScore += 0.8; else if (liveness.label === 'uncertain') riskScore += 0.4; if (identityMatch && !identityMatch.match) riskScore += 0.6; if (face.score < 0.7) riskScore += 0.2; riskScore = Math.min(1.0, riskScore); const confidence = face.score * (liveness.score || 0.5); const authenticated = liveness.label === 'live' && (!identityMatch || identityMatch.match); const result = { ok: true, endpoint: 'authenticate', request_id: requestId, user_id: user_id || null, authenticated, face: { detected: true, confidence: parseFloat(face.score.toFixed(4)) }, liveness, identity_match: identityMatch, risk_score: parseFloat(riskScore.toFixed(4)), risk_level: riskScore > 0.7 ? 'high' : riskScore > 0.3 ? 'medium' : 'low', inference_time_ms: Date.now() - start, }; await saveResult('authenticate', requestId, inputHash, result, confidence, { user_id, source: img.source }); res.json(result); } catch (e) { console.error('[authenticate]', e); res.status(500).json({ ok: false, error: e.message, request_id: requestId }); } }); export default router;