Unum pro multis: Using oauth2_proxy and nginx for web authentication

One of our jobs here in Shopkick’s infrastructure team is to save everyone time, without sacrificing security. During a recent hackathon, we decided it would be fun to replace some of our internal systems using http basic authentication with something a bit more - how shall I put it - from this decade. Fortunately, Bitly’s oauth2_proxy provides a fairly easy way to do just that, allowing us to leverage Google authentication with OAuth, which we were already using elsewhere.

We had a few issues to contend with in starting this.

  • We use nginx for SSL termination and reverse proxying our internal sites.
  • We want to make things more, not less, secure.
  • The transition had to be as seamless as possible.
  • We had to accommodate the WebSocket protocol for Jupyter notebook.
  • We had to be able to finish the work in a single 24-hour hackathon.

You only need two things to make this work: oauth2_proxy and nginx (1.5.4+ for auth_request support). Building a simple Go application like oauth2_proxy is fairly easy, but if you prefer to grab the precompiled binary you can find one on the GitHub releases page. Either way, all you need is the oauth2_proxy binary. In the examples here, we are running oauth2_proxy on the same hosts as nginx, but that is not a requirement.

The configuration file for oauth2_proxy is fairly simple:

email_domains = [
    "shopkick.com"
]
upstreams = [
    "http://<FOO>.shopkick.com"
]
cookie_secret = "<REDACTED COOKIE SECRET>"
cookie_secure = true
provider = "google"
client_id = “<REDACTED_CLIENT_ID>.apps.googleusercontent.com”

Certain configuration values are better left set as flags at runtime. Here are some that we set:

  • -set-xauthrequest - sets the X-Auth-Request-User and X-Auth-Request-Email headers which can be passed through by nginx
  • -client-secret - sets our client-secret at runtime, rather than in the config file
  • -authenticated-emails-file - sets the path to a newline-delimited list of external email addresses that are permitted to authenticate

Once you have your config in place, run oauth2_proxy like this:

$ oauth2_proxy -set-xauthrequest \
-config <CONFIG_FILE> \
-client-secret <REDACTED_SECRET> \
-authenticated-emails-file <EMAIL_LIST_FILE>

The rest of your configuration is in nginx. As mentioned before, make sure you are using 1.5.4+ or something with auth_request support. Following are some example configurations.

Here is an example with WebSocket support, for Jupyter notebook in our case.

server {
    listen 0.0.0.0:443 ssl;
    server_name foo.shopkick.com;
    location /oauth2/auth {
        internal;
        proxy_pass http://127.0.0.1:4180;
    }
    location /oauth2/ {
        proxy_pass http://127.0.0.1:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }
    location / {
        auth_request /oauth2/auth;
        error_page 401 = https://foo.shopkick.com/oauth2/sign_in;
        proxy_pass http://foo:8080/;
        proxy_set_header Host $host;
        # Jupyter requires WebSockets, so we have to add these lines
        # in order for terminals and notebooks to function.
        proxy_http_version 1.1;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
        proxy_read_timeout    86400;
    }
}

Example passing X-Forwarded-User and X-Email headers, based on the information provided by oauth2_proxy's -set-xauthrequest command-line flag.

server {
    listen  0.0.0.0:443 ssl;
    server_name bar.shopkick.com;
    proxy_headers_hash_max_size 2048;
    proxy_headers_hash_bucket_size 128;
    location / {
        auth_request /oauth2/auth;
        error_page 401 = https://bar.shopkick.com/oauth2/sign_in;
        proxy_set_header Host $host;
        proxy_set_header HTTP_X_FORWARDED_SSL on;
        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;
        auth_request_set $user   $upstream_http_x_auth_request_user;
        auth_request_set $email  $upstream_http_x_auth_request_email;
        proxy_set_header X-Forwarded-User  $user;
        proxy_set_header X-Email $email;
        proxy_pass http://bar:80;
    }
    location /oauth2/auth {
        internal;
        proxy_pass http://127.0.0.1:4180;
    }
    location /oauth2/ {
        proxy_pass http://127.0.0.1:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }
}

And finally, an example using a non-standard port for https.

server {
    listen  0.0.0.0:4443 ssl;
    server_name baz.shopkick.com;

    # Redirect to HTTPS when HTTP request comes in
    error_page 497 https://$host:4443$request_uri;
    location / {
        error_page 401 = https://baz.shopkick.com/oauth2/sign_in;
        auth_request /oauth2/auth;
        proxy_pass http://baz:4443;
        # Note that here we are passing $http_host instead of $host.
        # $http_host contains the port information, which in this
        # case is critical for the redirect URL that google hands
        # back after a successful authentication.
        proxy_set_header Host $http_host;
    }
    location /oauth2/auth {
        internal;
        proxy_pass http://127.0.0.1:4180;
    }
    location /oauth2/ {
        proxy_pass http://127.0.0.1:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }
}

While all of the above uses Google as the OAuth provider, everything here should work with any other supported provider: Azure, Facebook, Github, Gitlab, LinkedIn, or MyUSA. Just remember to set your provider flag or config setting.

Happy authenticating! And don't forget to subscribe!