
Connexion par QR code dans votre application : guide technique
Sur l'écran de la salle de réunion, un formulaire de connexion et un clavier virtuel. Quelqu'un déplace le curseur avec les flèches de la télécommande, lettre par lettre, et découvre que le mot de passe contient une majuscule et deux symboles. Quatre minutes plus tard, le formulaire expire et se vide.
Le scan-to-connexion existe à cause de cette salle. L'écran qui ne sait pas taper confie la saisie à l'appareil qui détient déjà les identifiants.
Le construire n'est pas difficile. Obtenir un modèle de sécurité correct demande plus de soin que ne le laisse croire le chemin nominal, car ce modèle a un mode de défaillance qu'aucune cryptographie ne corrige.
La forme de l'échange
L'échange fait intervenir cinq acteurs, dans cet ordre.
- Le client en attente. Application de bureau, télévision connectée, borne, tout ce qui n'est pas authentifié et possède un écran. Il demande à votre API d'ouvrir une session de connexion.
- Votre API. Elle crée un enregistrement en attente et renvoie deux valeurs distinctes.
- Le code. Le client affiche la moitié publique sous forme de symbole QR. Rien dans ce modèle n'est vraiment spécifique au QR ; le symbole n'est qu'un moyen de transport pour un identifiant court. Si la mécanique de ce transport vous est étrangère, commencez par ce qu'un QR code stocke réellement.
- Le téléphone. Déjà connecté. Il scanne, lit l'identifiant et demande à votre API ce qu'on lui propose d'approuver.
- L'approbation. L'utilisateur lit un écran qui nomme le demandeur, appuie une fois, et le client en attente reçoit les jetons.
Deux valeurs, pas une
La décision de conception la plus lourde de conséquences se joue à l'étape 2. Quand un client ouvre une session, renvoyez une paire.
- Un identifiant de session, à forte entropie et opaque, qui part dans le QR code.
- Un secret client, généré au même instant, qui ne quitte jamais le client en attente et n'apparaît jamais à l'écran.
Tout le monde dans la salle peut photographier le code sur le mur. Cela leur donne l'identifiant de session. Cela ne doit pas leur donner les jetons. Les jetons ne sont délivrés qu'à un appelant qui présente le secret client, si bien que l'identifiant affiché n'est rien de plus qu'un pointeur vers une demande en attente.
Stockez l'enregistrement avec un statut (pending, approved, consumed, expired, denied), created_at, expires_at, un hachage du secret client, et le contexte que vous montrerez plus tard à un humain : nom de l'application, plateforme, version, localisation approximative dérivée de l'adresse IP de la requête, et l'heure de début de la demande.
Attendre sans marteler
Le client en attente doit maintenant apprendre quand l'approbation arrive. Deux options.
Le polling. Un GET sur l'endpoint de statut toutes les deux secondes, avec le secret client dans un en-tête Authorization. Simple, survit à tous les proxys, coûte une requête par client et par intervalle. Plafonnez le nombre total d'interrogations par session côté serveur plutôt que de compter sur le client pour s'arrêter.
Une connexion maintenue. Un websocket ou un flux server-sent events, ouvert avec le secret client, poussant un seul message au changement d'état. Moins de requêtes, plus de pièces mobiles, et vous voudrez tout de même un repli en polling pour les réseaux qui coupent les connexions longues.
Dans les deux cas, les réponses antérieures à l'approbation ne doivent porter qu'un statut et un compte à rebours, rien d'autre. Aucune information de compte ne fuit vers une session non approuvée.
Ce que fait le téléphone
Votre application mobile scanne, extrait l'identifiant et appelle un endpoint de détail avec le jeton d'accès de l'utilisateur connecté. La réponse est le contexte que vous avez stocké : qui demande, depuis où, depuis quand. Le scan lui-même ne fait rien de plus que lire une chaîne, ce qu'il vaut la peine de rappeler si vous avez l'habitude de voir un scan comme une action.
Ensuite l'application affiche un écran d'approbation. Pas une notification passagère. Pas une boîte de dialogue dont la seule commande est un bouton intitulé Continuer. Un écran qui dit, dans la langue de l'utilisateur, quelle application demande l'accès, sur quel type d'appareil, depuis à peu près où, et depuis quand la demande est ouverte, avec Approuver et Refuser au même poids visuel.
À l'approbation, POST sur l'endpoint d'approbation avec l'identifiant et le jeton de l'utilisateur. Le serveur vérifie quatre choses avant de basculer quoi que ce soit :
- la session existe et est toujours en attente
- expires_at n'est pas dépassé
- le jeton approbateur est valide et appartient à une session pleinement établie, pas à une session créée il y a quelques instants par ce même flux
- la transition d'état gagne un compare-and-set, pour que deux appuis ou deux appareils ne puissent pas réussir tous les deux
Ensuite il lie l'approbation à cet enregistrement en attente précis : quel utilisateur a approuvé, à quelle heure, depuis quel appareil. Le prochain appel de statut par le détenteur du secret client renvoie les jetons, et l'enregistrement passe à consumed. Une session, un jeu de jetons, pas de rejeu.
L'expiration est une fonctionnalité, pas un réglage
Soixante à cent vingt secondes. Assez long pour retrouver son téléphone, assez court pour qu'une photo de l'écran soit périmée avant d'avoir pu être transférée où que ce soit.
Régénérez côté client quand le compte à rebours arrive à zéro, et rendez cette régénération visible pour que l'utilisateur sache que le code devant lui est le bon. Les enregistrements expirés sont supprimés, pas laissés en attente. C'est l'inverse d'un code marketing : un code dynamique est fait pour rester valide et être réorienté pendant des années, tandis qu'un code de connexion doit être mort en deux minutes et ne jamais revenir.
L'attaque qui définit ce modèle
Voici celle qui compte, et ce n'est pas un bug dans votre code.
Un attaquant ouvre une session de connexion sur sa propre machine. Votre serveur fait exactement ce qu'il doit et renvoie un identifiant authentique. L'attaquant prend une capture du code et l'envoie à une victime avec une histoire : vérifiez votre compte, confirmez la livraison, approuvez le contrôle de sécurité demandé par l'informatique. La victime scanne un vrai code, émis par un vrai serveur, et l'approuve. Les jetons sont livrés au client en attente de l'attaquant, pour le compte de la victime.
Rien n'a été falsifié. Le protocole s'est déroulé correctement de bout en bout. Le seul composant compromis était la compréhension qu'avait la victime de qui se trouvait en face. Cela appartient à la même famille que les codes placés là où ils n'ont rien à faire, et cela résiste aux correctifs techniques précisément parce que la partie technique a fonctionné.
Trois règles de conception y répondent.
Ne laissez jamais approuver une session de connexion depuis un lien. Si toucher une URL dans un message peut atteindre votre action d'approbation, la cérémonie de la caméra n'est que décoration. Limitez l'entrée de l'identifiant à votre scanner intégré. Quand une URL de connexion est ouverte dans un navigateur ou arrive en lien profond, servez une page d'explication sans le moindre bouton d'approbation.
Faites porter de l'information à l'écran d'approbation. Un utilisateur qui lit "Connexion sur une télévision connectée, Berlin, démarrée il y a 8 secondes" alors qu'il est assis dans une cuisine à Lisbonne a reçu le fait dont il a besoin. Un utilisateur qui lit "Approuver la connexion ?" n'a rien reçu.
Notifiez hors bande. Push et e-mail après chaque approbation, avec le même contexte et un moyen de mettre fin à cette session en une touche. Le second regard rattrape ce que le premier a manqué, ce qui est la leçon générale de la plupart des incidents QR au niveau du compte.
Et en France ?
La CNIL a publié des recommandations sur l'authentification par mot de passe, et elle encourage les dispositifs qui réduisent la dépendance au seul mot de passe. La connexion par QR code en fait partie, à condition de ne pas créer de nouvelle faiblesse.
Deux points retiendront l'attention d'un auditeur français : l'information de l'utilisateur, qui doit voir clairement sur son téléphone quel appareil il autorise et depuis quel lieu approximatif, et la conservation des journaux de connexion, à limiter à ce qui est nécessaire à la sécurité. Documentez ces choix dans votre registre des traitements.
Limites de débit, journaux et déploiement
Plafonnez la création de sessions par adresse IP et par empreinte d'appareil : un point d'entrée laissé ouvert offre gracieusement à l'attaquant l'énumération des sessions et la saturation du service. Appliquez la même logique aux interrogations de statut, limitées par session, et aux validations, limitées par compte et par heure. Journalisez ensuite chaque changement d'état avec le contexte de la requête et l'identité de la personne qui a validé, puis rendez ce journal consultable par votre support sans passer par un développeur. La phrase « ce n'est pas moi qui ai validé » finira par arriver dans un ticket, et ce jour-là la réponse doit tenir dans une recherche, pas dans une enquête.
Une note de rendu pour les clients TV et bornes : le code doit résister à une caméra à deux mètres dans une pièce avec des reflets. Fort contraste, zone de silence complète, et une taille choisie en fonction de l'endroit où se trouve réellement le canapé. Testez avec le comportement du scanner qu'utilisent vos utilisateurs, pas seulement avec votre propre téléphone.
Livrez derrière un drapeau de fonctionnalité, en gardant la connexion par mot de passe disponible, et surveillez deux chiffres pendant une semaine. Sessions créées contre sessions approuvées vous dit si le code est scannable. Le temps entre création et approbation vous dit si l'écran d'approbation déroute les gens, ce qui est exactement l'état dans lequel un attaquant a besoin de les mettre.


