Aller au contenu

Mobile SDK — intégration tenant SaaS / on-prem / PWA

Module : mobile-sdk (iOS, Android, React Native, Flutter) + web-sdk (PWA branded). Voir ADR-024 — Stratégie mobile SDK-first + tenant resolution.

Ce document spécifie comment intégrer VitaKYC dans une application mobile, comment le SDK découvre son tenant, et comment les flux mobiles sont traités selon le mode de déploiement (SaaS partagé, SaaS dédié, on-premise, hybride multi-pays, fallback web PWA).

Il sert de cahier de charges pour l’équipe SDK VitaKYC et de guide d’intégration pour les CTO des banques clientes.


1. Trois surfaces mobiles — pas d’app VitaKYC grand public

Section intitulée « 1. Trois surfaces mobiles — pas d’app VitaKYC grand public »
#SurfacePour quiPublisher storeDistribution
1SDK natif (iOS, Android, RN, Flutter)Client final KYCLa banqueMaven Central, CocoaPods, Swift PM, NPM, Gradle, Pub
2Web SDK / PWA brandedClient final KYC sans app banque (ou iOS sans NFC)— (URL SMS/QR)CDN sur kyc.<banque>.tn
3VitaKYC Agent MobileAgents, superviseurs, compliance en mobilitéVitaKYCApple App Store + Google Play + MDM enterprise (Jamf, Intune)

Le client final d’une banque ne télécharge JAMAIS une app “VitaKYC” — il fait confiance à sa banque. Règle d’industrie (Onfido, Jumio, Sumsub, Veriff, IDnow). Rationale complet dans l’ADR-024.


2. Matrice des déploiements × résolution tenant

Section intitulée « 2. Matrice des déploiements × résolution tenant »
DéploiementSurface mobileOù est le tenant_id ?Où pointe apiBase ?Cert pinning
SaaS partagéSDK natif embarquéBaked au build (BuildConfig)https://api-<region>.vitakyc.comCert VitaKYC
SaaS dédiéSDK natif embarquéBaked au buildhttps://<banque>.kyc.vitakyc.comCert VitaKYC
On-premiseSDK natif embarquéBaked au build (utile pour logs)https://mobile.<banque>.tn/kyc (gateway banque)Cert banque
Hybride multi-paysSDK natif embarquéRésolu runtime via /me/region (backend banque)Résolu runtimeRésolu runtime (pinset multi-cert)
Fallback PWAWeb SDK via SMS/QRDans le JWT + domainehttps://kyc.<banque>.tn (CNAME)HSTS + preload
Agent MobileApp VitaKYC standaloneRésolu au login du compte agentRégion de l’agentCert VitaKYC + tenant-specific overlay


Implications on-prem :

  • apiBase = https://mobile.<banque>.tn/kyc — domaine banque, pas vitakyc.com
  • Cert pinning = SHA256 du cert banque (baked au build de l’app banque)
  • Aucune règle firewall nouvelle (port 443 mobile banking déjà ouvert)
  • VitaKYC on-prem = mono-tenant, header X-Tenant-Id validé statique (mismatch → 403)
  • Pas de dépendance externe → conforme BCT “pas de flux transfrontaliers”

Limites PWA :

  • iOS Safari pas de NFC eMRTD (Web NFC = Android Chrome only) → si NFC obligatoire, retomber sur SDK natif côté banque.
  • Caméra live quality moins fine (pas d’accès à AVCaptureSession fine-tuning)
  • Pas de DeviceCheck iOS → attestation device dégradée, compenser par signaux comportementaux (device fingerprint, IP reputation, vélocité)

La banque choisit la région VitaKYC de son client — elle connaît IBAN, résidence, produit. VitaKYC ne fait que servir ce que la banque route. Un pinset multi-cert est embarqué au build pour permettre le runtime switching sans casser le pinning.


