Unleashing the Power of httpOnly Cookies in R Shiny Applications

A Comprehensive Guide

Published on February 20th, 2024
Written by Xavier Escribà Montagut, Software engineer

 13 min read

What's In Store?

Welcome to an exciting exploration of a topic that often flies under the radar but is crucial for enhancing your R Shiny applications—httpOnly Cookies. 

In today’s dynamic digital environment, security and user experience are paramount. That’s why we’re taking the time to guide you through the ins and outs of using httpOnly Cookies in R Shiny.

Before we dive deep into the mechanics of setting cookies in R Shiny, let’s take a look at the roadmap for today’s discussion. We have curated this comprehensive guide to walk you through each pivotal stage of the process, helping you understand not just the “how,” but also the “why” behind cookies.

  1. Crafting Your Own Router in Shiny for Cookie Management: A step-by-step guide to creating a custom router in Shiny, from setting up your uiPattern to crafting the UI logic.
  2. Setting a Cookie Endpoint: The keystone for setting httpOnly cookies on an R shiny application, and how to code it.
  3. Sending Cookie Value as a Request Header: The Client-Server Dance. Learn how to send a cookie value as a request header, from the client to the server, using JavaScript and R.
  4. The Importance of Encrypting Cookie Values: Safeguarding User Sessions. An exploration into the importance of encrypting cookie values and how to do it, followed by the methods to decrypt these values safely.
  5. Logging Out: How to Remove a Cookie in Omics Playground. Finally, we’ll discuss how to effectively remove cookies during events like user logout or session expiration.

By the end of this post, you won’t just have theoretical knowledge; you’ll be armed with actionable insights that you can apply directly to your R Shiny applications.

We’re thrilled to announce that the techniques and principles outlined in this blog post aren’t just theoretical—they’re directly applicable through a new feature in our very own Omics Playground. We understand that continually re-authenticating can disrupt your workflow and compromise the user experience. That’s why we’ve integrated secure, persistent user sessions using httpOnly cookies right into the platform. 

Now, you can enjoy a more streamlined, secure, and user-friendly experience without the need to authenticate every single time you use our application. It’s all part of our commitment to providing you with the most efficient and secure bioinformatics solutions.

Setting httpOnly cookies isn’t just a feature—it’s a necessity for creating secure and reliable R Shiny applications. These cookies provide an added layer of security by ensuring that the cookie data is not accessible through client-side scripts, reducing the risk of cross-site scripting attacks. Yet, despite the critical role they play, there’s a striking absence of resources and tutorials available online that cover how to implement httpOnly cookies in R Shiny. We were astonished to find this gap in available information, especially given the security advantages that come with utilizing them. This blog post aims to bridge that gap by providing you with the knowledge and tools you need to bolster your application’s security and user experience.

1. Crafting a Custom Router in Shiny for Cookie Management

What is a Router and Why is it Important?

In the context of a web application, a router serves as a traffic director, channeling client requests to specific handlers based on the route—or the path specified in the URL. Think of it as a virtual roadmap that decides what content or page to display when a certain URL is accessed. A router can also control the flow of data, ensuring that the right processes occur in the appropriate order, which is especially important for tasks like authentication, data validation, and yes, cookie management.

Why is a Custom Router Crucial for Cookie Management, You Ask?

You might be wondering, why go through the trouble of crafting a custom router when there are built-in routers available? Well, when it comes to cookie management, especially with the added layer of httpOnly cookies, a generic router simply won’t cut it. In our implementation, the custom router is not just a pathway but also the mechanism that sets the cookie.

Imagine trying to use a key for a lock that doesn’t quite fit. Sure, you might be able to turn it a bit, but it won’t serve its primary function—to lock or unlock. Similarly, a custom router in Shiny allows you to define specialized rules and conditions for setting your cookies, ensuring that they are implemented securely and effectively.

By crafting a custom router, you gain the flexibility to determine exactly how and when the httpOnly cookies are set, enabling more granular control over the cookie management process. This is vital for ensuring that cookies serve their intended function without compromising security or usability.

Incorporating a custom router for cookie management is a leap forward in creating a tailored, secure, and efficient application. And this isn’t just theoretical advice; it’s a feature we’ve passionately integrated into Omics Playground to ensure that you enjoy secure, persistent user sessions. By doing so, we’ve made it seamless for you to engage with our platform without the recurrent hassle of re-authentication. Stay with us, as we’ll soon guide you through the process of implementing this powerful feature into your own R Shiny applications.

