I'm using R Studio Server in combination with R Shiny, running on an Ubuntu 16.04. Everything works fine. I want to secure the R Shiny dashboards (username+pw), and I'm thinking about building a small webpage that communicates with AWS Cognito to verify the users.
I can't find any documentation about this combination (Shiny + Cognito), but do find quite some documentation about both R Shiny Authentication (using NGINX + Auth0) and the use of Cognito (for example in combination with NodeJS).
Is a combination of Shiny and Cognito (with for example PHP or Node JS) logical and secure? What would be the best way to go: a simple web page with some PHP, or a Node JS application, with Shiny incorporated in it?
I realize this question is rather broad, but since I'm sure I'm not the only one walking around with this questions, I still ask so everyone can profit from possible solutions.
Here is a description of the set-up I have implemented. This is using AWS Cognito along with AWS-specific features.
Context: I have a bunch of shiny apps, packaged in containers (typically using asachet/shiny-base or one of these Dockerfiles as a base). I want to host them privately and control who can access them.
The set-up below is an alternative to shiny-proxy. In fact, it does not need any kind of shiny server. Each app simply relies on shiny. Each of the containers exposes a port (e.g. EXPOSE 3838) and are simply launched with runApp(".", host="0.0.0.0", port=3838). The scaling policies take care of starting and stopping containers as needed. The authentication logic is completely decoupled from the app code.
My cloud set-up is:
An Application Load Balancer (ALB) is used as the user entry point. You must use an HTTPS listener to set up authentication. I simply redirect HTTP traffic to HTTPS.
A Elastic Container Service (ECS) task+service for each app. This makes sure my apps are provisioned adequately and run completely independently. Each app can have an independent scaling policy, so each app has the right amount of resource for its traffic. You could even configure the apps to automatically start/stop to save a lot of resources. Obviously, the apps need to be private i.e. only accessible from the ALB.
Each ECS has a different ALB target group, so requests to app1.example.com get forwarded to app1, app2.example.com to app2, etc. This is all set up in the ALB rules. This is where we can easily add authentication.
I have a Cognito "user pool" with user accounts allowed to access the apps. This can be used to restrict access to the app at the traffic level rather than the application level.
In order to do that, you first need to create a client app in your Cognito user pool. For app1, I would create a Cognito client app using the 'authorization code grant' flow with openid scope and app1.example.com/oauth2/idpresponse as the callback URL.
Once this is done, you can simply go into the ALB rules and add authentication as a prerequisite for forwarding:
From now on, the traffic on app1.example.com must be authenticated before being forwarded to app1. Unauthenticated requests will be redirected to the Cognito Hosted UI (something like example.auth.eu-west-2.amazoncognito.com) to enter their credentials. You can customise what the hosted UI looks like in the Cognito settings.
Helpful links
For packaging R code in a container:
Rocker project and notes on extending an image
My personal Dockerfiles in particular shiny-base
For setting up Cognito authentication with an ALB:
Amazon documentation
Walk through: https://www.thorntech.com/2018/09/user-authentication-alb-cognito/ (which contains this video)
You can utilize AWS Cognito API to authenticate. I wrote a post about it here.
To make this answer self-contained, here are the details in short. Basically, what you need to do is to use this code in the global.r file of your app:
base_cognito_url <- "https://YOUR_DOMAIN.YOUR_AMAZON_REGION.amazoncognito.com/"
app_client_id <- "YOUR_APP_CLIENT_ID"
app_client_secret <- "YOUR_APP_CLIENT_SECRET"
redirect_uri <- "https://YOUR_APP/redirect_uri"
library(httr)
app <- oauth_app(appname = "my_shiny_app",
key = app_client_id,
secret = app_client_secret,
redirect_uri = redirect_uri)
cognito <- oauth_endpoint(authorize = "authorize",
access = "token",
base_url = paste0(base_cognito_url, "oauth2"))
retrieve_user_data <- function(user_code){
failed_token <- FALSE
# get the token
tryCatch({token_res <- oauth2.0_access_token(endpoint = cognito,
app = app,
code = user_code,
user_params = list(client_id = app_client_id,
grant_type = "authorization_code"),
use_basic_auth = TRUE)},
error = function(e){failed_token <<- TRUE})
# check result status, make sure token is valid and that the process did not fail
if (failed_token) {
return(NULL)
}
# The token did not fail, go ahead and use the token to retrieve user information
user_information <- GET(url = paste0(base_cognito_url, "oauth2/userInfo"),
add_headers(Authorization = paste("Bearer", token_res$access_token)))
return(content(user_information))
}
In the server.r you use it like this:
library(shiny)
library(shinyjs)
# define a tibble of allwed users (this can also be read from a local file or from a database)
allowed_users <- tibble(
user_email = c("user1#example.com",
"user2#example.com"))
function(input, output, session){
# initialize authenticated reactive values ----
# In addition to these three (auth, name, email)
# you can add additional reactive values here, if you want them to be based on the user which logged on, e.g. privileges.
user <- reactiveValues(auth = FALSE, # is the user authenticated or not
name = NULL, # user's name as stored and returned by cognito
email = NULL) # user's email as stored and returned by cognito
# get the url variables ----
observe({
query <- parseQueryString(session$clientData$url_search)
if (!("code" %in% names(query))){
# no code in the url variables means the user hasn't logged in yet
showElement("login")
} else {
current_user <- retrieve_user_data(query$code)
# if an error occurred during login
if (is.null(current_user)){
hideElement("login")
showElement("login_error_aws_flow")
showElement("submit_sign_out_div")
user$auth <- FALSE
} else {
# check if user is in allowed user list
# for more robustness, use stringr::str_to_lower to avoid case sensitivity
# i.e., (str_to_lower(current_user$email) %in% str_to_lower(allowed_users$user_email))
if (current_user$email %in% allowed_users$user_email){
hideElement("login")
showElement("login_confirmed")
showElement("submit_sign_out_div")
user$auth <- TRUE
user$email <- current_user$email
user$name <- current_user$name
# ==== User is valid, continue prep ====
# show the welcome box with user name
output$confirmed_login_name <-
renderText({
paste0("Hi there!, ",
user$name)
})
# ==== Put additional login dependent steps here (e.g. db read from source) ====
# ADD HERE YOUR REQUIRED LOGIC
# I personally like to select the first tab for the user to see, i.e.:
showTab("main_navigation", "content_tab_id", select = TRUE)
# (see the next chunk for how this tab is defined in terms of ui elements)
# ==== Finish loading and go to tab ====
} else {
# user not allowed. Only show sign-out, perhaps also show a login error message.
hideElement("login")
showElement("login_error_user")
showElement("submit_sign_out_div")
}
}
}
})
# This is where you will put your actual elements (the server side that is) ----
# For example:
output$some_plot <- renderPlot({
# *** THIS IS EXTREMELY IMPORTANT!!! ***
validate(need(user$auth, "No privileges to watch data. Please contact support."))
# since shinyjs is not safe for hiding content, make sure that any information is covered
# by the validate(...) expression as was specified.
# Rendered elements which were not preceded by a validate expression can be viewed in the html code (even if you use hideElement).
# only if user is confirmed the information will render (a plot in this case)
plot(cars)
})
}
And the ui.r looks like this:
library(shiny)
library(shinyjs)
fluidPage(
useShinyjs(), # to enable the show/hide of elements such as login and buttons
hidden( # this is how the logout button will like:
div(
id = "submit_sign_out_div",
a(id = "submit_sign_out",
"logout",
href = aws_auth_logout,
style = "color: black;
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
text-decoration: none;
background:#ff9999;
position: absolute;
top: 0px; left: 20px;
z-index: 10000;
padding: 5px 10px 5px 10px;"
)
)
),
navbarPage(
"Cognito auth example",
id = "main_navigation",
tabPanel(
"identification",
value = "login_tab_id",
h1("Login"),
div(
id = "login",
p("To login you must identify with a username and password"),
# This defines a login button which upon click will redirect to the AWS Cognito login page
a(id = "login_link",
"Click here to login",
href = aws_auth_redirect,
style = "color: black;
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
text-decoration: none;
background:#95c5ff;
padding: 5px 10px 5px 10px;")
),
hidden(div(
id = "login_error_aws_flow",
p("An error has occurred."),
p("Please contact support")
)),
hidden(
div(
id = "login_confirmed",
h3("User confirmed"),
fluidRow(
textOutput("confirmed_login_name")),
fluidRow(
p("Use the menu bar to navigate."),
p(
"Don't forget to logout when you want to close the system."
)
)
)
),
),
tabPanel("Your actual content",
value = "content_tab_id",
fluidRow(plotOutput("some_plot")))
)
)
Related
We have a R shiny based website where we input some parameters and get some results. We can do it manually, but considering many actions are repeat, we want write some R code to automatically pull out the results. Look looks we cannot use some simple REST request, because the HTTP request used some information like "nonce" and "session". Also I am not familiar with the JavaScript. Could someone help me to understand how to do it?
There is a functionality session$registerDataObj which allows you to add an API endpoint for a specific shiny session. This will make the provided data available at http://{host}/session/{session_id}/dataobj/{name}.
The minimal app below invokes the produced endpoint via browseURL(). See ?shiny::session for more details.
shinyApp(
ui = fluidPage(),
server = function(input, output, session) {
name <- "iris"
session$registerDataObj(
name = name,
data = iris,
filterFunc = function(data, req) {
httpResponse(content = paste('nrow: ', nrow(data)))
}
)
uri <- paste0("http://", session$request$HTTP_HOST, "/session/",
session$token, "/dataobj/", name)
browseURL(print(uri))
}
)
#> Listening on http://127.0.0.1:5130
#> [1] "http://127.0.0.1:5130/session/fd47ff29025bbe4d9dbf922b935186b3/dataobj/iris"
This should open up a new browser tab that shows: "nrow: 150". The return value of session$registerDataObj() can also be used to create the uri with additional query parameters.
But I personally think it is better to use a database in 99% of all usecases. This would mean that there is an export button in your ui which causes the app server to write all relevant results into the database and the database is then accessed by another process that needs the app data.
I'm building a Shiny dashboard to show a large amount of data. People access the dashboard through a separate login page (non-Shiny) that sits in front, at which point a JWT is generated and put in a cookie. I've managed to read in the cookie in Shiny and parse the data, saving the token in a variable called userPermissions. Now, however, I'd like to show/hide tabs in the dashboard depending on the user permissions.
For example: I have data on both managers and assistants. I want to show the manager data to any user with userPermissions == 'manager', and the assistant data to anyone with userPermissions == assistant.
I thought the best way would be to use conditionalPanel() – here's a (simplified) reproducible example:
library(shiny)
# UI file
ui <- fluidPage(
# JS to read cookie -- simplified to just return value!
tags$head(tags$script(
HTML('
Shiny.addCustomMessageHandler("goReadTheCookie", function (message) {
Shiny.onInputChange("cookie", "manager");
})
')
)
# Title
,titlePanel('Test')
# Navbar
,navbarPage(
id="navbar"
,conditionalPanel(condition = "userPermissions() == 'manager'",
mainPanel(textOutput('Manager data')))
,conditionalPanel(condition = "userPermissions() == 'assistant'",
mainPanel(textOutput('Assistant data')))
)
))
# Server file
server <- shinyServer(function(input, output,session){
## Grab permissions from Cookie
# Prepare output
userPermissions <- reactiveVal("")
# Tell JS to return cookie
session$sendCustomMessage(type="goReadTheCookie", message=list(name="cookie_name"))
# Listen for cookie
observeEvent(input$cookie,{
## -- Bunch of code omitted for sake of brevity -- ##
userPermissions("manager")
})
})
# Run app
shinyApp(ui=ui, server=server)
The problem with this code is that, in a browser console, I get the error Can't find variable: userPermissions.
I guess what's going on here is that the entire ui is executed, before JS can grab and return the cookie. What would be the best way to solve this?
Or, maybe I'm going about this the wrong way. I obviously need to verify the cookie server-side (i.e., in R) not to divulge the secret; and preferably this check, and the hiding/showing is completed at the very start of the Shiny application (userPermissions won't change during the session). Maybe there's a different (& better) solution to get to that point?
Any help would be very much appreciated!
In the end I found that the function appendTab does exactly what I was looking for. This needs to be run in server.R though, within the function to look for the cookie (otherwise userPermissions indeed doesn't exist). I could then do:
appendTab("navbar" # the id of the navigation bar created in ui.R
,tabPanel("tab name"
,mainPanel(...))
)
where tabPanel(...) could be anything you'd normally put in ui.R.
The added benefit here is that hidden tabs are also not available in the HTML source, as they're never even passed from the server to the client!
I have a large shiny application used by multiple users and I use the shinymanager package to manage the credentials. Some users requested to have the user interface translated based on their own language.The desired languages (depending on the user) are: English, French and Dutch.
Only the UI components need a translation. I was wondering whether there would be a way to store language preferences in the sqlite credential database of the shinyapp package and then have all the UI translated based on that value, see a minimal example below to illustrate. The shinyadmin should be able to create a new user via the admin page and select language...
credentials <- data.frame(
user = c("shiny", "shinymanager"),
password = c("azerty", "12345"),
language = c("en","fr") # example.
stringsAsFactors = FALSE
)
Based on this how could I translate my UI components effectively ? (preferably keep the translation files in a distinct script/Json file or whatever.
Note that here I am not concerned about the shinymanager login page (where the user is not logged yet, therefore his language preference cannot be specified via the sqlite database).
[EDIT]
# server.R
shinyServer(function(input, output, session){
#### Credential login ####
res_auth <- secure_server(
check_credentials = check_credentials(sqlite_path, passphrase = "password")
)
output$lang <- renderPrint({
reactiveValuesToList(res_auth)$language # see the credential file above
})
Basically I want the UI below to change "This is a tab panel" to "Ceci est un onglet" if output$lang is == "fr". The difficulty I have here is with respect to the reactivity and server evaluation process. I would ideally avoid to have server-evaluated UI (i.e. with uiOutput & renderUi), as this increases the complexity of the code and the language does not change once the UI is initiated after login.
userinterface <- navbarPage('Dashboard', id="page", collapsible=TRUE, inverse=FALSE,
tabPanel("This is a tab panel", module.ui("mymoduleui")) #some ui in module.ui
)
ui <- secure_app(userinterface,
enable_admin = TRUE)
Is there a simple way for me to create a log-in page for my app? I want to render a ui with multiple tabs (ie. fluidPage(navbarPage(tabPanel("tab1", ...), tabPanel("tab2", ...),...)) conditional upon successful user authentication. I initiate a database connection with an R6 object, if it fails, I print out an "error" and the user can retry entering their credentials. If the connection is successful, I'd like to render the ui (ui2) I specify above.
ui <- fluidPage(fluidRow(column(width=12, textInput('uid', label =
"Enter your user id:"), passwordInput("pwd",
label = "Enter your password:"), actionButton("login",
"Login"), uiOutput("resulting_ui"), offset = 5)))
If you already set up a database that can hold usernames and passwords, you can have users enter their credentials, compare them to the ones in the database, and change a value of a reactiveVal() based on that comparison. The value of that reactiveVal() will control whether you show your UI or not.
For example you can use something like this:
logged <- reactiveVal(value = F)
# change the value of logged() when a user enters correct credentials
output$your_ui <- renderUI({
req(logged())
# your ui here...
})
For a more elaborated example take a look here:
https://github.com/yanirmor/shiny-user-management
Starting Shiny app after password input
This did mostly resolve my question. I just had to make some minor changes to get it to work.
I'd like to limit the number of users of my locally hosted r shiny app to one user at any one time.
So ideally when a second user attempted to run the app at the same time (users access the app by typing the local IP into the address field) the app would display a default message and stop any further progress. Nullifying any other user commands may not matter if the only thing shown upon entry is this denial message.
The content of the app doesn't matter so we can use this app as an example: http://shiny.rstudio.com/gallery/tabsets.html
Thanks for any help or info you can give.
I wouldn't recommend doing this, I think it's very dangerous, but there are ways you could hack this together. Here's one solution (as I said, it's hacky and I wouldn't do it myself). The basic idea is to have a global variable that keeps track of whether or not someone is using the app. If nobody is using the app, show the app and turn on the flag and make sure to turn off the flag when the user exits.
shinyBusy <- FALSE
runApp(shinyApp(
ui = fluidPage(
shinyjs::useShinyjs(),
shinyjs::hidden(
h1(id = "busyMsg", "App is busy")
),
shinyjs::hidden(
div(
id = "app",
p("Hello!")
)
)
),
server = function(input, output, session) {
if (shinyBusy) {
shinyjs::show("busyMsg")
} else {
shinyBusy <<- TRUE
session$onSessionEnded(function() shinyBusy <<- FALSE)
shinyjs::show("app")
}
}
),
launch.browser = TRUE)
Note: in order to show/hide app elements, I'm using a package that I wrote shinyjs