Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utilisation des intents sur application Cordova #232

Open
ptbrowne opened this issue Nov 29, 2017 · 21 comments
Open

Utilisation des intents sur application Cordova #232

ptbrowne opened this issue Nov 29, 2017 · 21 comments

Comments

@ptbrowne
Copy link
Contributor

ptbrowne commented Nov 29, 2017

J'ouvre cette issue pour démarrer une discussion sur comment va-t-on faire pour faire marcher les intents sur mobile. J'ai essayé de proposer quelques pistes et aimerait avoir vos retours là-dessus.

Cas d'usages d'intent

  • Ouverture d'un fichier dans drive (pour le moment implémentée en redirigeant l'utilisateur sur une URL spécifique de Drive https://mycozy-drive/#files/{factureID})
  • Récupération de l'URL de téléchargement d'un fichier (pour le moment fonctionnalité non implémentée car EDF n'est pas une application mobile native)
  • Recherche

Intent via inAppBrowser au lieu d'une iframe

L'utilisation de inAppBrowser pose le problème de la communication entre l'application et le service. Sur mobile nous ne pouvons pas utiliser les iframes à cause des politiques de sécurité de Cordova.
En effet, pour pouvoir ouvrir une iframe il faut inscrire son domain dans la whitelist et
étant donné que nous ne connaissons pas le domaine en avance, nous ne pouvons pas le faire.

Nous pouvons cependant utiliser inAppBrowser qui utilise une véritable fenêtre pour ouvrir
la page demandée.

Via postMessage, executeScript

const w = window.open('[Adresse de l'intent]', '_blank')

La communication entre les deux fenêtres semble difficile (surtout de l'inappbrowser vers l'app).

J'ai essayé (comme préconisé sur https://www.telerik.com/blogs/cross-window-communication-with-cordova's-inappbrowser) :

w.executeScript({ code: 'localStorage.setItem("hello", "world")' })
w.executeScript({ code: 'localStorage.getItem("hello")' }, function (values) { console.log(values) })

La valeur dans localStorage est bien inscrite mais on ne peut pas la récupérer ☹️. Cela semble un
problème qu'on plusieurs personnes : https://www.google.fr/search?q=inappbrowser+callback+not+firing&oq=inappbrowser+callback+not+firing&aqs=chrome..69i57j69i60l3.4359j0j4&sourceid=chrome&ie=UTF-8. Avec les politiques
de CSP strictes que l'on a, je doute que cela ne marche.

✅ On peut cependant écouter sur l'évenement "exit" de la fenêtre.

w.addEventListener('exit', () => { checkIfTheUserHasCompletedTheIntent() })

Stocker valeur de retour dans l'intent

Stocker une valeur de retour dans l'intent (via un HTTP POST par leur service et un HTTP GET par l'application) permettrait sa lecture par l'application à la fermeture de l'inAppBrowser.

⚠️ Nécessite une route PUT sur l'intent pour le modifier (seulement la valeur de retour doit être modifiable, juste une fois).

Websocket pour communication permanente

Idée pour communiquer des valeurs : passer par une websocket. Le service et l'application
recevrait de la stack un id + token ou ils pourraient communiquer. Inconvénients:
complexité chez la stack, latence, 2 connexions en plus par intent. Avantage : assez générique et
niveau de confiance plutôt élevé sur le fait que ca marche.

Abstraction du channel de communication app/service

Ces 2 possibilités d'ouverture/communication entre l'application et le service (via une iframe/postMessage, inAppBrowser/HTTP GET intents/id/returnValue) nécessiterait une abstraction niveau cozy-intent pour que l'utilisateur n'ait pas à se préoccuper de ces détails bas-niveaux, idéalement juste utiliser une option { mobile: true } ?

Possibles améliorations

Login automatique sur la stack

L'inAppBrowser ne partage pas ses cookies avec la webview Cordova, à l'ouverture de l'intent, l'utilisateur devrait donc se logguer. Pour améliorer l'expérience utilisateur, il pourrait être intéressant de le logguer directement dans l'inappbrowser, est ce que ce serait possible de demander à la stack un token d'authentification à n'utiliser qu'une fois et qui permettrait à l'utilisateur de ne pas avoir à retaper son mot de passe ?

w = window.open('https://mydomain.cozy.cloud/loginViaToken={token}&redirect={intentURL}', '_blank')

Nettoyage des intents

Le nettoyage des intents n'a pas été implémenté, l'idée étant de les supprimer juste après leur lecture par le service. Si l'on souhaitere stocker une valeur de retour dans l'intent pour lecture par l'application mobile, il faudrait le supprimer après lecture de la valeur de retour. Cela pourrait être configuré au lancement de l'intent avec une option spécifique { deleteAfter: 'readByService' }, { deleteAfter: 'readByApp' }. Autre possibilité : l'option {mobile:true} évoquée plus haut pourrait abstraire ce détail.