// build.gradle.kts (app banque)
dependencies {
implementation("com.vitakyc.sdk:android:2.0.0")
}
// BuildConfig values — différents par flavor (saas/on-prem/multi-region)
android {
buildTypes {
release {
buildConfigField("String", "VITAKYC_TENANT_ID", "\"TN-BANQUEX\"")
buildConfigField("String", "VITAKYC_API_BASE", "\"https://api-tn.vitakyc.com\"")
buildConfigField("String", "VITAKYC_CERT_SHA256", "\"sha256/AAAA...\"")
}
}
}
// Application.kt
class BanqueXApp : Application() {
override fun onCreate() {
super.onCreate()
VitaKYC.configure(
VitaKYCConfig(
tenantId = BuildConfig.VITAKYC_TENANT_ID,
apiBase = BuildConfig.VITAKYC_API_BASE,
pinnedSha256 = listOf(BuildConfig.VITAKYC_CERT_SHA256),
sessionProvider = { bankBackend.issueKycToken() },
locale = LocaleUtil.currentLocale(),
theme = VitaKYCTheme.fromBrandKit(),
onEvent = { event -> analytics.track(event) }
)
)
}
}
// Démarrer un flow KYC depuis une Activity
startActivityForResult(
VitaKYC.onboardingIntent(this, productCode = "COMPTE_COURANT"),
REQUEST_KYC
)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_KYC) {
val result = VitaKYC.resultFrom(data) // typed sealed class
when (result) {
is KycResult.Submitted -> showPending(result.sessionId)
is KycResult.Rejected -> showReason(result.reason)
is KycResult.Cancelled -> resumeNavigation()
is KycResult.Error -> reportIncident(result.exception)
}
}
}
AppDelegate.swift
// Podfile (app banque)
pod 'VitaKYC', '~> 2.0'
import VitaKYC
VitaKYC.configure(
tenantId: Bundle.main.infoDictionary!["VitaKYCTenantId"] as! String,
apiBase: URL(string: Bundle.main.infoDictionary!["VitaKYCApiBase"] as! String)!,
pinnedSha256: [Bundle.main.infoDictionary!["VitaKYCCertSha"] as! String],
sessionProvider: { completion in
bankBackend.issueKycToken { jwt in completion(.success(jwt)) }
},
locale: Locale.current,
theme: VitaKYCTheme.fromBrandKit(),
onEvent: { event in Analytics.track(event) }
)
// Démarrer depuis un UIViewController
VitaKYC.presentOnboarding(
from: self,
productCode: "COMPTE_COURANT"
) { result in
switch result {
case .submitted(let sessionId): self.showPending(sessionId)
case .rejected(let reason): self.showReason(reason)
case .cancelled: self.resumeNavigation()
case .error(let err): self.reportIncident(err)
}
}
import { VitaKYC } from '@vitakyc/react-native';
await VitaKYC.configure({
tenantId: Config.VITAKYC_TENANT_ID,
apiBase: Config.VITAKYC_API_BASE,
pinnedSha256: [Config.VITAKYC_CERT_SHA256],
sessionProvider: async () => await bankBackend.issueKycToken(),
locale: DeviceInfo.getDeviceLocale(),
theme: brandKit,
onEvent: (event) => analytics.track(event),
});
const result = await VitaKYC.startOnboarding({ productCode: 'COMPTE_COURANT' });
switch (result.type) {
case 'submitted': showPending(result.sessionId); break;
case 'rejected': showReason(result.reason); break;
case 'cancelled': resumeNavigation(); break;
case 'error': reportIncident(result.error); break;
}
import 'package:vitakyc_sdk/vitakyc_sdk.dart';
await VitaKyc.configure(VitaKycConfig(
tenantId: const String.fromEnvironment('VITAKYC_TENANT_ID'),
apiBase: const String.fromEnvironment('VITAKYC_API_BASE'),
pinnedSha256: [const String.fromEnvironment('VITAKYC_CERT_SHA256')],
sessionProvider: () async => await bankBackend.issueKycToken(),
locale: Localizations.localeOf(context),
));
final result = await VitaKyc.startOnboarding(productCode: 'COMPTE_COURANT');
<!-- Servi depuis kyc.banquex.tn (CNAME vitakyc-cdn.com) -->
<script src="https://cdn.vitakyc.com/web/v2/vitakyc.min.js"
integrity="sha384-ABCD..." crossorigin="anonymous"></script>
<script>
VitaKYC.init({
sessionToken: new URLSearchParams(location.search).get('t'),
// tenantId résolu côté serveur via Host header, pas trusté côté client
onComplete: (result) => window.location.href = result.redirectUrl,
});
VitaKYC.mount('#kyc-root');
</script>

