Adding 2-Factor Authentication to any Web App using Nginx

At work today, I was asked to add some form of authentication to one of our Web Apps. The app itself had no authentication built in and allowed users to submit URLs and files for analysis. Obviously, we couldn’t put this Web App straight into production, as it would almost immediately be abused by our users. Luckily, I already knew you could use Nginx as a reverse proxy, adding authentication to almost anything.

Getting Nginx to work as a reverse proxy is well-documented, and adding basic authentication is just a matter of writing passwords to a file, then editing your config.

But what if you needed something more robust?

My boss didn’t like the idea of people being able to guess/bruteforce user credentials, and I didn’t want to have to ssh into the proxy and edit a file every time we needed a new user, or a password had to be changed. There had to be something better.

I set about searching for an authentication front-end, with the ability for provisioning of new users.

This is where Arno0x’s Two Factor Auth came into play. It provides a pretty front end for managing and creating authentication accounts, whilst also providing a backend to allow nginx to ensure the current user is authenticated.

Downloading and configuring the service is pretty straight-forward. My 2FA config file was simple, I just had to follow the readme on github.

The Nginx configuration needed a little more coaxing into life. I finally got it working with the following config:


map $request_uri $loggable {
/submit/api/submit 1;
default 0;
}

 log_format phpcookie '$remote_addr - $remote_user [$time_local]  '
 '"$request" $status $body_bytes_sent '
 '"$http_referer" "$http_user_agent" - "$http_cookie"';

server {
root /var/www/;
server_name example.site;
index index.php;


error_page 401 = @error401;

location @error401 {
    return 302 $scheme://$host/twofactorauth/login/login.php?from=$uri;
}

location = /twofactorauth/nginx/auth.php {
    include /etc/nginx/fastcgi.conf;
    fastcgi_param  CONTENT_LENGTH "";
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}

location /twofactorauth/ {
index index.php;
}

location /twofactorauth/db/ {
    deny all;
}

location = /twofactorauth/login/login.php {
    allow all;
    auth_request off;
include /etc/nginx/fastcgi.conf;
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}

location ~ \.php {
	
    include /etc/nginx/fastcgi.conf;
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}


location /static/ {
alias /var/www/html/static/;
}

location / {
access_log /var/log/nginx/cuckoo.log phpcookie if=$loggable;
proxy_pass       http://127.0.0.1:8080;
auth_request /twofactorauth/nginx/auth.php;
proxy_buffering off;
}
}
 

The majority of the config here is to handle what the reverse proxy serves itself, and what it passes to our webapp.


Explaining the Nginx Config

Handling Logging

map $request_uri $loggable {
/submit/api/submit 1;
default 0;
}

 log_format phpcookie '$remote_addr - $remote_user [$time_local]  '
 '"$request" $status $body_bytes_sent '
 '"$http_referer" "$http_user_agent" - "$http_cookie"';

This code handles how logging occurs. I wanted to log specific requests to the proxy, so I used these two paragraphs.

The first section defines what requests can be considered as ’loggable’. Requests with the 1; ending are considered candidates for logging, with the default being set as do not log. If the 0 and 1 were reversed, this would be a blacklisting operation instead.

The second section handles what format I want the log to use. In this case, I chose to include the $http_cookie option. This allows me to see which user made the request.

The Main configuration

Setting the servername and handling authentication

server {
root /var/www/;
server_name example.site;
index index.php;


error_page 401 = @error401;

location @error401 {
    return 302 $scheme://$host/twofactorauth/login/login.php?from=$uri;
}

This defines the root directory for nginx to use, the server name to respond to and the what file to load when https://example.site/ is requested.

This section also tells nginx what to do if the user has not been authenticated yet. In this case, instead of throwing a 401 error, the user is redirected to the 2FA login page, with the orginally requested page included in the URI. Once the user is authenticated, they’ll be redirected back to that URI.

Handling PHP and the 2FA application

location = /twofactorauth/nginx/auth.php {
    include /etc/nginx/fastcgi.conf;
    fastcgi_param  CONTENT_LENGTH "";
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}

location ~ \.php {

    include /etc/nginx/fastcgi.conf;
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}

location = /twofactorauth/login/login.php {
    allow all;
    auth_request off;
include /etc/nginx/fastcgi.conf;
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}


location /twofactorauth/ {
index index.php;
}


location /twofactorauth/db/ {
    deny all;
}

Here, I’ve told nginx how to handle files with .php extensions. I basically let nginx know that any of these files are to be executed by php7.2-fpm. Nothing really special here.

auth_request off; specifies that access to the 2FA login page is not subject to authentication. (if it was, we’d never be able to log in!)

I also defined the index file for requests to https://example.site/twofactoauth/ and specified that any access to the 2FA database folder is denied to everyone.

Handling static files and the reverse proxy

location /static/ {
alias /var/www/html/static/;
}

location / {
access_log /var/log/nginx/cuckoo.log phpcookie;
#access_log /var/log/nginx/cuckoo.log phpcookie if=$loggable;
proxy_pass       http://127.0.0.1:8080;
auth_request /twofactorauth/nginx/auth.php;
proxy_buffering off;
}
}

The top secion of code ensures that static content is served from the Proxy, and not passed to the main webapp. This is to make sure that the larger resources aren’t being pulled over a potentially low-latency connection.

Next, we state that that we want a new log file to be created, using the previously set phpcookie format and validated against our previously set loggable whitelist.

I then set the reverse proxy destination to be our local 8080 port, and require authentication for this access.

Proxy buffering is diabled, as my WebApp uses streamed data and buffering can introduce breaking delays in this stream.

And that’s it! One 2FA implementation for an Authentication-less Web App!