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 »| # | Surface | Pour qui | Publisher store | Distribution |
|---|---|---|---|---|
| 1 | SDK natif (iOS, Android, RN, Flutter) | Client final KYC | La banque | Maven Central, CocoaPods, Swift PM, NPM, Gradle, Pub |
| 2 | Web SDK / PWA branded | Client final KYC sans app banque (ou iOS sans NFC) | — (URL SMS/QR) | CDN sur kyc.<banque>.tn |
| 3 | VitaKYC Agent Mobile | Agents, superviseurs, compliance en mobilité | VitaKYC | Apple 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éploiement | Surface mobile | Où est le tenant_id ? | Où pointe apiBase ? | Cert pinning |
|---|---|---|---|---|
| SaaS partagé | SDK natif embarqué | Baked au build (BuildConfig) | https://api-<region>.vitakyc.com | Cert VitaKYC |
| SaaS dédié | SDK natif embarqué | Baked au build | https://<banque>.kyc.vitakyc.com | Cert VitaKYC |
| On-premise | SDK natif embarqué | Baked au build (utile pour logs) | https://mobile.<banque>.tn/kyc (gateway banque) | Cert banque |
| Hybride multi-pays | SDK natif embarqué | Résolu runtime via /me/region (backend banque) | Résolu runtime | Résolu runtime (pinset multi-cert) |
| Fallback PWA | Web SDK via SMS/QR | Dans le JWT + domaine | https://kyc.<banque>.tn (CNAME) | HSTS + preload |
| Agent Mobile | App VitaKYC standalone | Résolu au login du compte agent | Région de l’agent | Cert VitaKYC + tenant-specific overlay |
3. Flow de bootstrap — SaaS partagé
Section intitulée « 3. Flow de bootstrap — SaaS partagé »4. Flow de bootstrap — On-premise
Section intitulée « 4. Flow de bootstrap — On-premise »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-Idvalidé statique (mismatch → 403) - Pas de dépendance externe → conforme BCT “pas de flux transfrontaliers”
5. Flow fallback PWA (SMS / QR)
Section intitulée « 5. Flow fallback PWA (SMS / QR) »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 à
AVCaptureSessionfine-tuning) - Pas de DeviceCheck iOS → attestation device dégradée, compenser par signaux comportementaux (device fingerprint, IP reputation, vélocité)
6. Flow hybride multi-pays (holding régional)
Section intitulée « 6. Flow hybride multi-pays (holding régional) »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.
7. Interfaces SDK — signatures par plateforme
Section intitulée « 7. Interfaces SDK — signatures par plateforme »7.1 Android / Kotlin
Section intitulée « 7.1 Android / Kotlin »// 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.ktclass 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 ActivitystartActivityForResult( 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) } }}7.2 iOS / Swift
Section intitulée « 7.2 iOS / 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 UIViewControllerVitaKYC.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) }}7.3 React Native / TypeScript
Section intitulée « 7.3 React Native / TypeScript »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;}7.4 Flutter / Dart
Section intitulée « 7.4 Flutter / Dart »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');7.5 Web SDK / PWA
Section intitulée « 7.5 Web SDK / PWA »<!-- 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>8. Structure du JWT d’intégration banque
Section intitulée « 8. Structure du JWT d’intégration banque »{ "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>"ETtenant_idclaim ET headerX-Tenant-Id→ les trois doivent matcher, sinon401.
9. Cert pinning — gestion des rotations
Section intitulée « 9. Cert pinning — gestion des rotations »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 :
- T-60j : la nouvelle cert (next) est ajoutée au pinset de la prochaine release SDK (baked dans l’app banque au build suivant).
- T-30j : la banque publie une release app avec pinset = [courant, next].
- T-0 : VitaKYC rotate le cert du LB vers next. Apps non mises à jour continuent de marcher (pinset inclut courant jusqu’à son expiration).
- 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).
10. Sécurité — checklist
Section intitulée « 10. Sécurité — checklist »| Contrôle | Android | iOS | Web SDK |
|---|---|---|---|
| Cert pinning | OkHttp CertificatePinner | URLSession + SecPolicyCreateSSL + delegate | HSTS includeSubDomains; preload |
| TLS min version | 1.3 (fallback 1.2) | 1.3 (fallback 1.2) | 1.3 |
| Downgrade protection | cleartextTrafficPermitted="false" | NSAppTransportSecurity strict | HSTS preload list |
| Device attestation | Play Integrity API | DeviceCheck / App Attest | — (signaux comportementaux) |
| Root/jailbreak detection | Jailroot detection lib + Play Integrity verdict | iOS Jailbreak heuristics | — |
| Obfuscation | R8 full mode + ProGuard rules | Bitcode + Swift obfuscator tooling | Minification Terser + SRI |
| Key storage | Android Keystore (hardware-backed TEE) | Keychain + Secure Enclave | — (JWT éphémère, pas de long-lived secret) |
| Logs PII | Scrubbing automatique (patterns CIN, passport, IBAN, téléphones) | idem | idem |
| Screenshot capture | FLAG_SECURE pendant liveness + doc capture | UIScreen.capturedDidChange + hook overlay | img-src 'none' CSP stricte |
| Screen recording | Détection + pause flow | Détection + pause flow | — |
| Clipboard | Interdit pendant saisie TIN, DOB | idem | <input autocomplete="off"> |
11. Edge cases
Section intitulée « 11. Edge cases »| Cas | Comportement attendu | Code erreur |
|---|---|---|
| Bascule tenant à chaud | Interdite — réinstall app banque | E_TENANT_LOCKED |
| Session expirée mid-flow | Refresh silencieux via sessionProvider; sinon écran “reprise ultérieure” avec deeplink retour | E_SESSION_EXPIRED |
| Cert pinning fail (proxy entreprise intercepte) | Refus connexion, message clair + lien support | E_PINNING_FAIL |
| Deep-link différent user | Intent vérifie sessionToken.sub == currentBankUser, sinon logout forcé + alerte | E_USER_MISMATCH |
| Downgrade HTTP | Refusé hard | E_INSECURE_TRANSPORT |
| Device rooted/jailbroken | Flag risk=high, policy tenant : bloquant OU review manuelle | W_DEVICE_RISK_HIGH |
| App en background pendant liveness | Reprise impossible iOS caméra → user recommence cette étape (state persisté côté serveur) | W_LIVENESS_INTERRUPTED |
| Perte réseau mid-upload | Retry backoff exp (1s, 2s, …, cap 60s), session 30 min | W_NETWORK_RETRY |
| Offline au cold start | Mode consultation du draft seulement, upload différé | W_OFFLINE |
| JWT invalide (signature, aud, tenant) | 401 + alerte SIEM + logout | E_AUTH_INVALID |
| Version SDK dépréciée | 426 Upgrade Required + écran “mettez à jour votre app” | E_SDK_OUTDATED |
| Langue non supportée par tenant | Fallback langue par défaut tenant | W_LOCALE_FALLBACK |
| Caméra permission refusée | Écran explicatif + deeplink settings OS | E_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énement | Quand | Payload |
|---|---|---|
sdk.initialized | Après configure() | tenant_id, sdk_version, platform, os_version |
session.started | Ouverture onboarding | session_id, product_code |
step.started | Entrée dans une étape | step_name, step_index |
step.completed | Sortie OK d’une étape | step_name, duration_ms |
step.error | Erreur bloquante | step_name, error_code, message |
capture.quality.low | Qualité doc/selfie insuffisante | capture_type, retry_count |
liveness.detected / liveness.failed | Fin liveness | attempt, score |
network.retry | Échec réseau retry | endpoint, attempt, delay_ms |
session.submitted | Soumission finale | session_id |
session.cancelled | User quitte | step_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).
13. Budget de taille — SDK
Section intitulée « 13. Budget de taille — SDK »| Plateforme | Budget baseline | Budget avec NFC | Budget 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 + natif | idem | idem |
| Flutter bundle | ≤ 3 MB | idem | idem |
| 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/issueavec 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
15. Références
Section intitulée « 15. Références »- ADR-024 — Stratégie mobile SDK-first + tenant resolution
- ADR-002 — Multi-tenant Row Level Security
- ADR-010 — Distribution SDKs
- ADR-011 — E-signature (TunTrust QES)
- Maquettes UI — application complète — workflow 9 mobile SDK
- Spec OpenAPI
- Architecture technique
- NIST SP 800-63B — device assurance
- OWASP MASVS v2 — Mobile Application Security Verification Standard
- OWASP MSTG — Mobile Security Testing Guide (chapitres Android + iOS)
- BCT Circulaire 2017-08 §III (résidence des données)