La méthode la plus sûre pour stocker les tokens d’accès dans une application moderne est de ne pas les persister : conservez le token d’accès à durée limitée en mémoire pendant la session et générez-en un nouveau à chaque lancement à partir d’un token de refresh stocké dans un stockage chiffré géré par la plateforme. Ne persistez que le token de refresh sur iOS dans le Keychain et sur Android avec Android Keystore (logiciel, TEE ou StrongBox selon l'appareil), ainsi que Jetpack DataStore + Tink. EncryptedSharedPreferences est obsolète dans androidx.security.crypto ; planifiez vos migrations dès maintenant. Pour les Capacitor apps, combinez la sécurité du transport (HTTPS, les outils et plugins de SSL pinning) avec des plugins de stockage natifs et @capgo/capacitor-native-biometric plutôt que de dupliquer la crypto en JavaScript.
Comment nous avons choisi ces modèles de stockage
Chaque recommandation de cet article passe par quatre filtres. D’abord, un stockage protégé par la plateforme : les secrets doivent résider dans Keychain ou Keystore, ou être chiffrés avec des clés qui y sont stockées. Privilégiez l’isolation matérielle quand l’appareil et la configuration des clés l’offrent réellement (par exemple des clés adossées à StrongBox ou Secure Enclave). Ensuite, un chemin de migration officiellement supporté. Si Google ou Apple a déprécié l’approche, nous ne la recommandons pas pour de nouvelles code. Troisièmement, la compatibilité Capacitor : le pattern doit fonctionner depuis la frontière d’un plugin sans exposer les clés brutes à JavaScript. Quatrièmement, la révocabilité : un credential fuité doit pouvoir être invalidé côté serveur.
Ce dernier filtre explique pourquoi la rotation des refresh tokens revient tout au long de cet article. Chainalysis a rapporté que plus de 305 millions de dollars ont été volés à DMM Bitcoin au Japon après la compromission d’une clé privée en 2024 (rapport 2024 de Chainalysis sur la criminalité). Les tokens d'auth mobile échouent de la même façon : un bearer token à longue durée de vie dans un stockage en clair est une invitation permanente. Nous cesserions de recommander toute architecture qui conserve indéfiniment un identifiant non rotatif.
Principes applicables sur chaque plateforme
Le tableau ci-dessous condense le tout en snapshots ; la logique sous-jacente est la séparation access/refresh token. Un token d'accès est à durée de vie courte, quelques minutes et non des jours, et c’est ce que votre API présente. Comme son rayon d’impact est limité et qu’il expire vite, il n’a sa place qu’en mémoire, reconstruit à chaque lancement de l’app. Le token de rafraîchissement est le secret durable qui vous permet d’émettre de nouveaux access tokens ; c’est donc lui seul qui mérite un stockage chiffré persistant, adossé au matériel. Des durées de vie différentes justifient des emplacements distincts ; stocker les deux de la même façon est l’origine de la plupart des fuites.
La séquence complète à l’exécution est la suivante : l’app se lance sans access token, appelle votre serveur d’auth avec le refresh token persisté, reçoit un nouvel access token en mémoire, puis détruit l’ancien refresh token car le serveur le fait pivoter à chaque utilisation. Si le serveur voit un refresh token déjà consommé présenté à nouveau, il le traite comme une preuve de vol et révoque toute la famille de tokens. À la déconnexion, le client efface le stockage local et le serveur révoque l’accord. Supprimer les données localement sans révocation côté serveur, c’est espérer, pas sécuriser.
| Pratique | Pourquoi c'est important |
|---|---|
| Tokens d’accès à durée limitée | Limite les dégâts en cas de fuite de token |
| Rotation des refresh tokens | Invalide les refresh tokens volés après une utilisation légitime |
| HTTPS uniquement | Bloque l’interception réseau opportuniste |
| Pinning de certificats (lorsqu’il est justifié) | Réduit les risques liés aux racines mal émises ou personnalisées ; voir les recommandations OWASP sur le certificate pinning |
| Aucun token dans les logs, les URL ou les analytics | Les tokens dans les outils d’observabilité deviennent des fuites |
| Révocation à la déconnexion | L’invalidation côté serveur vaut mieux que de compter sur la suppression côté client |
@capgo/capacitor-ssl-pinning peut faire du pinning TLS pour Capacitor HTTP sur iOS et Android lorsque vous avez besoin de contrôles de transport plus stricts. Pour un tour d’horizon plus large du durcissement du transport, voir Outils et plugins pour implémenter le SSL pinning.
Android : Keystore et DataStore (pas EncryptedSharedPreferences)
Google a déprécié le androidx.security.crypto helpers, y compris EncryptedSharedPreferences et MasterKey. Le résumé du package indique aux développeurs d'utiliser KeyGenerator avec le fournisseur AndroidKeyStore pour le nouveau matériel de clé.
Modèle recommandé
Utilisez un pipeline basé sur Tink de bout en bout :
- Créez un keyset Tink avec
AndroidKeysetManager, en stockant la clé maîtresse dans Android Keystore via unandroid-keystore://URI (voir ledatastore-tinknotes de release). AppelerisUsingKeystore()sur le gestionnaire et échouer de manière sécurisée s'il renvoiefalse. Tink peut sinon stocker le jeu de clés en clair. - Obtenir un
Aeadprimitive de ce jeu de clés. - Ajoutez
androidx.datastore:datastore-tink(DataStore 1.3.0 fournit l’artefact ; 1.2.x ne le fait pas), enveloppez votreSerializerparAeadSerializer, et passez-l’àDataStore. Lorsque plusieurs stores partagent un mêmeAeadclé, donnez à chaque store une valeur uniqueassociatedData, par exemple une valeur stable dérivée de son nom de fichier.
Google documente cela comme le chemin de remplacement pris en charge pour les androidx.security.crypto helpers obsolètes. Ne mélangez pas un JCA autonome SecretKey par AeadSerializer; le sérialiseur attend un Tink Aead instance.
Options de keystore à connaître
setIsStrongBoxBacked(true)QuandFEATURE_STRONGBOX_KEYSTOREest disponible pour une isolation des clés plus robuste.- Authentification utilisateur binding (
setUserAuthenticationRequired) when tokens should be accessible only after biometrics or device credential. - Désactiver la sauvegarde automatique pour les fichiers contenant des keysets ou du chiffré, sauf si vous comprenez le comportement de restauration ; la doc d’EncryptedSharedPreferences de Google mettait en garde contre la restauration de chiffré sans les clés.
Les modules Kotlin ou Java natifs dans Capacitor doivent implémenter ce pattern à l’intérieur du plugin ; JavaScript ne doit recevoir que des handles opaques ou l’état de session, jamais les clés brutes.
iOS : Keychain et Secure Enclave
Le Keychain reste l'endroit approprié pour les tokens sur iOS. Les éléments sont chiffrés et protégés par le système. Utilisez une constante d'accessibilité au périmètre restreint et Contrôle d’accès lorsque vous avez besoin d’une authentification par Face ID, Touch ID ou code.
Le choix de l'accessibilité est une véritable décision, pas une valeur par défaut à copier. Choisissez kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly when tokens must be usable by background work, such as push-triggered refreshes or background fetch, but must never migrate to a new device via backup. Pick kSecAttrAccessibleWhenUnlockedThisDeviceOnly pour un usage des tokens au premier plan uniquement, où la fenêtre plus étroite justifie la contrainte. Dans les deux cas, activez kSecUseDataProtectionKeychain sur les cibles modernes pour bénéficier du Keychain avec protection des données plutôt que de l'ancien basé sur des fichiers.
CryptoKit aide lorsque vous chiffrez vous-même les payloads, mais pour des tokens bearer OAuth standard, stocker directement la chaîne du token dans le Keychain avec la bonne accessibilité suffit généralement. Les clés adossées au Secure Enclave (une clé P-256 générée avec SecureEnclave dans CryptoKit) ne valent la peine que si la clé elle-même ne doit jamais quitter le matériel sécurisé, par exemple pour des opérations de signature ou d’attestation d’appareil. Elles ne peuvent pas stocker une chaîne de token à votre place ; elles protègent votre propre clé d’enveloppe. Si vous ne faites pas de rotation d’enveloppes et ne gérez pas ce cycle de vie, le stockage simple dans le Keychain est plus simple et tout aussi sûr. Un chiffrement supplémentaire au niveau de l’application n’est utile que si vous faites aussi tourner les clés et gérez les enveloppes avec soin.
Capacitor @capacitor/preferences écrit dans UserDefaults, qui est pas un stockage de secrets. Ne mettez pas de tokens là-dedans.
Capacitor : options de stockage sécurisé dans l’organisation Cap-go
Capgo fait pas publier un package nommé « secure storage », mais deux plugins couvrent les besoins courants en Capacitor :
| Plugin | Idéal pour | Socle natif |
|---|---|---|
@capgo/capacitor-native-biometric setData / getSecureData |
Petits secrets (tokens de session, clés API) | iOS Keychain ; chiffrement via Android Keystore dans SharedPreferences |
| @capgo/capacitor-data-storage-sqlite avec options de chiffrement | Données locales volumineuses ou structurées | SQLite avec SQLCipher en option |
Le guide du stockage clé-valeur compare Preferences, les stores SQLite et NativeBiometric.setData pour les secrets.
Nécessite une @capgo/capacitor-native-biometric 8.6.0 ou ultérieur (alignez la version majeure du plugin sur la version majeure de votre Capacitor). Exemple avec lecture protégée par biométrie :
import {
AccessControl,
NativeBiometric,
} from '@capgo/capacitor-native-biometric';
const TOKEN_KEY = 'auth.refresh_token';
export async function saveRefreshToken(token: string) {
await NativeBiometric.setData({
key: TOKEN_KEY,
value: token,
accessControl: AccessControl.BIOMETRY_CURRENT_SET,
});
}
export async function loadRefreshToken(): Promise<string | null> {
try {
const { value } = await NativeBiometric.getSecureData({
key: TOKEN_KEY,
reason: 'Unlock your session',
title: 'Authenticate',
});
return value ?? null;
} catch {
return null;
}
}
getData ne renvoie que les valeurs stockées sans accessControl. Pour la protection biométrique, associez toujours BIOMETRY_CURRENT_SET ou BIOMETRY_ANY par getSecureData.
Pour les builds web, ne persistez jamais les tokens de production dans localStorage. Utilisez plutôt le modèle de session web de votre backend.
Cycle de vie du token côté serveur et client
Oui, les applications mobiles doivent stocker les refresh tokens. L’alternative serait d’obliger les utilisateurs à se réauthentifier à chaque lancement, ce qui est un théâtre de la sécurité bien pire que la sécurité réelle. La nuance est où et dans: ne persistez que le refresh token, uniquement dans un stockage reposant sur Keychain ou Keystore, et uniquement avec une rotation et une révocation côté serveur.
Comment gérer et faire tourner en toute sécurité les refresh tokens dans une application mobile
La rotation, c'est ce qui rend la persistance sûre. Émettez les refresh tokens comme des à usage unique : chaque utilisation renvoie un nouveau refresh token et invalide l'ancien. Le serveur maintient une courte fenêtre de détection de réutilisation. Si un token censé avoir été consommé est présenté à nouveau, le système considère la famille comme compromise et révoque toute la chaîne, forçant une nouvelle connexion sur chaque appareil qui l’a partagé. Prévoyez une courte période de grâce (quelques secondes à une minute) pour les requêtes concurrentes et les reconnexions hors ligne, où un client légitime peut présenter l’ancien token une dernière fois avant de prendre connaissance du remplacement. L’absence totale de période de grâce exclut les utilisateurs sur des connexions instables ; une fenêtre trop longue élargit la surface d’attaque.
L’emplacement de chaque token découle directement de sa durée de vie :
- Problème tokens d’accès avec des TTL courts (en minutes, pas en jours) et les conserver uniquement en mémoire pendant la session.
- Persistez le token de rafraîchissement dans un stockage chiffré basé sur Keychain ou Keystore, et faites-le pivoter comme indiqué ci-dessus.
- On password change, logout, or device loss, révoquez les tokens de refresh côté serveur. La suppression des données locales par le client n’est pas une révocation.
- Envisagez une stratégie de liaison de token légère (attestation de clé d'appareil ou ID d'instance d'app) si votre modèle de menace l'exige.
When you ship security fixes to token handling in Capacitor apps, les live updates Capgo peut livrer rapidement les modifications éligibles au niveau web si vous respectez les politiques des stores. Les modifications du stockage natif nécessitent toujours un build store. Capgo vue d’ensemble de la sécurité résume la façon dont les apps de production combinent transport, stockage et contrôles de mise à jour.
Comparaison des options de stockage
| Méthode | Sécurité | Compatibilité Capacitor |
|---|---|---|
| Variable JS en mémoire | Élevée lors de l'exécution, perdue à la fermeture | OK pour un token d'accès uniquement pendant la session |
localStorage / Préférences |
Faible | Paramètres uniquement, jamais de secrets |
| Keychain iOS / Keystore Android | Élevée | Via code natif ou les plugins Capgo |
| DataStore + Tink (natif Android) | Élevée | Plugin Capacitor personnalisé ou auth native SDK |
| Crypto personnalisée en JS | Variable | À éviter ; les clés dans le JS sont extractibles |
À noter sur cette dernière ligne : le mode d’échec du chiffrement JavaScript ne réside pas dans des algorithmes faibles, mais dans le placement. Toute clé lisible par un runtime JS peut être exfiltrée par n’importe quel autre élément présent dans ce runtime, si bien que la « haute sécurité » est structurellement inatteignable à ce niveau, quelle que soit la bibliothèque choisie.
Checklist de migration depuis EncryptedSharedPreferences
- Inventory which keys hold secrets versus public config.
- Implémenter DataStore +
AeadSerializeravec un keyset Tink adossé au Keystore. - Migrer avec
SharedPreferencesMigrationen utilisant un producteur qui ouvre la version legacy EncryptedSharedPreferences instance (et non la version simpleSharedPreferences), puis supprimez les fichiers legacy après une lecture réussie. - Déployez la modification native dans une release store ; testez la sauvegarde et la restauration sur un appareil physique.
- Supprimer
androidx.security.cryptoles dépendances quand rien ne les référence.
FAQ
Comment implémenter un stockage sécurisé des tokens ?
Sur iOS, écrivez les refresh tokens dans le Keychain avec une accessibilité adaptée aux besoins de rafraîchissement en arrière-plan. Sur Android, utilisez un keyset Tink dans Android Keystore avec Jetpack DataStore et AeadSerializer au lieu des EncryptedSharedPreferences dépréciées. Conservez les access tokens uniquement en mémoire et utilisez HTTPS. Ajoutez le certificate pinning lorsque votre modèle de menace l’exige.
Comment gérer et faire tourner les refresh tokens en toute sécurité dans une application mobile ?
Stockez le refresh token uniquement dans un stockage adossé au Keychain ou au Keystore. Émettez des refresh tokens à usage unique afin que chaque utilisation génère un nouveau refresh token et invalide l’ancien. Maintenez une courte fenêtre de détection de réutilisation pour que la lecture d’un token rejoué révoque toute la famille, avec une petite période de grâce pour les clients hors ligne ou en concurrence.
Le stockage sécurisé des access tokens et refresh tokens est-il identique sur iOS et Android ?
La répartition est la même sur les deux plateformes : mémoire pour les access tokens à courte durée de vie, stockage chiffré protégé par la plateforme pour les refresh tokens. Les API diffèrent (Keychain versus Keystore plus DataStore), mais il faut éviter WebView localStorage, UserDefaults et SharedPreferences en clair pour les secrets.
Faut-il chiffrer les tokens avant de les mettre dans le Keychain ?
En général, aucune couche supplémentaire n’est nécessaire pour des bearer tokens standards si l’accessibilité du Keychain correspond à votre modèle de menace. Ajoutez un chiffrement par enveloppe si la réglementation ou les politiques de rotation des clés l’exigent.
SQLCipher dans un plugin Capacitor remplace-t-il le Keystore ?
SQLCipher protège le fichier de base de données, mais il faut toujours une clé forte dans Keystore ou Keychain pour déchiffrer la base. Intégrez la gestion de la phrase de passe de chiffrement à votre conception du stockage sécurisé.
Puis-je stocker des tokens dans Capacitor Preferences avec de l’obfuscation ?
L’obfuscation n’est pas du chiffrement. Preferences correspond à UserDefaults et SharedPreferences, sans protection matérielle.
Les apps mobiles doivent-elles stocker les refresh tokens ?
Oui, mais uniquement dans un stockage chiffré basé sur Keychain ou Keystore, associé à une rotation et une révocation côté serveur. Ne pas les stocker oblige à se réauthentifier à chaque lancement, ce que les utilisateurs contournent en exigeant des exigences de connexion plus faibles. Ce qui n’est jamais acceptable, c’est le refresh token dans Preferences, localStorage ou tout fichier en clair.
Quelle est la méthode la plus sûre pour stocker des access tokens dans une application moderne ?
Ne les persistez pas du tout. Gardez le token d’accès de courte durée en mémoire pendant la session et régénérez-l’à partir du refresh token à chaque lancement. Ne persistez que le refresh token dans un stockage chiffré basé sur le Keychain iOS ou le Keystore Android, et laissez la rotation et la révocation limiter les dégâts en cas de compromission d’un appareil.
Continuer la lecture : Secure Token Storage: Best Practices for Mobile Developers
Si vous utilisez Stockage sécurisé des tokens : bonnes pratiques pour les développeurs mobiles pour planifier la sécurité et la conformité, connectez-l’à Chiffrement pour les détails d'implémentation du chiffrement, Conformité pour les détails d'implémentation de la conformité, Scanner de sécurité Capgo pour le workflow produit dans Capgo Security Scanner, Capgo et la sécurité pour le workflow produit dans Capgo Security, et Capgo Centre de confiance pour le workflow produit dans Capgo Trust Center.