Using a local web server for Shiny development with OAuth APIs

macos
shiny
rstats
destiny2
api
Author

Alan Schussman

Published

September 3, 2023

Filed under: Notes to myself so I remember this later

(This is part 1; part 2 describes the Shiny development side of this)

It took a fair amount of trial and error to get a working, local development environment for Shiny apps that have to interact with an API’s OAuth flow. OAuth has a lot going on. I can’t cover every scenario here, so I’m writing up the problems that I’ve solved. In my case, this is for my Armorer and Traveler apps using the Destiny 2 API, but these general ideas should be applicable in other situations that use OAuth, too, albeit perhaps with some modification.

I needed to solve a couple of core problems, and along the way I do a little bit of OAuth explaining:

  1. A local web server running https: When a user users an OAuth workflow, they are saying to the remote system, “yes, please give this application access to your data on my behalf” — that’s why when you interact with an OAuth application as a user, you appear to log into the remote system, never to the third party tool that you’re authorizing. This keeps the third-party app from having your actual user credentials.

    When you authorize an application using OAuth, the system that you interact with to perform that authorization has to send that permission back to the requesting application, in the form of a token that represents the user and the actions they’ve authorized the new application to perform. The clever part is that it redirects your requesting application essentially back to itself by appending the authorization token to a request to your own application that it will receive and process.

    How does the remote API know where to send that redirected request? You tell it! Part of your request for authorization says, “hey, go here when complete,” and in the case of local development, that place to go is your own local machine, where your dev application lives: localhost. Likewise, for a production app, that redirect location is some internet-accessible place where your app lives, like heckyeahshiny.com.

    So that’s why you need a local web server. And in the case of the Destiny 2 API, that redirect must be an https destination, which is a couple of ticks more complex than just running a local server.

  2. The second problem is to gracefully handle the local and remote authentication and hosting scenarios within the application. I don’t want to have to make configuration or code changes when publishing a Shiny application, so the app needs to be able to understand what context it’s running, make the authorization request accordingly, and then behave appropriately for the right environment. Once the server is set up and reliably working, this part is pretty straightforward, but it’s tightly interactive and requires elements that work together across ui.R and server.R.

Local web server setup

I learned exactly enough about this to solve the very specific need that I was facing: Shiny doesn’t use https, but the redirect URL following authorization with the Destiny 2 API must be https. So I needed to set up a local web server on MacOS, allow it to receive requests using https, and forward those requests to my Shiny app. I do this with nginx, and start by installing it using brew:

sudo brew install nginx

I then followed this short gist to make a purely local, self-signed SSL certificate that could backstop my https connection. The best practice is to not self-sign; but we won’t be serving anything to anybody but ourselves, so this is okay here. The key steps are the following:

sudo openssl genrsa -out ~/.localhost-ssl/localhost.key 2048
sudo openssl req -new -x509 -key ~/.localhost-ssl/localhost.key -out ~/.localhost-ssl/localhost.crt -days 3650 -subj /CN=localhost
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.localhost-ssl/localhost.crt

Then I used this writeup at RStudio to figure out how to configure nginx to receive requests made to https and redirect them to the port that my Shiny app is configured to listen on. (More on this in the app code section further below.

This section of configuration replaces the stock, commented-out HTTPS server section close to the bottom of the nginx configuration file that brew installed for me at /usr/local/etc/nginx/nginx.conf

# HTTPS server
    
server {
        listen       443 ssl;
        server_name  localhost;

        ssl_certificate      /Users/alan/.localhost-ssl/localhost.crt;
        ssl_certificate_key  /Users/alan/.localhost-ssl/localhost.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

  location / {

    proxy_set_header    Host $host;
    proxy_set_header    X-Real-IP $remote_addr;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto $scheme;
    proxy_pass          http://localhost:8100;
    proxy_read_timeout  20d;
    proxy_buffering off;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_http_version 1.1;

    proxy_redirect      / $scheme://$host/;
  }    

}

A couple of notes about the configuration: You can see the local directory where my ssl certificate and key are stored; I built those in the first step above; and the proxy_pass parameter is the next important bit, which needs to match the local URL set up in my Shiny app later on.

With nginx installed, it can be started and stopped with:

  • brew services start nginx and

  • brew services stop nginx

(This is part 1; part 2 describes the Shiny development side of this)