@nono
Copy link
Member

nono commented Nov 29, 2017

Login automatique sur la stack

Non, ce n'est pas possible. Ce serait une grave faille de sécurité que de permettre ça.

@y-lohse
Copy link
Contributor

y-lohse commented Nov 29, 2017

En effet, pour pouvoir ouvrir une iframe il faut inscrire son domain dans la whitelist et
étant donné que nous ne connaissons pas le domaine en avance, nous ne pouvons pas le faire.

Quelles seraient les conséquences de whitelister * ? Si ca ne touche que les intents, ils ont leur propre couche de sécurité ou ils n'écoutent que les messages venant d'une fenêtre bien précise, donc je ne crois pas qu'il y aurais de gros risques. Par contre j'imagine que ca crée d'autres risques ailleurs ?

Stocker valeur de retour dans l'intent

On n'était pas super chaud pour ca à l'origine, pour éviter de faire transiter les valeurs de retour par le serveur (alors que techniquement elle est déjà sur l'appareil, "juste" dans une autre fenêtre). Du coup la les websockets me paraissent le mieux /o. Il se trouve qu'on a d'autres use-cases entre temps pour de la communication permanente via les intents. go get websocket-server !

Désolé pour ces réponses pas très drôles.

@ptbrowne
Copy link
Contributor Author

ptbrowne commented Nov 29, 2017

Non, ce n'est pas possible. Ce serait une grave faille de sécurité que de permettre ça.

Un peu plus d'explication s'il te plaît ? J'ai soif d'apprendre.

Quelles seraient les conséquences de whitelister * ?

Ca ne toucherait pas que les intents. Ca permettrait à n'importe quelle iframe d'être ouverte donc potentiellement à une application voyou de faire fuiter des données vers un serveur tiers.

On n'était pas super chaud pour ca à l'origine, pour éviter de faire transiter les valeurs de retour par le serveur (alors que techniquement elle est déjà sur l'appareil, "juste" dans une autre fenêtre).

Passer par une websocket fait aussi transiter les données via le serveur. Mais elles ne sont pas stockées. Le stockage est un problème de ressource, ou de vie privée ? Est ce que c'est vraiment un soucis si l'intent est supprimé ?

Il se trouve qu'on a d'autres use-cases entre temps pour de la communication permanente via les intents.

Tu as des exemples ?

@y-lohse
Copy link
Contributor

y-lohse commented Nov 29, 2017

Ca ne toucherait pas que les intents. Ca permettrait à n'importe quelle iframe d'être ouverte donc potentiellement à une application voyou de faire fuiter des données vers un serveur tiers.

Ok, mais qui ouvre cette iframe ? Pour nos propres applis, on sait ce qu'on ouvre je pense (rien à part des intents que je sache). Et des applis tierces connectés au Cozy, elles n'ont pas besoin d'iframe pour faire fuiter des données.

Passer par une websocket fait aussi transiter les données via le serveur. Mais elles ne sont pas stockées. Le stockage est un problème de ressource, ou de vie privée ? Est ce que c'est vraiment un soucis si l'intent est supprimé ?

Tu as raison, au temps pour moi. C'était plus une question de ressources, étant donné que les données qui transitent sont souvent déjà connus par le serveur.
En fait dans ma tête j'ai pensé à WebRTC et pas WebSockets. Est-ce que vous avez regardé de ce côté la aussi par hasard ?

Tu as des exemples ?

Oui j'aurais du les inclure d'ailleurs. Essentiellement Claudy et la recherche. Pas sûr que ce soient des sujets sur mobile aussi.

@ptbrowne
Copy link
Contributor Author

Et des applis tierces connectés au Cozy, elles n'ont pas besoin d'iframe pour faire fuiter des données.

Mmh je pensais justement que notre but avec les CSP, c'était de prévenir ça.

En fait dans ma tête j'ai pensé à WebRTC et pas WebSockets. Est-ce que vous avez regardé de ce côté la aussi par hasard ?

J'ai pas du tout regardé WebRTC mais ca me semble risqué de partir là dessus étant donné que les webview Cordova peuvent être vieilles et que WebRTC est encore à l'état de draft. Je m'y connais pas plus.

la recherche. Pas sûr que ce soient des sujets sur mobile aussi.

La recherche me semble être un sujet très mobile, j'avais complètement zappé que la recherche utilisait un intent. En effet, dans le cas de la recherche, la valeur de retour n'est pas top puisque on veut faire transiter plusieurs informations sur un même intent.

@kosssi
Copy link
Contributor

kosssi commented Nov 30, 2017

Actuellement nous arrivons à communiquer entre une fenêtre InAppBrowser et l'application qui la lance grâce à l'événement loadstart qui permet de récupérer l'url (c'est un hack). Par exemple, nous utilisons se procéder lors de l'authentification pour récupérer le token.

Le premier soucis à résoudre est je pense l'authentification.

Voici une solution :

  • Au lieu de passer par une fenêtre InAppBrowser pour s'authentifier la première fois (ce qui n'est pas forcément top au niveau UX), on demanderait directement le mot de passe de l'utilisateur dans l'application.
  • On pourrait alors demander un token spécifique pour l'application (en arrière plan) avec les bons droits à la stack et faire comme maintenant.
  • Et en même temps ouvrir une fenêtre InAppBrowser invisible par l'utilisateur avec une authentification automatique (vu que l'on a le mot de passe). InAppBrowser sauvegarde donc un cookie d'authentification valide pour 7 jours. On peut donc maintenant ouvrir une fenêtre InAppBrowser pour n'importe quel intent. (il est possible de dire à InAppBrowser de garder les cookie avec le paramètre clearcache=no et clearsessioncache=no)

