MarkEdmondson1234 / googleAuthR

Google API Client Library for R. Easy authentication and help to build Google API R libraries with OAuth2. Shiny compatible.
https://code.markedmondson.me/googleAuthR
Other
175 stars 54 forks source link

googleAuth_jsUI() not working in Shiny app packed into Docker #188

Closed domsle closed 4 years ago

domsle commented 4 years ago

What goes wrong

A Shiny app that authenticates using googleAuth_jsUI works in RStudio Server, does not allow to autenticate when build into Docker image, and then run either directly from Docker or Shiny Proxy, or Shiny Server (here we will focus only on direct Docker).

Steps to reproduce the problem

Create and run the following app:

library(shiny)
options(googleAuthR.scopes.selected = c("https://www.googleapis.com/auth/webmasters"))
library(googleAuthR)
library(searchConsoleR)
Sys.setenv(GAR_CLIENT_WEB_JSON = "auth/analytics.json")

ui <- fluidPage(
  googleAuth_jsUI('auth', login_text = 'Login to Google')
)

server <- function(input, output, session) {
  auth <- callModule(googleAuth_js, "auth")
  observe(print(googleAuth_jsUI('auth', login_text = 'Login to Google')))
  ga_accounts <- reactive({
    req(auth())

    with_shiny(
      searchConsoleR::list_websites,
      shiny_access_token = auth()
    )

  })

  observeEvent(ga_accounts(),{observe(print(ga_accounts()))})

}

shinyApp(ui = ui, server = server)

This application requires to have a json file in auth/analytics.json. This, RStudio Server version can be run from RStudio, or from docker with this Dockerfile:

FROM rocker/r-ver:3.6.2 
RUN apt-get update 
RUN apt-get install -y libcurl4-openssl-dev 
RUN apt-get install -y libssl-dev 
RUN apt-get install -y libmariadbclient-dev 
RUN apt-get install -y zlib1g-dev 
RUN apt-get install -y libxml2-dev 
RUN R -e "install.packages('shiny')"
RUN R -e "install.packages('remotes')"
RUN R -e "install.packages('googleAuthR')"
RUN R -e "install.packages('searchConsoleR')"
RUN mkdir /root/app
COPY . /root/app

EXPOSE 3838
CMD ["R", "-e", "shiny::runApp('/root/app', port = 3838, host = '0.0.0.0')"]

Then, build it in console with: docker build -t image_tag . And run at port 4000 with: docker run -p 4000:3838 image_tag

Expected output

Note that while all this little thingie does is print your Google Console websites in console, the problem appears way earlier - the application running in RStudio Server shows only the login button: image After clicking and authentication, it will print a list of websites to console.