Creating a Custom Router in Shiny

Creating your own custom router in Shiny involves a series of well-structured steps, but before diving into the code, it’s essential to understand a key component—running your application using shiny::shinyApp.

This function serves as the bedrock of our custom routing because it contains an argument known as uiPattern. The uiPattern parameter allows our application to listen at different routes. For instance, with this capability, different sections or functionalities of your app can be accessed via unique URLs like myshinyapp.com/end1.

In our example, we’ll set uiPattern to '.*', which in essence tells our application that we’re open to listening at any route. Now, you might wonder, “Does this mean my application will respond to any random route thrown at it?” Not quite! While uiPattern = '.*' opens up the possibility of using any route, we will still exercise control through the UI to determine which routes we are actually interested in and how they should be handled.

The real magic happens when you marry this uiPattern flexibility with a UI that filters and directs incoming routes to their respective destinations. By configuring the UI to listen to specific routes, you can allocate resources, render pages, or even set cookies—like we do in Omics Playground—based on the route being accessed.

So, in summary, setting uiPattern = '.*' provides you with a blank canvas, and your UI logic serves as the paintbrush, helping you create a masterpiece of an application that routes exactly as you desire.

Below is a minimal example of a Shiny app that demonstrates the use of the uiPattern argument to serve two different UIs. This Shiny app will listen for two routes: / and /page2, each serving a different UI.

library(shiny)

# Define the UI for Page 1
ui_page1 <- fluidPage(
  titlePanel("This is Page 1"),
  sidebarLayout(
    sidebarPanel(
      h3("Page 1 Sidebar")
    ),
    mainPanel(
      h3("Page 1 Main Panel")
    )
  )
)

# Define the UI for Page 2
ui_page2 <- fluidPage(
  titlePanel("This is Page 2"),
  sidebarLayout(
    sidebarPanel(
      h3("Page 2 Sidebar")
    ),
    mainPanel(
      h3("Page 2 Main Panel")
    )
  )
)

# Define custom UI function
custom_ui <- function(req) {
  if (req$PATH_INFO == '/') { # Here we serve `/` petitions
    return(ui_page1)
  } else if (req$PATH_INFO == '/page2') { # Here we serve `/page2` petitions
    return(ui_page2)
  } else {
    return("404: Page not found")
  }
}

# Main server logic
server <- function(input, output, session) {
  # Server logic here
}

# Shiny App with custom router logic
shinyApp(
  ui = custom_ui,
  server = server,
  uiPattern = '.*'
)

If we navigate to / or /page2 we can see how indeed we are serving different pages:

2. Setting a Cookie Endpoint: The Key to Persistent User Sessions

Now that we’ve tackled the challenge of custom routing in Shiny, it’s time to turn our focus towards setting up a cookie endpoint. This is the crucial piece that allows us to leverage httpOnly cookies for secure, persistent user sessions, just like we’ve done in Omics Playground.

The Code: Cookie Setting Endpoint

To illustrate how to create a cookie endpoint, let’s take a look at some actual code:

if (identical("/cookie", x$PATH_INFO)) {
  value <- x$HTTP_HEADER_USER_COOKIE
  return(cookies::set_cookie_response(
    cookie_name = "persistentOPG",
    cookie_value = value,
    http_only = TRUE,
    secure_only = TRUE,
    redirect = "/close",
    same_site = "Strict"
  ))
}

This code has to be placed as a new UI endpoint, which instead of displaying an actual webpage, will set a cookie. Note that the cookie value is being passed as a request header, we will now see how to achieve that.

3. Sending Cookie Value as a Request Header: The Client-Server Dance

While setting up an endpoint to handle the cookie is a significant piece of the puzzle, it isn’t the whole picture. To fully realize the capability of httpOnly cookies in a Shiny application, we also need a way to send the cookie value as a request header from the client to the server. This is precisely what we’ve achieved in Omics Playground, and we’ll show you how it’s done.

JavaScript Code: Client-Side Logic

On the client side, you’ll use JavaScript to send an asynchronous HTTP request, setting the cookie value in the request header. Here’s the code snippet for it:

Shiny.addCustomMessageHandler('redirect', function(message) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/cookie", true);
  xhr.setRequestHeader("Header-User-Cookie", message);
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200)
    window.location = message;
  };
  xhr.send();
});

Here’s what happens in the JavaScript function: Upon receiving the message from the server, a new XMLHttpRequest object is created. This object opens a GET request to our /cookie endpoint. Crucially, it sets a custom HTTP header called “Header-User-Cookie” with the value passed in the message (i.e., the encrypted email). Once the request is ready and successful (status 200), the browser is redirected using window.location = message;.

R Server Code: Triggering the JavaScript Function

On the server side, in your Shiny app, you can trigger the JavaScript function to run by sending a custom message. Here’s how:

session$sendCustomMessage(type = 'redirect', message = email_encrypted_value)

By combining these two pieces of code, we have effectively set up a mechanism for securely setting httpOnly cookies via an encrypted value sent from the server to the client and back again. Just like we’ve done in Omics Playground, this methodology enables you to provide secure, persistent user sessions without necessitating frequent re-authentication.

4. The Importance of Encrypting Cookie Values: Safeguarding User Sessions

When it comes to web security, every detail matters, especially when you’re handling sensitive information like user identification or authentication tokens. In our Omics Playground application, we authenticate users by their email addresses. While this is a straightforward and user-friendly method, it presents a security challenge: email addresses are relatively easy to obtain or guess, and if we stored them plainly in cookies, they could become a target for session forgery attacks.

Imagine a scenario where an attacker creates a cookie with someone else’s email address. They could then impersonate that user and gain unauthorized access to their account, compromising the integrity and confidentiality of the data. This would be disastrous both for the user and for the trustworthiness of our application.

To mitigate this risk, we encrypt the email value that’s stored in the cookie. This isn’t just any encryption, but one that’s performed server-side using a key that’s not exposed to the end-users. By doing so, even if someone were able to access the cookie, the encrypted value would be meaningless without the server-side key to decrypt it.

Thus, encryption acts as an additional layer of security, effectively barricading the door against session forgery. In Omics Playground, this ensures that our users can enjoy a secure, streamlined experience without the nagging worry of potential session hijacking. We believe that taking these extra steps in security protocols underlines our commitment to offering not just a powerful analytical tool, but also a safe environment for our users.

Encryption in Omics Playground: How We Secure Your Session

When it comes to ensuring the security of persistent sessions in Omics Playground, we leave no stone unturned. Our encryption method is robust, taking into account both the confidentiality and integrity of the data being stored as cookies. Let’s dig into the code snippet that performs this crucial task:

key_base64 <- readLines(paste0(OPG, "/etc/keys/cookie.txt"))[1]
passkey <- sodium::sha256(charToRaw(key_base64))
plaintext <- cred$email
plaintext.raw <- serialize(plaintext, NULL)
ciphertext <- sodium::data_encrypt(plaintext.raw, key = passkey)

email_encrypted_nonce <- paste(as.character(attr(ciphertext, "nonce")), collapse = "")
email_encrypted_value <- paste(as.character(ciphertext), collapse = "")

The code begins by reading a base64-encoded encryption key stored in a secure location (/etc/keys/cookie.txt). This key is then hashed using the SHA-256 algorithm via the sodium package, generating a passkey that will be used for the encryption process.

Next, we take the email credential (cred$email) and serialize it, converting the plaintext email into a raw vector format. This serialized data is then encrypted using the previously generated passkey.

The encryption process creates two important attributes: the “nonce” and the encrypted “value”. The nonce, or “number used once” ensures that each encryption is unique, even if the same email and key are used. The encrypted “value” is the actual encrypted email data.

Storing Two Cookies for Session Persistence

Due to the creation of both a nonce and an encrypted value, we actually set two separate cookies in Omics Playground to manage sessions securely. One cookie stores the encrypted “value,” and the other stores the “nonce.” This dual-cookie strategy adds an extra layer of security, making it even more challenging for malicious actors to forge a session.

With this method, Omics Playground not only ensures the confidentiality of your session but also robustly guards against any tampering or forgery attempts. Our aim is to provide you with a platform where you can focus on your analyses, confident in the knowledge that your session is secure and persistent.

Decryption: Reversing the Process to Retrieve the Original Value