Je viens de voir avec le back mais il réfléchissent à ce que le cookie n'expire seulement au bout de 7 jours d'inactivité. Il suffirait donc de pinguer la stack à intervalles réguliers pour que le cookie n'expire pas.

Un des soucis, c'est si le cookie expire et que l'on n'a pas sauvegardé le mot de passe alors il faudra redemander le mot de passe à l'utilisateur. On peut résoudre ça en le sauvegardant mais c'est vraiment pas top.

NB: Cette solution n'est pas forcément idéal. C'est plus un hack pour aller vite mais clairement la solution des websockets serait top. Avec toujours le soucis que l'on devrait redemander le mot de passe de l'utilisateur.

@ptbrowne
Copy link
Contributor Author

ptbrowne commented Nov 30, 2017

@kosssi m'a pointé le code de l'authentification mobile qui utilise l'évenement loadstart pour communiquer avec l'inAppBrowser. Il est possible de communiquer avec l'inAppBrowser en se basant sur loadstart/loadstop.

Cette méthode me semble cependant assez fragile, il faut que l'intent et possiblement toutes les pages qu'il peut charger n'aient pas de problème avec des urls "bizarres".

Dans le service :

const base = window.location.href // wishing the application will not change URLs, brittle
const sendData = function (data) {
  window.location.href = base + '?data=' + encodeURIComponent(JSON.stringify(data))
}

Dans l'application :

const regex = /.*\?data=(.*)/
const getData = function (url) {
  const match = regex.exec(url)[1];
  return match &&  JSON.parse(decodeURIComponent(match))
}

const onLoadStop = function (options) {
  console.log(getData(options.url))
}

const w = window.open('http://adresse-de-l-intent', '_blank')
w.addEventListener('loadstop', onLoadStop)

@ptbrowne
Copy link
Contributor Author

Au lieu de passer par une fenêtre InAppBrowser pour s'authentifier la première fois (ce qui n'est pas forcément top au niveau UX), on demanderait directement le mot de passe de l'utilisateur dans l'application.

Je ne vois pas vraiment de différence entre redemander le mot de passe de l'utilisateur dans l'application et dans l'inAppBrowser. Cela poserait aussi un problème de sécurité car l'application aurait accès au mot de passe de l'utilisateur (et donc à tout ses doctypes et plus...)

@nono
Copy link
Member

nono commented Nov 30, 2017

Version longue de ma réponse précédente (que je n'avais pas eu le temps d'écrire jusque là).

Le modèle de sécurité pour les apps mobils n'est pas très clair. On était parti sur le principe que l'app mobile que l'on développe doit montrer la bonne manière de faire pour les développeurs tiers. En particulier, on n'a aucun moyen fiable côté stack de savoir si une requête provient d'une app mobile Cozy ou d'une app tiers.

On s'est appuyé sur OAuth 2, avec un modèle à 3 pattes. Et dans ma tête, la partie où l'utilisateur se connecte sur la stack puis donne les permissions se faisait dans le navigateur de l'utilisateur, pas dans l'inAppBrowser. D'un point de vue UX, ce n'était pas acceptable et on s'est donc rabattu sur l'inAppBrowser. On considère toujours que l'app ne doit pas avoir accès au mot de passe, ni aux données de l'utilisateur en dehors de celles qu'il a accepté explicitement. C'est particulièrement important pour les apps mobiles, car contrairement aux apps web, on ne peut pas contrôler ces communications externes avec du CSP (et donc les fuites de données).

Du coup, si on ajoute un moyen pour les apps mobiles de donner un token qui authentifie l'utisateur, elle pourrait transmettre ce token à un tiers, qui aurait alors accès à tout le cozy de l'utilisateur (mot de passe des comptes bancaires y compris).

@nono
Copy link
Member