The console output, apart from the list (as I have an observe print on the button:

<script src="https://apis.google.com/js/auth.js"></script>
<button id="auth-login" onclick="auth();" class="btn btn-primary">Login to Google</button>
<button id="auth-logout" onclick="out();" class="btn btn-danger">Log Out</button>
<script type="text/javascript">var authorizeButton = document.getElementById('auth-login');var signoutButton = document.getElementById('auth-logout');signoutButton.style.display = 'none';function auth() {var config = {'client_id': '812127988718-4m3tejkgbqljgdn6ijok2qakk92ibl39.apps.googleusercontent.com','scope': 'https://www.googleapis.com/auth/webmasters' ,'approval_prompt':'force'};gapi.auth.authorize(config, function() {token = gapi.auth.getToken();console.log('login complete');Shiny.onInputChange('auth-js_auth_access_token', token.access_token);Shiny.onInputChange('auth-js_auth_token_type', token.token_type);Shiny.onInputChange('auth-js_auth_expires_in', token.expires_in);authorizeButton.style.display = 'none';signoutButton.style.display = 'block';});}function out(){gapi.auth.signOut();location.reload();}</script>
<script src="https://apis.google.com/js/auth.js"></script>
<button id="auth-login" onclick="auth();" class="btn btn-primary">Login to Google</button>
<button id="auth-logout" onclick="out();" class="btn btn-danger">Log Out</button>
<script type="text/javascript">var authorizeButton = document.getElementById('auth-login');var signoutButton = document.getElementById('auth-logout');signoutButton.style.display = 'none';function auth() {var config = {'client_id': '[hidden_censored_stuff].apps.googleusercontent.com','scope': 'https://www.googleapis.com/auth/webmasters' ,'approval_prompt':'force'};gapi.auth.authorize(config, function() {token = gapi.auth.getToken();console.log('login complete');Shiny.onInputChange('auth-js_auth_access_token', token.access_token);Shiny.onInputChange('auth-js_auth_token_type', token.token_type);Shiny.onInputChange('auth-js_auth_expires_in', token.expires_in);authorizeButton.style.display = 'none';signoutButton.style.display = 'block';});}function out(){gapi.auth.signOut();location.reload();}</script>
2020-07-28 08:21:48> Skipping token checks as using cache
2020-07-28 08:21:48> Request: https://www.googleapis.com/webmasters/v3/sites/
2020-07-28 08:21:48> Making new cache
2020-07-28 08:21:48> Forgetting cache

Actual output

Instead of one login button, there are shown both images at the same time: image

Console Output:

<script src="https://apis.google.com/js/auth.js"></script>
<button id="auth-login" onclick="auth();" class="btn btn-primary">Login to Google</button>
<button id="auth-logout" onclick="out();" class="btn btn-danger">Log Out</button>
<script type="text/javascript"></script>

After clicking the Login button this error appears: image Which is caused by the empty script in selected line: image In the correct website there is a full script in this line: image

This code appears to not be able to import JavaScript function here. I have also tried putting the .js file in the app directly in js folder, and this did not work.

Session Info

Server:

R version 3.6.2 (2019-12-12)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Debian GNU/Linux 9 (stretch)

Matrix products: default
BLAS:   /usr/lib/libblas/libblas.so.3.7.0
LAPACK: /usr/lib/lapack/liblapack.so.3.7.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C               LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8    LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C             LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] DT_0.5                 data.table_1.11.8      dplyr_0.8.0.1          stringr_1.3.1         
 [5] lubridate_1.7.4        tidyr_0.8.2            tseries_0.10-46        CausalImpact_1.2.3    
 [9] bsts_0.8.0             xts_0.11-2             zoo_1.8-4              BoomSpikeSlab_1.0.0   
[13] Boom_0.8               MASS_7.3-51.1          AnomalyDetection_1.0   Rcpp_1.0.0            
[17] d3heatmap_0.6.1        plotly_4.8.0           ggplot2_3.1.0          dygraphs_1.1.1.6      
[21] googleAnalyticsR_0.5.0 shinybusy_0.2.0        shinyWidgets_0.4.8     shinythemes_1.1.2     
[25] shinysky_0.1.3         shinyjs_1.0            searchConsoleR_0.3.0   googleAuthR_0.7.0     
[29] shiny_1.3.2           

loaded via a namespace (and not attached):
 [1] lattice_0.20-38   png_0.1-7         packrat_0.5.0     assertthat_0.2.1  digest_0.6.25     mime_0.9         
 [7] R6_2.4.1          plyr_1.8.4        httr_1.3.1        pillar_1.3.1      rlang_0.3.4       lazyeval_0.2.1   
[13] curl_3.2          rstudioapi_0.8    TTR_0.23-4        htmlwidgets_1.3   munsell_0.5.0     compiler_3.6.2   
[19] httpuv_1.5.1      pkgconfig_2.0.2   base64enc_0.1-3   htmltools_0.3.6   tidyselect_0.2.5  tibble_2.1.1     
[25] quadprog_1.5-5    viridisLite_0.3.0 crayon_1.3.4      withr_2.2.0       later_0.8.0       grid_3.6.2       
[31] jsonlite_1.5      xtable_1.8-3      gtable_0.2.0      magrittr_1.5      scales_1.0.0      quantmod_0.4-13  
[37] stringi_1.2.4     promises_1.0.1    tools_3.6.2       RJSONIO_1.3-1.4   glue_1.3.0        purrr_0.2.5      
[43] rsconnect_0.8.13  colorspace_1.3-2  memoise_1.1.0  

Docker:

R version 3.6.2 (2019-12-12)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Debian GNU/Linux 10 (buster)

Matrix products: default
BLAS/LAPACK: /usr/lib/x86_64-linux-gnu/libopenblasp-r0.3.5.so

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=C             
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] searchConsoleR_0.4.0 googleAuthR_1.1.1    shiny_1.4.0         

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.3       digest_0.6.25    later_1.0.0      assertthat_0.2.1
 [5] mime_0.9         R6_2.4.1         jsonlite_1.6.1   xtable_1.8-4    
 [9] magrittr_1.5     httr_1.4.1       rlang_0.4.4      fs_1.3.1        
