This is a first look into tower, an R package dedicated to make it convenient to write custom middlewares directly into Shiny.
Tower was born with the purpose to help us implement authorization flows directly into our Shiny Apps, however it has been used for many other purposes.
In this post we will cover how to get started with tower and how to implement a very simple authorization flow with HTTP cookies.
Tower works by taking a Shiny app object and breaking it down into a mutable structure to which we can add layers. A layer is just a function that takes in a request environment object and returns either an HTTP response or NULL. If a layer returns a response it short circuits and takes the response back to the user, if it returns NULL then it goes on to the next layer until it reaches a response.
Tower also includes some helper functions to build HTTP responses more easily, we will take a look into that later in the post.
For this post we will be building a very simple (not secure in any sense of the word) authorization workflow. Basically, the user will have to access a super secret URL before being allowed to use the application.
For example, if the user tries to enter the app without previous authorization then they should get some sort of “Not authenticated” page. Once the user enter http://localhost:3838/secret_entry_page, then they should be able to use the app.
To achieve this we will use HTTP cookies and build our service directly into our Shiny app using tower.
Let’s start by building a very minimal Shiny App. This is the code for the app we will build on top of.
library(shiny)
ui <- fluidPage(
"This is a secret Shiny app"
)
server <- function(input, output, session) {}
shinyApp(ui, server)
We want the user to not be able to use the app unless they have some sort of key. For this example, we will hard-code it into our app.
SECRET <- "SUPER_SECURE_PASSPHRASE"
Now we will need to build a middleware layer that checks the user’s cookies and checks if it has the secret. This function will take a request object and continue the flow if the secrets match, if not it will return a 401 HTTP response.
http_check_secret_layer <- function(req) {
cookies <- tower::req_cookies()
if (identical(cookies$secret, SECRET)) {
return(NULL)
}
tower::response_builder() |>
tower::set_status(401) |>
tower::add_body("Not authorized") |>
tower::build_response()
}
Now in order to add this layer to our app we will need to take the app object and insert the layer into it. Now our code should look something like this:
shinyApp(ui, server) |>
tower::create_tower() |>
tower::add_http_layer(http_check_secret_layer) |>
tower::build_tower()
In this case we take our app, create a tower, add the layer we just built and construct the tower into a new shiny app object. If we try to access the app now we should see a not authorized response.
Now that we can block incoming HTTP requests, we need to add a way to allow users to enter. For this we will create a secret page that if the user enters it sets the appropriate cookie and returns the user back to the home page.
For this we will create another layer, this time this layer will work as a route for our application.
http_add_secret_cookie <- function(req) {
tower::response_builder() |>
tower::set_status(302) |>
tower::set_header("Location", "/") |>
tower::add_cookie("secret", SECRET) |>
tower::add_body(NULL) |>
tower::build_response()
}
shinyApp(ui, server) |>
tower::create_tower() |>
tower::add_get_route("/secret_entry_page", http_add_secret_cookie) |>
tower::add_http_layer(http_check_secret_layer) |>
tower::build_tower()
The layer returns a response that redirects the user back to the home page “/”, and in the process, adds the secret as a cookie.
We then add this handler to our tower before the http_check_secret_layer middleware layer. Now if we try to enter into http://localhost:3838/secret_entry_page we will see our app.
Tower is a very powerful R package to make Shiny application development more flexible. It is still under development so expect changes, but feedback is more than welcome. Any suggestions, bug reports, etc are welcome in the official GitHub Page.