{
"iss": "https://auth.banquex.tn",
"sub": "user:5f3a7b2c",
"aud": "vitakyc:TN-BANQUEX",
"tenant_id": "TN-BANQUEX",
"kyc_session_id": "ksn_01HWR2X3",
"product_code": "COMPTE_COURANT",
"risk_context": {
"channel": "mobile-app",
"app_version": "4.12.0",
"device_risk": "low",
"geo_country": "TN"
},
"iat": 1714473600,
"nbf": 1714473600,
"exp": 1714475400
}
  • Signature : RS256 (clé privée banque). VitaKYC récupère la clé publique via JWKS à https://auth.banquex.tn/.well-known/jwks.json.
  • TTL : 30 minutes. Refresh via sessionProvider (SDK rappelle le backend banque).
  • Cross-check : VitaKYC vérifie aud == "vitakyc:<tenant_id>" ET tenant_id claim ET header X-Tenant-Id → les trois doivent matcher, sinon 401.

VitaKYCConfig(
pinnedSha256 = listOf(
"sha256/AAAA...", // cert courant
"sha256/BBBB...", // cert backup (pour rotation sans downtime)
"sha256/CCCC..." // cert next (préparation rotation)
)
)

Stratégie de rotation sans casser les apps déployées :

  1. T-60j : la nouvelle cert (next) est ajoutée au pinset de la prochaine release SDK (baked dans l’app banque au build suivant).
  2. T-30j : la banque publie une release app avec pinset = [courant, next].
  3. T-0 : VitaKYC rotate le cert du LB vers next. Apps non mises à jour continuent de marcher (pinset inclut courant jusqu’à son expiration).
  4. T+60j : courant retiré du pinset au build suivant.

On-prem : même logique, pilotée par la banque elle-même (c’est son cert).


ContrôleAndroidiOSWeb SDK
Cert pinningOkHttp CertificatePinnerURLSession + SecPolicyCreateSSL + delegateHSTS includeSubDomains; preload
TLS min version1.3 (fallback 1.2)1.3 (fallback 1.2)1.3
Downgrade protectioncleartextTrafficPermitted="false"NSAppTransportSecurity strictHSTS preload list
Device attestationPlay Integrity APIDeviceCheck / App Attest— (signaux comportementaux)
Root/jailbreak detectionJailroot detection lib + Play Integrity verdictiOS Jailbreak heuristics
ObfuscationR8 full mode + ProGuard rulesBitcode + Swift obfuscator toolingMinification Terser + SRI
Key storageAndroid Keystore (hardware-backed TEE)Keychain + Secure Enclave— (JWT éphémère, pas de long-lived secret)
Logs PIIScrubbing automatique (patterns CIN, passport, IBAN, téléphones)idemidem
Screenshot captureFLAG_SECURE pendant liveness + doc captureUIScreen.capturedDidChange + hook overlayimg-src 'none' CSP stricte
Screen recordingDétection + pause flowDétection + pause flow
ClipboardInterdit pendant saisie TIN, DOBidem<input autocomplete="off">

CasComportement attenduCode erreur
Bascule tenant à chaudInterdite — réinstall app banqueE_TENANT_LOCKED
Session expirée mid-flowRefresh silencieux via sessionProvider; sinon écran “reprise ultérieure” avec deeplink retourE_SESSION_EXPIRED
Cert pinning fail (proxy entreprise intercepte)Refus connexion, message clair + lien supportE_PINNING_FAIL
Deep-link différent userIntent vérifie sessionToken.sub == currentBankUser, sinon logout forcé + alerteE_USER_MISMATCH
Downgrade HTTPRefusé hardE_INSECURE_TRANSPORT
Device rooted/jailbrokenFlag risk=high, policy tenant : bloquant OU review manuelleW_DEVICE_RISK_HIGH
App en background pendant livenessReprise impossible iOS caméra → user recommence cette étape (state persisté côté serveur)W_LIVENESS_INTERRUPTED
Perte réseau mid-uploadRetry backoff exp (1s, 2s, …, cap 60s), session 30 minW_NETWORK_RETRY
Offline au cold startMode consultation du draft seulement, upload différéW_OFFLINE
JWT invalide (signature, aud, tenant)401 + alerte SIEM + logoutE_AUTH_INVALID
Version SDK dépréciée426 Upgrade Required + écran “mettez à jour votre app”E_SDK_OUTDATED
Langue non supportée par tenantFallback langue par défaut tenantW_LOCALE_FALLBACK
Caméra permission refuséeÉcran explicatif + deeplink settings OSE_PERM_CAMERA
NFC indisponible (iOS < 13, Android sans NFC)Parcours dégradé sans NFC (selfie + doc photo seulement)W_NFC_UNAVAILABLE

12. Observabilité — ce que le SDK doit émettre

Section intitulée « 12. Observabilité — ce que le SDK doit émettre »

Événements émis via onEvent callback + relayés à l’API VitaKYC pour agrégation :

ÉvénementQuandPayload
sdk.initializedAprès configure()tenant_id, sdk_version, platform, os_version
session.startedOuverture onboardingsession_id, product_code
step.startedEntrée dans une étapestep_name, step_index
step.completedSortie OK d’une étapestep_name, duration_ms
step.errorErreur bloquantestep_name, error_code, message
capture.quality.lowQualité doc/selfie insuffisantecapture_type, retry_count
liveness.detected / liveness.failedFin livenessattempt, score
network.retryÉchec réseau retryendpoint, attempt, delay_ms
session.submittedSoumission finalesession_id
session.cancelledUser quittestep_name, session_id

Ces événements alimentent les KPIs drop-off par étape et la détection proactive de régressions (cf. dashboard exécutif).


PlateformeBudget baselineBudget avec NFCBudget avec VideoKYC
iOS (universal, bitcode off)≤ 12 MB≤ 15 MB≤ 18 MB
Android (AAB, armeabi + arm64-v8a)≤ 8 MB≤ 10 MB≤ 13 MB
React Native bundle≤ 2 MB JS + natifidemidem
Flutter bundle≤ 3 MBidemidem
Web SDK core≤ 180 KB gzipped

Stratégie :

  • Modèles ONNX liveness/doc quality téléchargés à la première ouverture depuis CDN signé (pas bundled).
  • Module NFC chargé lazy (iOS : framework optionnel ; Android : feature module dynamique).
  • Module VideoKYC chargé lazy à l’ouverture du flow visio.
  • Règles i18n : langues client (FR/AR/EN) bundled, autres téléchargées à runtime.

Voir ADR-016 (à finaliser).


14. Checklist intégration pour une banque cliente

Section intitulée « 14. Checklist intégration pour une banque cliente »

Avant go-live, chaque banque doit valider :

  • Compte Apple Developer + Play Console actifs
  • Keystore Android dédié (pas de partage cross-apps)
  • Certs signing iOS (Distribution + Provisioning) présents dans Jenkins/Xcode Cloud/Fastlane
  • Endpoint JWKS publié sur https://auth.<banque>.tn/.well-known/jwks.json
  • Clé privée JWT protégée HSM/KMS (pas de fichier plat)
  • Backend banque expose POST /kyc/session/issue avec MFA fort
  • Cert pinning SHA256 (courant + backup + next) figés dans BuildConfig / Info.plist
  • Mobile gateway configuré pour rate-limit /kyc/* (anti-abus)
  • Monitoring APM (Datadog, New Relic, ELK) branché sur événements SDK
  • Runbook support pour codes erreur SDK (E_PINNING_FAIL, E_DEVICE_RISK_HIGH…)
  • DPA + LIR (Legitimate Interest) signés avec VitaKYC (ou accord on-prem)
  • Tests QA : iOS 14+, Android 9+, devices bas de gamme (RAM ≤ 2GB), mode faible réseau
  • Tests sécurité : device rooted, jailbreak, proxy intercept (Burp), replay attack
  • Go-live : canary sur 5% des users, full rollout si KPI drop-off < seuil

En on-prem ajouter :

  • Reverse proxy mobile gateway banque configuré pour /kyc/* → VitaKYC on-prem
  • Cert pinning = cert banque (pas cert VitaKYC) baked
  • mTLS gateway ↔ VitaKYC configuré (CA privée banque)
  • Audit trail BCT : logs WORM activés sur instance on-prem
  • Tests de rupture réseau : on-prem isolé d’internet, le SDK doit continuer à marcher