[13] promises_1.1.0   tools_3.6.2      glue_1.3.1       httpuv_1.5.2    
[17] fastmap_1.0.1    compiler_3.6.2   gargle_0.4.0     memoise_1.1.0   
[21] htmltools_0.4.0 
MarkEdmondson1234 commented 4 years ago

I don't know the root cause, but I deployed the app.R above in a Docker container and it did load the JS script correctly, can you see if the below configuration solves your issue?

Folder:

|
|- app.R
|- Dockerfile
|- client.json

app.R

library(shiny)
library(searchConsoleR)
library(googleAuthR)
options(googleAuthR.verbose = 2)
gar_set_client(web_json = "client.json",
               scopes = "https://www.googleapis.com/auth/webmasters")

ui <- fluidPage(
  googleAuth_jsUI('auth', login_text = 'Login to Google'),
  tableOutput("sc_accounts")
)

server <- function(input, output, session) {
  auth <- callModule(googleAuth_js, "auth")
  # debug
  observe(print(googleAuth_jsUI('auth', login_text = 'Login to Google')))

  sc_accounts <- reactive({
    req(auth())

    with_shiny(
      list_websites,
      shiny_access_token = auth()
    )

  })

  output$sc_accounts <- renderTable({
    sc_accounts()
  })

}

shinyApp(ui = ui, server = server)

Dockerfile

FROM rocker/shiny

# install R package dependencies
RUN apt-get update && apt-get install -y \
    libcurl4-openssl-dev libssl-dev \
    ## clean up
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/ \
    && rm -rf /tmp/downloaded_packages/ /tmp/*.rds

## Install packages from CRAN
RUN install2.r --error \
    -r 'http://cran.rstudio.com' \
    googleAuthR searchConsoleR

COPY client.json /srv/shiny-server/client.json
COPY app.R /srv/shiny-server/app.R

EXPOSE 8080

USER shiny

# avoid s6 initialization
# see https://github.com/rocker-org/shiny/issues/79
CMD ["/usr/bin/shiny-server"]

The client.json was a web client json from my project:

{"web":{"client_id":"10XXX","project_id":"XXXX","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"XXXXX","redirect_uris":["http://localhost"],"javascript_origins":["https://www.example.com","http://localhost:1221"]}}

I needed to add the domain of my URL where the docker file was running to the JavaScript origins in the GCP console.

This ran and populated the JS button, HTML source of app:

<body>
  <div class="container-fluid">
    <script src="https://apis.google.com/js/auth.js"></script>
    <button id="auth-login" onclick="auth();" class="btn btn-primary">Login to Google</button>
    <button id="auth-logout" onclick="out();" class="btn btn-danger">Log Out</button>
    <script type="text/javascript">var authorizeButton = document.getElementById('auth-login');var signoutButton = document.getElementById('auth-logout');signoutButton.style.display = 'none';function auth() {var config = {'client_id': 'XXXX.apps.googleusercontent.com','scope': 'https://www.googleapis.com/auth/webmasters' , 'prompt':'consent'};gapi.auth.authorize(config, function() {token = gapi.auth.getToken();console.log('login complete');Shiny.onInputChange('auth-js_auth_access_token', token.access_token);Shiny.onInputChange('auth-js_auth_token_type', token.token_type);Shiny.onInputChange('auth-js_auth_expires_in', token.expires_in);authorizeButton.style.display = 'none';signoutButton.style.display = 'block';});}function out(){gapi.auth.signOut();location.reload();}</script>
    <div id="sc_accounts" class="shiny-html-output"></div>
  </div>
</body>
</html>
Screenshot 2020-07-28 at 13 45 25
MarkEdmondson1234 commented 4 years ago

I have this running on Cloud Run here: https://shiny-cloudrun-sc-ewjogewawq-ew.a.run.app/

domsle commented 4 years ago

I am sorry to be bothering you so much - unfortunately I still have a problem with the image. While it works, as it displays a button, and this button lets me autenticate, I think the autentication is never completed. After agreeing to all consents, the app never shows me the table of sites. Your app on Cloud Run works for me, so I think this might be Shiny Proxy related.

MarkEdmondson1234 commented 4 years ago

Where is it deployed? When doing an authentication flow open the web console to see any errors. The app deployed above did take a couple of hours to register the domain as allowable, so did error as you describe for the first few hours after deployment, which came up in the web console as "this domain is not allowed"

domsle commented 4 years ago

First of all, I would like to thank you for helping me with this issue, and for your patience :)

So I was right about redirect thing, but still have slight issue - so it is almost done! I fixed this, by adding a manually set:

  url <- reactive({
    paste0(
      session$clientData$url_protocol, "//", session$clientData$url_hostname, ifelse(session$clientData$url_port == "" | is.null(session$clientData$url_port), "", ":"), session$clientData$url_port,
      session$clientData$url_pathname
    )

And in a reactive object like observeEvent:

options(googleAuthR.redirect = url()

This flawlessly works on the Docker image, using latest R. But... my RStudio Server is using an older version of R, and there seem to be an incompatibility of some sort, so I get an error (not worth investigating, because it is cause by older library version I think). So, I decided to run this locally, on my machine's RStudio, but there, again I get the same issue. As I auth - the table does not appear. There don't seem to be any log messages, and the redir appears to be correctly set to: "http://localhost:7344/"

MarkEdmondson1234 commented 4 years ago

Glad it made progress. What library + version is causing the incompatibility? I can't help with the local issue without some more logging info.

domsle commented 4 years ago

Rstudio Server:

Verbose output:

ℹ 2020-07-30 15:48:27 > 
options(googleAuthR.scopes.selected=c(' https://www.googleapis.com/auth/webmasters ')) 
options(googleAuthR.client_id='  ') 
options(googleAuthR.client_secret='  ') 
options(googleAuthR.webapp.client_id='XXX.apps.googleusercontent.com ') 
options(googleAuthR.webapp.client_secret='YYYYYY')
Error in gar_set_client(web_json = "client.json", scopes = "https://www.googleapis.com/auth/webmasters") : 
  object 'project_id' not found

Sessioninfo:

R version 3.6.2 (2019-12-12)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Debian GNU/Linux 9 (stretch)

Matrix products: default
BLAS:   /usr/lib/libblas/libblas.so.3.7.0
LAPACK: /usr/lib/lapack/liblapack.so.3.7.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] rvest_0.3.2              xml2_1.2.0              
 [3] googleAuthR_1.3.0.9000   stringr_1.3.1           
 [5] plotly_4.8.0             ggplot2_3.1.0           
 [7] shinyjqui_0.3.3          shinyBS_0.61            
 [9] shinycustomloader_0.9.0  DT_0.5                  
[11] purrr_0.2.5              shinydashboardPlus_0.6.0
[13] shinycssloaders_0.2.0    lubridate_1.7.4         
[15] dplyr_0.8.0.1            dygraphs_1.1.1.6        
[17] CausalImpact_1.2.3       bsts_0.8.0              
[19] xts_0.11-2               zoo_1.8-4               
[21] BoomSpikeSlab_1.0.0      Boom_0.8                
[23] MASS_7.3-51.1            searchConsoleR_0.3.0    
[25] shinyWidgets_0.4.8       shinyjs_1.0             
[27] shiny_1.3.2              shinydashboard_0.7.1    

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.0        lattice_0.20-38   tidyr_0.8.2      
 [4] assertthat_0.2.1  digest_0.6.25     packrat_0.5.0    
 [7] mime_0.9          R6_2.4.1          plyr_1.8.4       
[10] httr_1.4.2        pillar_1.3.1      rlang_0.4.7      
[13] lazyeval_0.2.1    rstudioapi_0.8    data.table_1.11.8
[16] htmlwidgets_1.3   munsell_0.5.0     compiler_3.6.2   
[19] httpuv_1.5.1      pkgconfig_2.0.2   htmltools_0.3.6  
[22] tidyselect_0.2.5  tibble_2.1.1      viridisLite_0.3.0
[25] fansi_0.4.1       crayon_1.3.4      withr_2.2.0      
[28] later_0.8.0       grid_3.6.2        jsonlite_1.7.0   
[31] xtable_1.8-3      gtable_0.2.0      magrittr_1.5     
[34] scales_1.0.0      cli_2.0.2         stringi_1.2.4    
[37] fs_1.4.2          promises_1.0.1    tools_3.6.2      
[40] glue_1.4.1        colorspace_1.3-2  gargle_0.5.0     
[43] memoise_1.1.0    

Maybe lets ignore my PC for now then ;)