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 am trying to build a shiny app where the user can copy and paste a web address as text input, and the app will then read in the web data from the address they provided. I tried using the textinput to let user enter their web address, and then using the read_html function in rvset to read in the web data.
On the ui side I have something like this:
conditionalPanel(
condition = "input.Articlesource == 'Web page'",
textInput("fileRequest", "Copy and paste link to aritcle here")
)
)
On the server side:
article <- reactive({
if (input$Articlesource == "Web page") {
read_html(input$fileRequest)
}
})
I don't think it reads anything in like this. I just need to read the web data in and then do some further analysis on it. And no, I do not have a specific website I want to use. Basically I want the user to be able to enter whatever they want. Anyone with experience on something like this?
I'm wondering if there's any way I can access user log for my Shiny App.
Currently, I use the code below to get who logged in and when did this person log off.
However, I'd prefer to know the time the user logged in so that I'm able to know how long does the user using the app.
session$onSessionEnded(function(){
UserInfo <- data.frame(
LoginName = session$user,
Time = as.character(Sys.time())
)
Plus, I understand that Google Analytics can easily access this kind of info, but I do prefer a 'shiny' way to solve it.
I've also tried to use `session$clientData' as the document says it's used for "Getting Non-Input Data From the Client", but I don't know how could I get the login time.
Does anyone have any idea on how could I achieve this? Thanks in advance!
Per the shiny scoping rules, everything inside server <- function(input, output, session) is per-session.
server <- function(input, output, session) {
# everything in here is run once per-session, so it should run as soon as
# a user starts using the app
started <- Sys.time()
# ... reactives here ...
session$onSessionEnded(function() {
UserInfo <- data.frame(
LoginName = session$user,
Time = as.character(Sys.time())
)
# ... do something with UserInfo ...
})
In fact, the scoping rules suggest exactly this, but they named it startTime instead. (Hard things: cache invalidation and variable naming.)
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")))
)
)
I asked this question on the Shiny user group, but haven't been able to get a response, so I'm posting it here as well.
I have an app that needs to track a user's preference, with the possibility that several users may be using the app simultaneously. For simplicity, let's say I have a list to contain a user's settings stored within the shiny server function. It's a long list (1000 elements) that contains attributes based on the user's interaction with the app. The user can in effect change any index of this list to one of hundreds of possible settings. My initial solution was something like this:
shinyServer(function(input, output, session) {
settings <<- rep("A",1000)
observe({
input$changeSettingsButton
settings[input$changeIndex] <<- input$newSetting
})
}
Which works great unless you have multiple people using the app at the same time, because the <<- creates a global, shared variable across sessions. Is there a good way to do this?
You can use reactiveValues to store persistent user info. Something like
shinyServer(function(input, output, session) {
myReactives <- reactiveValues(settings = rep("A",1000))
observe({
input$changeSettingsButton
isolate(myReactives$settings[input$changeIndex] <- input$newSetting)
})
}
may work for you.