nono commented Dec 7, 2017

OK, donc, si je résume, la proposition serait :

  • l'app mobile fait un appel POST /intents sur la stack pour créer l'intent
  • puis, en fonction du retour, elle ouvre un inAppBrowser sur le service
  • en parallèle (en fait peut-être même avant), elle se connecte au temps-réel côté stack (websocket sur /realtime/) demande à être notifié pour {"method": "SUBSCRIBE", "payload": {"type": "io.cozy.intents", "id": "intent-id"}}
  • l'utilisateur se connecte dans cet inAppBrowser puis y fait des choses
  • le service peut faire un ou plusieurs POST /intents/:intent-id pour envoyer des messages JSON
  • l'app mobile reçoit ses messages
  • quand c'est fini, l'inAppBrowser est fermé.

Côté stack, ça me paraît bien aller.

@ptbrowne
Copy link
Contributor Author

ptbrowne commented Dec 7, 2017

Côté stack, ça me paraît bien aller.

🎉

le service peut faire un ou plusieurs POST /intents/:intent-id pour envoyer des messages JSON

Je pensais que le service se connecterait aussi sur la websocket pour envoyer/recevoir des messages, ca peut marcher comme ca avec /realtime ? Je pense surtout à des intents comme la search qui envoie et recoivent possiblement plusieurs messages rapides.

@nono
Copy link
Member

nono commented Dec 7, 2017

Envoyer des messages dans la websocket ne permet que de contrôler ce que l'on veut recevoir dedans. Si ton besoin est de pouvoir passer de l'information de l'app mobile vers le service, on peut imaginer que le service écoute lui aussi en websocket et que l'app mobile puisse également faire des requêtes vers la stack pour envoyer des données.

@ptbrowne
Copy link
Contributor Author

ptbrowne commented Dec 7, 2017

Envoyer des messages dans la websocket ne permet que de contrôler ce que l'on veut recevoir dedans.

Pas sur d'avoir compris ? "ne permet pas de contrôler" c'est ca ?

Si ton besoin est de pouvoir passer de l'information de l'app mobile vers le service, on peut imaginer que le service écoute lui aussi en websocket et que l'app mobile puisse également faire des requêtes vers la stack pour envoyer des données.

Ok ca me va

@nono
Copy link
Member

nono commented Dec 7, 2017

C'est décrit ici -> https://cozy.github.io/cozy-stack/realtime.html#websocket-api

En gros, tu peux envoyer des trucs dans la websocket pour dire « je veux être prévenu quand il y a un changement sur les fichiers », mais ça n'aura aucun effet sur le système (la base de données et les autres connexions en websocket).

@gregorylegarec
Copy link
Contributor

(Juste pour rappel, on a une implémentation du realtime dans Collect.)

@ptbrowne
Copy link
Contributor Author

on peut imaginer que le service écoute lui aussi en websocket et que l'app mobile puisse également faire des requêtes vers la stack pour envoyer des données.

OK parfait.

Donc si je comprends bien, il n'y a rien à faire côté stack vu que le realtime est déjà implémenté ?

Du côté de cozy-client-js il faudrait rapatrier le realtime.js de collect dans cozy-intents (qui naitra prochainement) et s'en servir pour les nouveaux services.

@nono
Copy link
Member

nono commented Dec 18, 2017

Si, côté stack, il y a un peu de boulot pour ajouter la route POST /intents/:intent-id

@ptbrowne
Copy link
Contributor Author

ptbrowne commented Dec 18, 2017

Au final la route POST /intents/:intents-id ne ferait que mettre des messages dans la websocket ? Ne vaut-il mieux pas avoir une route POST /websocket/:websocket-id ? Par mieux je veux dire, pour que ce soit plus générique et que ce ne soit pas seulement utilisable par les intents.

@nono
Copy link
Member

nono commented Dec 18, 2017

Au final la route POST /intents/:intents-id ne ferait que mettre des messages dans la websocket ?

Je pensais aussi garder dans couchdb un historique (des logs) de ce qui passe par ce canal.

Ne vaut-il mieux pas avoir une route POST /websocket/:websocket-id ?

On n'a pas actuellement d'id de websocket et c'est assez compliqué à gérer à cause des déconnexions/reconnexions possibles. Et il faut également savoir gérer les autorisations, ce qui me semblent aussi être un terrain assez glissants.

@ptbrowne
Copy link
Contributor Author

OK pour moi

@gregorylegarec
Copy link
Contributor

je déterre ce sujet pour ajouter deux informations

  • L'implémentation actuelle du temps réel côté front ne gère pas l'envoi de messages
  • Je pense qu'il ne faut pas intégrer le realtime à la future lib des intents mais bien que chacun de ces modules soit une lib.

@CPatchane CPatchane removed their assignment Sep 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants