Le Conte des Deux Shiny Apps: Google Auth & ShinyProxy

Le Conte des Deux Shiny Apps: Google Auth & ShinyProxy

Il était une fois une horde de preux Chevaliers R qui ont se lancèrent une nouvelle quête : ils avaient pour mission d’amener le tout puissant Google Auth dans le royaume de ShinyProxy. Voici leur histoire, racontée de l’intérieur.

Au commencement, il y avait un conteneur

Pour ceux de nos lecteurs qui ne connaissent pas ShinyProxy, ce projet open source porté par Open Analytics est conçu pour déployer un serveur qui peut exécuter plusieurs Shiny Apps, et où chaque nouvelle application est lancée dans une nouvelle session. En quelques mots, ce que fait ShinyProxy est essentiellement de connecter un serveur à une liste de conteneurs Docker, et quand un nouvel utilisateur se connecte au service et veut ouvrir un app, un nouveau conteneur est lancé.

Ce processus de « one user one container » permet de contourner le problème de « single thread » de R : sur une application classique Shiny, si plusieurs utilisateurs se connectent en même temps, la même session R est utilisée. Et si l’on lance un processus qui prend 15 secondes, l’application est gelée pour le reste des utilisateurs qui doivent attendre que ce processus soit terminé. Vous pouvez trouver un bel exemple de cela dans ce talk : Joe Chen – Scaling Shiny apps with async programming, qui présente une autre façon de traiter ce problème : la programmation asynchrone avec le 📦 {promises}.

J’entends  d’ici votre question : pourquoi a-t-on besoin de ShinyProxy pour une « simple » connexion Google Auth ? Il se peut qu’il n’y ait pas beaucoup de calcul dans un contexte de tableau de bord, et même dans le cas contraire, nous pourrions toujours ajouter plus de RAM. Eh bien, non. Si nous ne déployons qu’une seule application , chaque utilisateur aura la même session R, ce qui signifie qu’une fois que le premier utilisateur est connecté, chaque nouvel utilisateur venant sur cette application sera connecté au même compte Google.

Mais bref, à première vue, rien de très compliqué jusqu’ici : une fois notre application Shiny développée, il nous suffit de construire une image Docker qui l’exécute, de la copier sur le serveur, et de laisser ShinyProxy faire le reste (enfin, après un peu de configuration, bien sûr…).

La première sous-requête : lancez Google Auth dans Docker

Notre histoire commence par une simple Shiny App.

Une histoire de session non interactive

Nous avons donc une application Shiny qui se connecte à Google Analytics et construit un rapport. Voici une application rapide, juste pour l’exemple :

library(shiny)
library(googleAnalyticsR)
ui <- fluidPage(
  tagList(
    # hide the button after connection
    tags$script('Shiny.addCustomMessageHandler("hideButton", function(id) { $(id).hide(); });'),
    actionButton("connect", "Retrieve data"), 
    verbatimTextOutput("list")
  )
)
server <- function(input, output, session) {
  observeEvent(input$connect, {
    account_list <- ga_account_list()
    output$list <- renderPrint({ account_list })
    # Hide the connection button once the connection is done
    session$sendCustomMessage(type = "hideButton", "#connect")
  })
}
shinyApp(ui, server)

Maintenant, nous aimerions mettre ça dans un Docker. Parce que, rappelez-vous, nous avons besoin de mettre des conteneurs sur notre serveur, afin qu’il fonctionne avec ShinyProxy. Mais voici ce qui se passe quand on essaie de lancer notre container :

Error in : Authentication options didn’t match existing session token and not interactive session
so unable to manually reauthenticate

Oui, par défaut, la « suite de packages Google » de Mark Edmondson ne permet pas de se connecter dans une session non interactive. Faisons une petite digression à ce sujet.

Le manuscrit de la connexion API

Lorsque vous créez une application sur un backend d’API (Twitter, Facebook, ou dans notre cas Google Developer Console), vous obtenez des paramètres d’identification (généralement appelé un client id et un client secret). Sur cette même application, vous devez définir un callback URL, qui est l’URL vers laquelle les utilisateurs se dirigent une fois l’authentification effectuée, et qui contient le jeton d’accès. Ces éléments sont passés d’un point à l’autre par paramètres d’URL, qui sont les éléments étranges que vous voyez parfois après le ? dans votre url.

En d’autres termes, si vous possédez un site appelé A.com et que vous voulez établir une connexion à une API sur B.com, vous devrez passer la connexion info avec B.com?bla=pouet&plop=12, B.com étant ici le point final de l’API. Lorsque vous ouvrez B.com avec ces paramètres, B.com analyse cette URL, crée un jeton d’accès blablabla, et ouvre le callback URL avec le jeton d’accès comme paramètre (dans notre cas, il ouvre A.com ? blablabla). Une fois que nous sommes de retour sur A, l’URL est analysée et nous avons en A notre clé d’accès à B.

Lorsque vous essayez de vous connecter à B.com depuis R, voici ce qui se passe (au moins avec {httr}) :

  • Vous donnez à R votre identifiant client et votre secret
  • R s’ouvre et écoute http://127.0.0.1:1410
  • R ouvre un navigateur vers B.com?id=this&secret=that
  • Le site web B.com analyse l’URL, fait sa magie, et ouvre l’URL de rappel avec le token comme paramètre
  • Comme vous l’avez défini sur B.com, ce rappel est http://127.0.0.1:1410, B.com ouvre http://127.0.0.1:1410?mytokenhere
  • Pendant que R écoute sur ce port 1410, il reçoit l’URL avec les paramètres, analyse ces paramètres, fait un peu de magie, ramène le jeton dans l’environnement utilisateur, et crée un petit fichier appelé .httr-oauth avec tout son contenu.

Et voilà le piège : quand sur une session non-interactive, R ne peut pas ouvrir un navigateur ou un port spécifique dans l’hôte.

Cela explique donc l’erreur que nous avons eue juste avant : R non interactif ne peut pas ouvrir un navigateur web, et aucun .httr-oauth n’est trouvé. Une solution serait de créer ce fichier lors d’une session interactive et de le copier dans l’image du Docker. Mais cela signifie que vous ne pouvez accéder qu’au compte google que vous avez précédemment connecté. Pas très scalable, car nous voulons que n’importe quel utilisateur puisse utiliser notre application.

(Pour info, cela arrive malheureusement aussi avec RMarkdown : lorsque vous « tricotez » votre document, une nouvelle session non-interactive est ouverte.)

Sauvé par {googleAuthR}

Alors, comment pouvons-nous contourner ça ? Peut-être utiliser la solution JavaScript ? C’est ce que Mark a développé dans son paquet : {googleAuthR} contient un module appelé gar_auth_jset gar_auth_jsUI, qui permet de créer un bouton de connexion à Google via JavaScript. Sur tadaa 🎉, cela fonctionne.

Problème résolu ? C’est ce qu’ont pensé nos héros qui ont réussi à déployer leur application dans un Docker. Mais leur joie a été courte, parce qu’alors est venu un événement redouté mais attendu : le déploiement avec ShinyProxy.

Combattre le dragon ShinyProxy

Nous y sommes : autoriser l’IP de notre serveur sur la console Google, paramétrer le callback, et faire fonctionner notre application en lançant un Docker. Parce que oui, Google Console demande deux choses quand il s’agit d’autorisation : l’URL d’où venir pour utiliser OAuth, et une URL vers laquelle aller une fois la connexion établie.

Lancer notre application dans la nature

Donc maintenant, nous étions prêts à commencer ce qui allait être notre dernière bataille : configurer ShinyProxy, et lancer les applications. Et là, nous nous sommes arrêtés, déconcertés : notre application n’a pas pu se connecter à Google Auth. Pas d’autorisation. Erreur 400. Avec pour seule aide, un robot amical qui murmurait des incantations bizarres :

 

Nous nous efforçons de déchiffrer les mots que ce robot magique nous disait. Et c’était là : le problème. « La redirection URI dans la requête .../endpoint/0ea5... ne correspond pas à celle autorisée pour le client OAuth ». Parce que oui, de manière cachée , ShinyProxy crée un id de endpoint, ajouté à la fin de l’URL de votre application pour chaque instance de l’application (qui n’est pas affiché à l’utilisateur) :

Donc, c’est simple, pouvons-nous mettre cette URL complète comme URL autorisée dans la console Google ? Oui, nous le pouvons. Et ça marche.

Mais comme vous avez pu le deviner, cette victoire n’était pas une victoire, et cette aventure n’est pas encore terminée.

Le chapitre sur les endpoint aléatoires

Voici le nouveau rebomdissement : ShinyProxy crée… des endpoints aléatoires où il dessert les applications. Cela signifie que nous ne pouvons pas anticiper à l’avance l’id de ces endpoints. Et, bien sûr, la console google n’autorise pas les wildcards (cela signifie que vous ne pouvez pas mettre url/app/endpoint/* pour spécifier que vous voulez une URL qui correspond à cette expression).

On avait l’impression d’être dans une impasse : pas de caractères génériques sur la console Google, pas moyen de prédire les ids (et même si, voudrions-nous vraiment copier et coller des centaines d’ids ?).

Une idée nous est venue à l’esprit : et si on accédait à la console par la ligne de commande ? Si nous le pouvions, nous aurions simplement à obtenir l’identifiant du terminal avec JavaScript (il est caché en haut de la page), l’envoyer à R, l’autoriser à la console Google en ligne de commande, et tout irait pour le mieux dans le meilleur des mondes.

 

Cependant, la connexion en ligne de commande à Google Console était introuvable. On a crié à l’aide :

Mais Internet n’était pas prêt à répondre à notre appel désespéré, et (merci Mark pour ton aide !) Twitter nous a fait remarquer que cela ne devait pas être possible d’accéder à la console Google par la ligne de commande.

A happy ending

A ce point de notre histoire, nos héros peuvent sembler perdus, sans aucune chance d’accomplir leur quête. Mais, dans une tournure inattendue des événements, l’un de nos braves chevaliers eu une idée…

Dissectons {googleAuthR}

Impossible d’accepter « Ce n’est pas possible », il fallait trouver un moyen de résoudre ce puzzle. Et voici une révélation : nulle part il n’était écrit que nous ne devions utiliser qu’une seule application pour faire cela. Non. Il nous en faut deux.

En clair : nous utilisons ShinyProxy parce que nous voulons contrôler la connexion, mais aussi parce que nous voulons que chaque utilisateur ait son propre R,pour qu’il soit plus facile de gérer les ressources et l’auth. Mais Google Auth ne demande pas autant de ressources. Ainsi, à part ces applications envoyées à des endpoints aléatoires de notre serveur, nous pouvons avoir une autre application qui ne gère que la connexion. Cette application est à une IP stable, donc nous pouvons facilement l’autoriser.

En théorie, ça marche. Si vous pouvez, bien sûr, ramener le jeton de l’application de connexion vers l’application principale. Et facilitez la tâche de l’utilisateur grâce à une interface utilisateur conviviale.

J’étais donc là, à disséquer les fonctions {googleAuthR}, juste pour identifier une chose : quelle est l’information minimale dont R a besoin pour créer un token ? Et la réponse est simple : un vecteur de caractères définissant l’état de la connexion, qui est renvoyé à l’URL de rappel comme paramètre. Donc, si j’obtiens cela, je peux créer une connexion (bien sûr, vous devez creuser à l’intérieur des fonctions de {googleAuthR} et recoder certains éléments pour permettre cela). C’est ce que notre application de connexion donnera à l’utilisateur, qui n’aura qu’à copier et coller ce vecteur de caractère dans l’application principale.

Et, pour rendre tout cela plus convivial, mettons en place deux petites choses :

  • L’application de connexion ne s’ouvre pas dans un nouvel onglet ou une nouvelle fenêtre, mais dans un pop-up, ce qui la rend un peu plus naturelle pour l’utilisateur, car il semble que vous n’ouvrez pas une nouvelle application. Comme vous pouvez le voir ici, je suis sur deux ports différents du serveur. Mais je peux parier que 99% des utilisateurs ne le remarqueront pas.

    • Créer un bouton pour copier le token (juste un peu de JavaScript et c’est fait)

Je dois admettre que ce n’est pas optimal (enfin, comparé à l’authentification interactive), mais ça marche, et en fin de compte ça ne semble pas si artificiel ou complexe du point de vue de l’utilisateur : click, click, click, click, click, click, click, paste, click et c’est terminé.

Ce fut un bon combat

Maintenant que nos héros sont partis pour d’autres aventures passionnantes, nous espérons que leur histoire vous aidera à faire face à la redoutable quête d’amener Google Auth dans ShinyProxy. La morale de l’histoire ? Une fois confronté à ce qui semble être une quête infaisable, prenez du recul, sortez de votre application, et peut-être avez-vous juste besoin de coder une autre application.