Skip to Content
Course content

89: Authentication in Shiny Apps

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this mistake more times than I can count. A developer wants to protect a dashboard, so they create a simple "logged in" flag. It looks like it works during their local testing, but the moment they deploy it to a server with more than one user, they've accidentally created a massive security hole.

# WARNING: This code is fundamentally broken
is_authenticated <- FALSE # Global variable

ui <- fluidPage(
  uiOutput("main_ui")
)

server <- function(input, output, session) {
  output$main_ui <- renderUI({
    if (is_authenticated) {
      h1("Welcome to the Secret Executive Dashboard")
    } else {
      div(
        textInput("user", "Username"),
        passwordInput("pw", "Password"),
        actionButton("login", "Log In")
      )
    }
  })

  observeEvent(input$login, {
    # Using super-assignment to update the global variable
    if (input$user == "admin" & input$pw == "password123") {
      is_authenticated <<- TRUE 
    }
  })
}

shinyApp(ui, server)

The Global State Trap

At first glance, this looks logical. You have a variable, you check it, and you update it when the button is clicked. But look closely at where is_authenticated is defined. It's sitting outside the server function.

In Shiny, the server function is executed once for every single user who connects to the app. However, any variable defined outside that function is shared across the entire R process. By using the super-assignment operator (<<-), you aren't just logging in one user; you are flipping a switch for every person currently using the app. If User A logs in successfully, User B—who might have just opened the app—will suddenly see the "Secret Executive Dashboard" without ever entering a password. That's a catastrophic failure in any production environment.

Encapsulating State in the Server Session

To fix this, we need to move the authentication state inside the server function. This ensures that the variable is created in a fresh environment for every new session. I also recommend using a reactiveVal instead of a standard variable, because renderUI needs a reactive trigger to know when to redraw the screen.

ui <- fluidPage(
  uiOutput("main_ui")
)

server <- function(input, output, session) {
  # State is now local to this specific user session
  authenticated <- reactiveVal(FALSE)

  output$main_ui <- renderUI({
    # renderUI will now automatically re-run whenever authenticated() changes
    if (authenticated()) {
      h1("Welcome to the Secret Executive Dashboard")
    } else {
      div(
        textInput("user", "Username"),
        passwordInput("pw", "Password"),
        actionButton("login", "Log In")
      )
    }
  })

  observeEvent(input$login, {
    if (input$user == "admin" & input$pw == "password123") {
      authenticated(TRUE)
    } else {
      showNotification("Invalid credentials", type = "error")
    }
  })
}

shinyApp(ui, server)

Now, when User A logs in, authenticated(TRUE) only happens within their specific session memory. User B's session remains FALSE. It's a simple shift in where the variable lives, but it's the difference between a secure app and a liability.

Moving Beyond Basic Logic

While the logic above solves the "shared state" bug, you shouldn't manually write password checks for a serious app. Hard-coding credentials in your script is a huge no-no (they'll end up in your git history), and simple if statements don't handle session timeouts or encrypted passwords.

For real-world projects, I suggest looking at packages like shinymanager or shinyauthr. They handle the "boilerplate" of authentication—like database lookups, password hashing, and session cookies—so you can focus on the actual data visualization. If you're deploying to a corporate environment, you'll likely be using OAuth or Active Directory, which usually happens at the server level (like ShinyProxy or Posit Connect) rather than inside the R code itself.




📋 Practical Task

Build a Session-Aware Gatekeeper for Sensitive Data

Your goal is to create a Shiny app that protects a "Financial Report" tab. You must ensure that the authentication state is session-specific and that the user cannot access the report without the correct credentials.

  • Create a uiOutput that switches between a login screen and the main content.
  • Implement a reactiveVal inside the server function to track the login status.
  • The login should only succeed if the username is "analyst" and the password is "secure2024".
  • If the login fails, use showNotification() to alert the user.
  • Once logged in, display a table (you can use head(mtcars) as a placeholder for the "Financial Report") and a "Logout" button that sets the authentication state back to FALSE.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.