Use nginx to Add Authentication to Any Application

avatar-aaronpk.jpg Aaron Parecki

Ever found yourself wanting to put an application behind a login form, but dreading writing all that code to deal with OAuth 2.0 or passwords? In this tutorial, I’ll show you how to use the nginx auth_request module to protect any application running behind your nginx server with OAuth 2.0, without writing any code! Lasso, a microservice written in Go, handles the OAuth dance to any number of different auth providers so you don’t have to.

Why Authenticate at the Web Server?

Imagine you use nginx to run a small private wiki for your team. At first, you probably start out with adding a wiki user account for each person. It’s not too bad, adding new accounts for new hires, and removing them when they leave.

A few months later, as your team and company start growing, you add some server monitoring software, and you want to put that behind a login so only your company can view it. Since it’s not very sophisticated software, the easiest way to do that is to create a single password for everyone in an .htpasswd file, and share that user with the office.

Another month goes by, and you add a continuous integration system, and that comes with GitHub authentication as an option, which seems reasonable since most of your team has GitHub accounts already.

At this point, when someone new joins, you have to create a wiki account for them, add them to the GitHub organization, and give them the shared password for the other system. When someone leaves, you can delete their wiki account and remove them from GitHub, but let’s face it, you probably won’t change the shared password for a while since it’s annoying having to distribute that to everyone again.

Surely there must be a better way to integrate all these systems to use a common shared login system! The problem is the wiki is written in PHP, the server monitoring system just ends up publishing a folder of static HTML, and the CI system is written in Ruby which only one person on your team feels comfortable writing.

If the web server could handle authenticating users, then each backend system wouldn’t need to worry about it, since the only requests that could make it through would already be authenticated!

Using the nginx auth_request Module

Enter the nginx auth_request module.

This module is shipped with nginx, but requires enabling when you compile nginx. When you download the nginx source and compile, just include the --with-http_auth_request_module flag along with any others that you use.

The auth_request module sits between the internet and your backend server that nginx passes requests onto, and any time a request comes in, it first forwards the request to a separate server to check whether the user is authenticated, and uses the HTTP response to decide whether to allow the request to continue to the backend.

Flowchart illustrating the nginx auth_request module

This diagram illustrates a request that comes in for the server name stats.avocado.lol. First, nginx fires off a sub-request to login.avocado.lol (1), and if the response (2) to that request returns HTTP 200, it then continues forwarding the request on to the backend stats.avocado.lol.

Choosing an Auth Proxy

Since the nginx auth_request module has no concept of users or how to authenticate anyone, we need something else in the mix that can actually handle logging users in. In the diagram above, this is illustrated by the server name login.avocado.lol.

This server needs to handle an HTTP request and return HTTP 200 or 401 depending on whether the user is logged in. If the user is not logged in, it needs to know how to get them to log in and set a session cookie.

To accomplish this, we’ll use the open source project “Lasso”. Lasso is written in Go, so it’s super easy to deploy. Everything can be configured via a single YAML file. Lasso can be configured to authenticate users via a variety of OAuth and OpenID Connect backends such as GitHub, Google, Okta or any other custom servers.

We’ll come back to configuring Lasso in a few minutes, but for now, let’s continue on to set up your protected server in nginx.

Configure Your Protected nginx Host

Starting with a typical nginx server block, you just need to add a couple lines to enable the auth_request module. Here is an example server block that should look similar to your own config. This example just serves a folder of static HTML files, but the same idea applies whether you’re passing the request on to a fastcgi backend or using proxy_pass.

server {
  listen 443 ssl http2;
  server_name stats.avocado.lol;

  ssl_certificate /etc/letsencrypt/live/avocado.lol/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/avocado.lol/privkey.pem;

  root /web/sites/stats.avocado.lol;

  index index.html;
}

Add the following to your existing server block:

# Any request to this server will first be sent to this URL
auth_request /lasso-validate;

location = /lasso-validate {
  # This address is where Lasso will be listening on
  proxy_pass http://127.0.0.1:9090/validate;
  proxy_pass_request_body off; # no need to send the POST body

  proxy_set_header Content-Length "";
  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;

  # these return values are passed to the @error401 call
  auth_request_set $auth_resp_jwt $upstream_http_x_lasso_jwt;
  auth_request_set $auth_resp_err $upstream_http_x_lasso_err;
  auth_request_set $auth_resp_failcount $upstream_http_x_lasso_failcount;
}

error_page 401 = @error401;

# If the user is not logged in, redirect them to Lasso's login URL
location @error401 {
  return 302 https://login.avocado.lol/login?url=https://$http_host$request_uri&lasso-failcount=$auth_resp_failcount&X-Lasso-Token=$auth_resp_jwt&error=$auth_resp_err;
}