We’ve talked about how to encrypt and securely store user emails as cookies for session persistence in Omics Playground. But the encryption process should be reversible—after all, we need to retrieve the original email address for user authentication during subsequent sessions. Here, we’ll discuss how we decrypt the cookie value to obtain the original email.

The Code: Decrypting the Cookie

key_base64 <- readLines(paste0(OPG, "/etc/keys/cookie.txt"))[1]
email_nonce_raw <- sodium::hex2bin(nonce)
email_raw <- sodium::hex2bin(cookie)
attr(email_raw, "nonce") <- email_nonce_raw
# Return NULL if unsuccessful decryption
email <- tryCatch({
  unserialize(
    sodium::data_decrypt(
      email_raw,
      key = sodium::sha256(charToRaw(key_base64))
    )
  )
}, error = function(w){
  NULL
})

Much like the encryption process, decryption also starts by reading the base64-encoded key from a secure location. The encrypted cookie and its associated nonce (both stored in hex format) are then converted back to their binary representation using sodium::hex2bin().

We then attach the nonce back to the encrypted email, as it is a crucial component for the decryption process.

The real decryption happens in the tryCatch block. Here, we use the sodium::data_decrypt function along with the SHA-256 hashed key_base64 to decrypt the binary email data. Once decrypted, the data is unserialized to retrieve the original email address. If any error occurs during the decryption process, we gracefully handle it by returning NULL.

Understanding how to correctly decrypt the encrypted email is as crucial as the encryption process itself. This ensures a seamless and secure user experience during both login and subsequent sessions. In Omics Playground, this reversible encryption-decryption process forms the bedrock of our secure, persistent user sessions.

And with that, you now have a complete understanding of how Omics Playground uses encryption and decryption to manage httpOnly cookies in R Shiny applications. Whether you’re a user enjoying the seamless experience or a developer aiming to implement similar features, you can appreciate the level of thought and detail that has gone into securing your data and session.

5. Logging Out: How to Remove a Cookie in Omics Playground

Now that we’ve covered how to create a secure, persistent session by setting encrypted cookies, it’s crucial to discuss the other end of the lifecycle: how to remove these cookies. In scenarios like user logout or session expiration, it’s important to know how to effectively clear the cookies. This ensures not only that the session is terminated but also that the user data remains secure.

The Code: Removing a Cookie

To complete our journey on session management, let’s look at the code that handles the removal of a cookie in Omics Playground:

} else if (identical("/cookie_remove", x$PATH_INFO)) {
  return(cookies::set_cookie_response(
    cookie_name = "persistentOPG",
    cookie_value = "",
    expiration = -1,
    http_only = TRUE,
    secure_only = TRUE,
    redirect = "/close"
  ))
}

The code snippet listens for a route named /cookie_remove. Upon triggering this route, it invokes the set_cookie_response function from the cookies package. However, this time, we set the cookie_value to an empty string and set the expiration to -1. These parameters effectively invalidate the cookie immediately, rendering it useless for future sessions.

By incorporating this logout functionality, Omics Playground ensures that users can not only initiate secure, persistent sessions but also end them safely when required. So, while you focus on your important analytical work on our platform, rest assured that we’ve got your session security covered from start to finish. 

Wrap up

In today’s digital world, securing user data and sessions is not just an added feature—it’s a necessity. Through this comprehensive guide, we’ve walked you through how Omics Playground leverages httpOnly cookies in R Shiny to provide you with a secure and persistent user experience.

From crafting a custom router in Shiny for cookie management to understanding the critical importance of httpOnly cookies for security, we’ve covered it all. We’ve dived deep into the code, showing you how to set, encrypt, decrypt, and even remove cookies.

The features discussed in this post are not just theoretical musings; they are practical, real-world implementations that you can experience right now in Omics Playground. We are committed to continually enhancing user security and convenience, making it easier for you to manage and analyze your data effectively.

Thank you for taking the time to read through this guide. We hope it has provided valuable insights into how seriously we take your security at Omics Playground. So go ahead, log in or sign up, and experience a secure, seamless analytical journey like never before!

See you in Omics Playground! 

Unlock the full potential of your RNA-seq and proteomics data.

About the Author

Xavier Escribà Montagut

Xavier Escribà Montagut is a bioinformatician at the Barcelona Institute for Global Health (ISGlobal). He has a master’s degree in science and engineering and a PhD in bioinformatics. Xavier is currently working as a Software Engineer Consultant at BigOmics Analytics.