How to protect your Shiny app with a Password

This article belongs to the series How to deploy a Shiny app on AWS, divided into 7 parts. To access the other articles, use the following table of contents:

It’s time to learn how to protect your Shiny app behind an authentication portal.

Most Shiny apps I create for clients are internal apps.

They use confidential data and the dashboard is intended to be used by a specific team.

Not by the public Internet.

In some companies, they isolate the app thanks to a VPN.

But sometimes, it’s not possible.

Sometimes you want to open your app to the Internet and restrict it to some users.

And you want to manage their rights.

You are at the right place.

From the simplest solution to the most complex one:

  1. How to set up a simple password with nginx
  2. How to use a third-party service like Auth0
  3. How to create your own authentication portal

Now…

Before we start, a small disclaimer.

We’re talking about security.

I am NOT a security expert.

It’s a real job. My job is analyzing data. Not security.

You are responsible for the security of your applications.

You are responsible for your data.

This article is purely informative.

:)

Now that this is stated, let’s start!

Your choices

There is no best way to secure a Shiny app.

Here are your possibilities:

Shiny Server Pro

Haha.

I’m kidding!

Use your web server - nginx

Shiny AWS create application

We have already used nginx as a reverse proxy.

Remember.

nginx is your doorman.

It redirects visitors to the right service. To the right port number.

Another role for this doorman could be to accept or deny access to these visitors.

Rather than letting anyone pass, it could ask for credentials.

This solution works well. It’s easy to set up. And it’s secured.

Use a third-party service - Auth0

Shiny AWS create application

auth0 is an authentication service you can integrate into any application.

It’s free up to 7000 users.

The biggest advantage is that you don’t have to worry about security.

That’s THEIR job.

Their expertise.

Not yours, nor mine.

So they know better than us!

However, using a third-party might be an issue for you. It creates dependency. You need to trust them.

Build your own authentication portal

Shiny AWS create application

Or: Do It Yourself.

If you’ve red all articles up until this one, well… you probably like to do things by yourself.

This is the most flexible solution, but also the less secure.

Since you’re not a security expert.

And me neither.

You’ve got to ask yourself…

Is Shiny secure?

Imagine that you add an authentication portal at the start of your Shiny app.

The logic of the code is:

“If the user logs in, then we show the rest of the app.”

First, it means the user does access to the Shiny Server to log in.

Is the rest of the code well hidden? How well?

Could the user manipulate the Javascript console? Or another backdoor we don’t know anything about?

Is the authentication portal resistant to SQL injection? To code injection? To brute force? To DoS attacks?

Answers aren’t always clear.

And even if you can answer to these questions, what about what you don’t know.

So…

This solution works.

And it’s not optimal for security.

Now let’s dig into each of the other solutions.

Use nginx as an authentication server

This is the simplest solution.

And the least flexible.

In short…

You write the credentials in a file.

Then you get an unwelcoming popup at each connexion to the app.

It’s a quick and dirty solution.

But it’s efficient if you have an app that you want to show only to a few people.

For example, to a client.

You want to show him what your work looks like, but to him only.

The simplest way is to host it on your server, create a password with nginx, and share it with your client.

To set up this solution, we use the console once again.

As a prerequisite, you need to have followed along the instructions of the two previous articles:

Your config file (the one at /etc/nginx/sites-available/shiny.conf) should look like this:

server {
    listen 80;
    listen [::]:80;
    server_name shiny.charlesbordet.com;
    server_tokens off;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    root /dev/null;
    server_tokens off;
    client_max_body_size 0;

    server_name shiny.charlesbordet.com;

    access_log /var/log/nginx/shiny-access;
    error_log /var/log/nginx/shiny-error;

    location / {
        proxy_pass http://localhost:3838;
        proxy_redirect http://localhost:3838/ $scheme://$host/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 20d;
        proxy_buffering off;
    }

    # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
    ssl_certificate /etc/letsencrypt/live/shiny.charlesbordet.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/shiny.charlesbordet.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam.pem
    ssl_dhparam /etc/nginx/dhparam.pem;

    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
    add_header Strict-Transport-Security "max-age=63072000" always;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /etc/letsencrypt/live/shiny.charlesbordet.com/chain.pem;
}

This file is getting big! But as we completed it step by step, it’s not that scary.

Now, we add a new location bloc that’s almost identical to the existing one, with two differences:

  1. The location won’t be the root (i.e. the slash /) but the /movie-explorer directory.
  2. Then, we’ll add the password.

Here is the new location bloc that you add below the existing one:

location /movie-explorer {
    proxy_pass http://localhost:3838;
    proxy_redirect http://localost:3838/ $scheme://$host/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 20d;
    proxy_buffering off;
    auth_basic "Restricted Content";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

Why adding a location?

We could add only the last two lines to the previous location bloc:

auth_basic "Restricted Content";
auth_basic_user_file /etc/nginx/.htpasswd;

But if you do that, ALL your Shiny apps will be protected by the same passwords.

That’s not what we want.

We want to have some apps that are protected, and others that stay accessible by anyone.

With my solution and a second location bloc, you can choose.

Save the config file.

Now, we fill the /etc/nginx/.htpasswd with the credentials.

It’s super simple.

First, run the following instruction to create the login:

$ sudo sh -c "echo -n 'charles:' >> /etc/nginx/.htpasswd"

Of course, you can replace charles by any other login.

Then, the following instruction will enable you to type a password but store only a hashed version of it (for security):

$ sudo sh -c "openssl passwd -apr1 >> /etc/nginx/.htpasswd"

That’s it!

You can repeat the operation as much as you want to create other logins.

Now, restart the nginx server:

$ sudo nginx -t
$ sudo systemctl restart nginx

And your app is protected.

Try for example my app at https://shiny.charlesbordet.com/movie-explorer-secure/ and notice the authentication popup.

No credentials? No access.

Try:

  • login = guest
  • password = trololo

And now you can access.

This method is secured because you force visitors to walk through nginx.

And by the way: Don’t forget to block the 3838 port in your firewall.

You can do so in AWS (reverse what we did in part 4)or by using ufw.

Otherwise people could walk around nginx.

Another important point: Use a strong password.

This method is not resistant against bruteforce.

To protect yourself against it, use fail2ban.

Now…

This method works well.

It’s pretty secured.

But.

It’s not flexible at all!

It’s complicated to create new accounts.

No interface.

You have to use the console.

And only you or someone technical enough can do it. It’s a bit complicated.

You can’t automate it.

It’s not user-friendly.

Could you imagine a pop-up like that to log in to an e-commerce website?

Ha ha.

No.

It doesn’t meet all possible needs.

Either you get in.

Or not.

No account management.

No user right.

You don’t even know who the user is in the Shiny app.

So let’s try an other solution.

Use Auth0 as an authentication server

This second solution is a bit more complicated to set up, but has interesting features:

  • Security is optimal,
  • You can manage users,
  • It’s simple.

Auth0 is an authentication and authorization service provider.

Rather than creating our own authentication portal, Auth0 will deal with this itself.

How to set up Auth0

Create an Auth0 account.

Go to https://auth0.com/signup and create an account.

The service is free up to 7000 users.

Once the account is created, log in and click on the big CREATE APPLICATION button:

Shiny AWS create application

Give it a name and choose Regular Web Applications:

Shiny AWS create application

You won’t find Shiny in the default app, so let’s configure the portal manually.

Click on Settings.

Here are the important information to note:

  • Domain
  • Client ID
  • Client Secret

You will need it later.

A bit below, in the Allowed Callback URLs cell, you have to fill in your URL followed by callback.

For me, it’s https://shiny.charlesbordet.com/callback.

Warning: No trailing slash /

Save the modifications.

Install the portal on your server.

SSH into it and follow these steps:

1. Start by installing NodeJS.

$ sudo apt update
$ sudo apt install curl
$ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
$ sudo apt install nodejs

2. Then, install the Auth0 application:

$ git clone git@github.com:auth0/shiny-auth0.git
$ cd shiny-auth0
$ npm install

3. Finally, configure Auth0 with the information you got earlier.

Create a new file named .env (by typing nano .env for example) and enter the following:

AUTH0_CLIENT_SECRET=votreClientSecretAuth0
AUTH0_CLIENT_ID=votreClientIDAuth0
AUTH0_DOMAIN=votreDomainAuth0
AUTH0_CALLBACK_URL=https://shiny.charlesbordet.com/callback
COOKIE_SECRET=r7QVJVVxj77CJyuX5tGrE238uLGGh8XZb84c3WdvWgQ5RPVwYc
SHINY_HOST=localhost
SHINY_PORT=3838
PORT=3000

The first four fields are the information you got earlier in your Auth0 account.

The COOKIE_SECRET must be a random string. I used a password generator to get mine.

You can leave the rest as it is.

Alright!

Is our app secured?

Not yet.

Shiny listens on port 3838.

But the authentication portal listens on port 3000.

And we configured nginx to redirects to port 3838.

We have to change that.

Open your nginx config file (at /etc/nginx/sites-available/shiny.conf) and change the instructions for proxy_pass and proxy_redirect:

proxy_pass http://localhost:3000;
proxy_redirect http://localhost:3000/ $scheme://$host/;

I changed the port number 3838 for 3000.

Save the file and restart nginx:

$ sudo nginx -t
$ sudo systemctl restart nginx

Is our app secured?

Not yet!

We broke the app.

It’s not even accessible anymore!

Indeed, we’re redirecting to the Auth0 portal now, but we haven’t started it!

Go back to the shiny-auth0 directory and run the following:

$ cd /home/charles/shiny-auth0
$ node bin/www

The console shouldn’t display anything. And the prompt is gone as well.

That’s because this instruction manually starts the authentication portal server.

It stays this way and will display the access logs.

At least now we can test the portal.

Go to your Shiny app and you should see this:

Shiny AWS create application

You can create an account. Log in and then access to the app.

Cool!

If you want to get back your console prompt, you have to stop the server with Ctrl + C.

Something is weird though.

We have two issues:

  1. This manual start is not convenient. Are you supposed to keep the console open all the time?
  2. The goal is to restrict access, not to offer the possibility to anyone to open an account!

Let’s take care of this.

Create a service for Auth0

We have a few web servers on our machine.

  • The Shiny Server: On port 3838.

  • The nginx Server: On port 80.

And now, the Auth0 Server. On port 3000.

Why do we have to start the Auth0 server manually and not the others?

The others are services. They start automatically as soon as the machine boots. And the service configuration has been done during the installation of these web servers.

For Auth0, the service configuration hasn’t been done.

We have to do it. Then, everything will be automatic.

It’s not complicated.

Create a new file:

$ sudo nano /etc/systemd/system/shiny-auth0.service

And fill it with this service configuration:

[Service]
ExecStart=/usr/bin/node /home/charles/shiny-auth0/bin/www
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=charles
User=charles
Group=charles
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Make sure you replace charles with your username.

In general, it’s better to create a new username for each service. Like the shiny username that was created for the Shiny Server.

If the service is compromised, the rest of the server stays safe.

Once the configuration is done, let’s activate and start the service:

$ sudo systemctl enable shiny-auth0
$ sudo systemctl start shiny-auth0

Now, your authentication portal is online, and there’s no need for a manual start anymore!

Let’s tackle the other issue: How to restrict the access?

Create access rules

Go back to the Auth0 account management.

There is a Rules section that will enable you to create Javascript snippets.

Click on CREATE RULE and you will see a ton of possibilities:

Shiny AWS create application

For example, you can:

  • Specify a list of authorized emails,
  • Forbid social logins (like Google, Facebook, etc.),
  • Connect to an API to check if an email is authorized,
  • Create your own custom rules.

I must admit this part is a bit intimidating to me.

I don’t know Javascript that well, so I prefer to use only the pre-configured rules.

For example, for a client who wants the app accessible only to employees, it’s easy to limit the access only to emails that look like xxx@companyname.com.

It’s worth noting that being able to use Javascript allows for a lot of flexibility.

You could imagine having a SaaS product with your Shiny app. The buyer is added to your Mailchimp account on a specific list. Then, the rule calls Mailchimp API to check if the email is authorized.

Or the other way around. Your Shiny app is free, but users must create an account and their contact information is sent to Mailchimp, so that you can talk with your users or notify them of new versions.

For us Shiny developers, learning Javascript can’t hurt anyway.

So. This Auth0 service is quite good.

Except I don’t use it much except in very specific cases such as the exemples I just mentionned.

  • I don’t like much it uses NodeJS. I don’t know this language at all, so that’s scary to me.
  • Using a third party creates a dependency. What if they change their conditions tomorrow?
  • While the rules system is flexible, the portal isn’t. What if I want to create a nice landing page?

Hence the third and last solution: Create your own authentication portal.

How to create your own authentication portal with Shiny

Here is the package you’ve been looking for: shinymanager

It’s available on CRAN:

install.packages("shinymanager")

You will find some documentation on their Github repo.

But I find it a bit light.

Let’s start with their minimalist example to add the authentication portal.

# define some credentials
credentials <- data.frame(
  user = c("shiny", "shinymanager"),
  password = c("azerty", "12345"),
  stringsAsFactors = FALSE
)

library(shiny)
library(shinymanager)

ui <- fluidPage(
  tags$h2("My secure application"),
  verbatimTextOutput("auth_output")
)

# Wrap your UI with secure_app
ui <- secure_app(ui)


server <- function(input, output, session) {
  
  # call the server part
  # check_credentials returns a function to authenticate users
  res_auth <- secure_server(
    check_credentials = check_credentials(credentials)
  )
  
  output$auth_output <- renderPrint({
    reactiveValuesToList(res_auth)
  })
  
  # your classic server logic
  
}

shinyApp(ui, server)

It works!

Shiny AWS create application

So let’s try to adapt it to our movie explorer app.

As a reminder, the code of the app is available here.

After reading the documentation, we understand we need to:

  1. Create credentials
  2. Load the shinymanager package
  3. Wrap the UI function with secured_app
  4. Add the authentication module in the server part

Let’s do it!

Create credentials

The easiest part.

I use the piece of code from their example and put it in the global.R file:

credentials <- data.frame(
  user = c("shiny", "shinymanager"),
  password = c("azerty", "12345"),
  stringsAsFactors = FALSE
)

Yup.

Credentials are stored like that.

Not secured!

We’ll come back to it later :)

Load shinymanager

Well, this is the easiest part.

I load it in the global.R file as well:

library(shinymanager)

Wrap the UI with secure_app

In the ui.R file, find the fluidPage function.

That’s where the UI starts.

I add the secure_app function around fluidPage:

secure_app(fluidPage(
  # Code inside
))

And don’t forget to close the ending bracket.

Add the authentication module

Finally, I’ll copy/paste another piece of code at the beginning of the server function:

# call the server part
# check_credentials returns a function to authenticate users
res_auth <- secure_server(
  check_credentials = check_credentials(credentials)
)

Boom!

Done.

Except… We created a bug.

Because of ggvis.

It doesn’t like empty data.frames.

And because of the authentication part, that’s what we have.

I tell it to plot something empty as long as we’re not logged in:

vis <- reactive({
  if (is.null(input$xvar)) return(ggvis(data.frame(x = 0, y = 0), ~x, ~y))
  # Lables for axes
  # Reste du code...
})

Now it works!

I’ve put my code here: https://gitlab.charlesbordet.com/charles/movie-explorer-login.

And hosted it here: https://shiny.charlesbordet.com/movie-explorer-login/.

You can try to log in with the shiny/azerty credentials.

More options

shinymanager doesn’t stop there.

You can also:

  • have an administration mode to manage users,
  • use an encrypted database to store credentials,
  • change the language of the portal.

But…

If you want to offer the possibility to create accounts, you have to do it yourself.

There are still a lot of things you have to do by yourself.

shinymanager is a good starting point.

Not a full solution.

Whenever I create authentication portals with Shiny, I do the following:

  • I clone shinymanager and add the code to my project
  • I get rid of all features I don’t need (logs or the admin interface)
  • I add my own features

The first time was time-consuming as I had to go through the intricacies of the code.

But now I’m good.

I recommend you do the same.

Plus, you will better understand the strengths and weaknesses of the portal in terms of security.

I add these features:

  • Createan account,
  • Use rules to restrict who can create an account,
  • Add a “Forgot password” link,
  • Add cookies to remember sessions,
  • Hash & Salt passwords.

In the end…

I like this solution.

Because of the flexibility.

The only drawback is security.

I don’t know what I don’t know.

So be careful.

If you deal with confidential data, ask a professional.

Compare solutions

Here is a short recap of all the pros & cons of each solution:

Solution Price Setup Dependency Security Administration
Shiny Server Pro Expensive Easy Independent Good No
nginx Free Easy Independent Good No
Auth0 Free up until 7k users Medium Dependent Good Yes
Shiny portal Free Hard Independent Medium Yes

Which one will you choose?


This article ends my series on How to deploy a Shiny app on AWS.

I hope you found it useful!

I know there are still plenty of topics to cover.

Docker, shinyproxy.io, scalability, …

I will write about these.

:)

To get updates, you can subscribe below.

Updated:

Comments

Rucha Deshpande

Hi Charles. Your article was very helpful to get me going.
As of now I am getting 502 Bad Gateway when I try to secure my app with Auth0

Charles

Hi Rucha and thank you for the message. I’d love to help you but you’ve got to tell me a bit more about the issue you’re having. The more details I have, the better I can help you.

Zac

Charles I just wanted to comment that these tutorials are truly excellent. You’ve made a daunting task way more manageable. Love your writing style! Really looking forward to any thoughts you have around Docker and shinyproxy too.

Leave a Comment

Required fields are marked *

Loading...

Comments are validated manually. The page will refresh after validation.