Using Cookie Based Authentication with Shiny

Oct 3, 2017 00:00 · 1286 words · 7 minutes read R Shiny Shiny-Server

Introduction

Authentication is one of the features the open source version of shiny-server is missing. The simplest way is to set up a proxy and let it handle the user authentication. But in some scenarios, this isn’t sufficient as you are not able to determine in shiny who is the current user and thus are e.g. unable to apply an authorization scheme.

One possible solution for this problem is a cookie based authentication. After a short explanation of the underlying concept, the various implementation steps are shown and a complete working example is given.

The Authentication Process (Step 4)

To keep it simple, this example has no database backed. Usually all user credentials would be stored in a database, instead the hashed password is stored in the variable password_hash. You should never store passwords as clear text and only as a cryptographic hash, which is infeasible to invert. The bcrypt package is an easy way to hash the password and to validate it.

First, let’s generate a password hash for the very secure password secret123:

library(bcrypt)
password_hash <- hashpw('secret123') 
print(password_hash)
## [1] "$2a$12$6e5s/KFmn96fN0jkZ7FjEu1YSMJ8NketA.y2Cun79DRpOViKBo2KK"

In a second step we can use check_pw to compare the given clear text password to the password hash which usually is stored in a database:

checkpw(password = 'secret123', hash = password_hash)
## [1] TRUE

Using shinyjs to Set and Get Cookies

For the steps 5.2 and 6 we need to set and receive cookies, which can be done with some lines of javascript code. An convenient way therefor are the js-cookie functions. The javascript code needs to be downloaded to the www/ folder:

if (!dir.exists('www/')) {
  dir.create('www')
}

download.file(
  url = 'https://raw.githubusercontent.com/js-cookie/js-cookie/master/src/js.cookie.js',
  destfile = 'www/js.cookies.js'
)

To interact with these javascript functions, we use Dean Atali’s shinyjs. First we need some wrappers around the functions to let shiny know what’s happening. These following lines of code are stored in the variable jsCode.

  shinyjs.getcookie = function(params) {
    var cookie = Cookies.get("id");
    if (typeof cookie !== "undefined") {
      Shiny.onInputChange("jscookie", cookie);
    } else {
      var cookie = "";
      Shiny.onInputChange("jscookie", cookie);
    }
  }
  shinyjs.setcookie = function(params) {
    /* expires after 12 hours  */
    Cookies.set("id", escape(params), { expires: 0.5 }); 
    Shiny.onInputChange("jscookie", params);
  }
  shinyjs.rmcookie = function(params) {
    Cookies.remove("id");
    Shiny.onInputChange("jscookie", "");
  }

Thanks to Shiny.onInputChange("jscookie", cookie); the cookie’s value is accessible in shiny through input$jscookie.

Second, we need to:

  1. source the javascript cookie code which we copied to the www/ folder,
  2. activate shinyjs and
  3. extend shinyjs with our cookie wrapper from above.
ui <- fluidPage(
  tags$head(
    tags$script(src = "js.cookies.js") # (1)
  ),
  useShinyjs(), # (2)
  extendShinyjs(text = jsCode), # (3)
  [...]
)

Login Process

When a users hits the login button, the password is checked and if correct, an id for the session is generated. This session id is stored in the database (server side) and via a cookie in the users browser. Subsequent requests compare the cookie’s value to the corresponding value from the database.

The following code

  1. checks the password and
  2. sets the cookie.

For simplification purposes, the session id isn’t stored in a database and instead hard coded as sessionid in the beginning of the script. Besides that, an additional cookie with the username would be helpful.

  observeEvent(input$login, {
    if (input$username == 'admin' & 
        checkpw(input$password, hash = password_hash)) { # (1)
      # generate a sessionid and store it in the database:
      sessionid <- paste(
        collapse = '', 
        sample(x = c(letters, LETTERS, 0:9), size = 64, replace = TRUE)
      )
      # but we keep it simple in this example...
      js$setcookie(sessionid) # (2)
    } else {
      status('out, cause you don\'t know the password secret123 for user admin.')
    }
  })

Check if the User is Logged in (Step 6)

To determine the login status of a user, we observe if a cookie has been set and if it is identical to the user’s sessionid:

  status <- reactiveVal(value = NULL)
  # check if a cookie is present and matching our super random sessionid  
  observe({
    js$getcookie()
    if (!is.null(input$jscookie) && 
        input$jscookie == sessionid) {
          status(paste0('in with sessionid ', input$jscookie))
    }
    else {
      status('out')
    }
  })

Complete Example

Copy the code below and save it as app.R:

library(shiny)
library(shinyjs)
library(bcrypt)


# This would usually come from your user database.

# Never store passwords as clear text
password_hash <- hashpw('secret123') 

# Our not so random sessionid
# sessionid <- paste(
#   collapse = '', 
#   sample(x = c(letters, LETTERS, 0:9), size = 64, replace = TRUE)
# )
sessionid <- "OQGYIrpOvV3KnOpBSPgOhqGxz2dE5A9IpKhP6Dy2kd7xIQhLjwYzskn9mIhRAVHo" 


jsCode <- '
  shinyjs.getcookie = function(params) {
    var cookie = Cookies.get("id");
    if (typeof cookie !== "undefined") {
      Shiny.onInputChange("jscookie", cookie);
    } else {
      var cookie = "";
      Shiny.onInputChange("jscookie", cookie);
    }
  }
  shinyjs.setcookie = function(params) {
    Cookies.set("id", escape(params), { expires: 0.5 });  
    Shiny.onInputChange("jscookie", params);
  }
  shinyjs.rmcookie = function(params) {
    Cookies.remove("id");
    Shiny.onInputChange("jscookie", "");
  }
'

server <- function(input, output) {

  status <- reactiveVal(value = NULL)
  # check if a cookie is present and matching our super random sessionid  
  observe({
    js$getcookie()
    if (!is.null(input$jscookie) && 
        input$jscookie == sessionid) {
          status(paste0('in with sessionid ', input$jscookie))
    }
    else {
      status('out')
    }
  })

  observeEvent(input$login, {
    if (input$username == 'admin' & 
        checkpw(input$password, hash = password_hash)) {
      # generate a sessionid and store it in your database,
      # sessionid <- paste(
      #   collapse = '', 
      #   sample(x = c(letters, LETTERS, 0:9), size = 64, replace = TRUE)
      # )
      # but we keep it simple in this example...
      js$setcookie(sessionid)
    } else {
      status('out, cause you don\'t know the password secret123 for user admin.')
    }
  })
  
  observeEvent(input$logout, {
    status('out')
    js$rmcookie()
  })
  
  output$output <- renderText({
    paste0('You are logged ', status())}
  )
}

ui <- fluidPage(
  tags$head(
    tags$script(src = "js.cookies.js")
  ),
  useShinyjs(),
  extendShinyjs(text = jsCode),
  sidebarLayout(
    sidebarPanel(
      textInput('username', 'User', placeholder = 'admin'),
      passwordInput('password', 'Password', placeholder = 'secret123'),
      actionButton('login', 'Login'),
      actionButton('logout', 'Logout')
    ),
    mainPanel(
      verbatimTextOutput('output')
    )
  )
)

shinyApp(ui = ui, server = server)