Let’s look at what’s going on here. The first line, auth_request /lasso-validate; is what enables this flow. This tells the auth_request module to first send any request to this URL before deciding whether it’s allowed to continue to the backend server.

The block location = /lasso-validate captures that URL, and proxies it to the Lasso server that will be listening on port 9090. We don’t need to send the POST body to Lasso, since all we really care about is the cookie.

The line error_page 401 = @error401; tells nginx what to do if Lasso returns an HTTP 401 response, which is to pass it to the block defined by location @error401. That block will redirect the user’s browser to Lasso’s login URL which will kick off the flow to the real authentication backend.

Configure a Server Block for Lasso

Next, configure a new server block for Lasso so that it has a publicly accessible URL like https://login.avocado.lol. All this needs to do is proxy the request to the backend Lasso server.

server {
  listen 443 ssl;
  server_name login.avocado.lol;

  ssl_certificate /etc/letsencrypt/live/login.avocado.lol/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/login.avocado.lol/privkey.pem;

  # Proxy to your Lasso instance
  location / {
    proxy_set_header  Host  login.avocado.lol;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_pass        http://127.0.0.1:9090;
  }
}

Configure and Deploy Lasso

You’ll need to download Lasso and compile the Go binary for your platform. You can follow the instructions in the project’s README file.

Once you’ve got a binary, you’ll need to create the config file to define the way you want Lasso to authenticate users.

Copy config/config.yml_example to config/config.yml and read through the settings there. Most of the defaults will be fine, but you’ll want to create your own JWT secret string and replace the placeholder value of your_random_string.

The easiest way to configure Lasso is to have it allow any user that can authenticate at the OAuth server be allowed to access the backend. This works great if you’re using a private OAuth server like Okta to manage your users. Go ahead and set allowAllUsers: true to enable this behavior, and comment out the domains: chunk.

You’ll need to choose an OAuth 2.0 provider to use to actually authenticate users. In this example we’ll use Okta, since that’s the easiest way to have a full OAuth/OpenID Connect server and be able to manage all your user accounts from a single dashboard. Before you can finish filling out the config file, you’ll need to sign up for an Okta Developer account at developer.okta.com/. Once you create an account, click Applications in the top menu, and create a new application. Choose Web as the application platform.

Create a web application with Okta

In the next screen, you’ll need to configure the Base URI and Login redirect URI to match your own server’s settings. Lasso’s redirect URI ends in /auth so your configuration should look like the below screenshot.

Create a web application with Okta

Once you’ve done that, Okta will give you a client ID and secret, which you’ll need to include in the config file.

config.yml

oauth:
  provider: oidc
  client_id: {yourClientID}
  client_secret: {yourClientSecret}
  auth_url: https://{yourOktaDomain}/oauth2/default/v1/authorize
  token_url: https://{yourOktaDomain}/oauth2/default/v1/token
  user_info_url: https://{yourOktaDomain}/oauth2/default/v1/userinfo
  scopes:
    - openid
    - email
  # Set the callback URL to the domain that Lasso is running on
  callback_url: https://login.avocado.lol/auth

Now you can run Lasso! It will listen on port 9090, which is where you’ve configured nginx to send the auth_request verifications as well as serve traffic from login.avocado.lol.

When you reload the nginx config, all requests to stats.avocado.lol will require that you log in via Okta first!

Bonus: Who Logged In?

If you’re putting a dynamic web app behind nginx and you care not only about whether someone was able to log in, but also who they are, there is one more trick we can use.

By default, Lasso will extract a user ID via OpenID Connect (or GitHub or Google if you’ve configured those as your auth providers), and will include that user ID in an HTTP header that gets passed back up to the main server.

In your main server block, just below the line auth_request /lasso-validate; which enables the auth_request module, add the following:

auth_request_set $auth_user $upstream_http_x_lasso_user;

This will take the HTTP header that Lasso sets, X-Lasso-User, and assign it to the nginx variable $auth_user. Then, depending on whether you use fastcgi or proxy_pass, include one of the two lines below in your server block:

fastcgi_param REMOTE_USER $auth_user;
proxy_set_header Remote-User $auth_user;

These will set an HTTP header with the value of $auth_user that your backend server can read in order to know who logged in. For example, in PHP you can access this data using:

<?php
echo 'Hello, ' . $_SERVER['REMOTE_USER'] . '!';

Now you can be sure that your internal app can only be accessed by authenticated users!

Learn More About OAuth 2.0 and Secure User Management with Okta

For more information and tutorials about OAuth 2.0, check out some of our other blog posts!

As always, we’d love to hear from you about this post, or really anything else! Hit us up in the comments, or on Twitter @oktadev!