NGINX and 2FA

on under NGINX
8 minute read

As we were planning our move to AWS, one of the items that needed to be replaced was an old installation of Microsoft’s UAG and Forefront. These services were used as a reverse proxy to internal websites along with adding a multifactor authentication piece to applications that were provided to the organization, but we did not have the source code to and could not modify. UAG and Forefront have an official end of life in April 2020, and the new solution Microsoft supports integrates mostly into Active Directory. While considering what to replace these with, I at first looked into NGINX and JWTs. While this seemed like a workable solution, I later found that NGINX can host Dotnet Core applications and use the output of a particular website to determine if a user is authenticated.

NGINX has a module that you can install called [ngx_http_auth_request_module] (http://nginx.org/en/docs/http/ngx_http_auth_request_module.html). This module allows you to determine if a user has been authenticated using a website you control. If the user is not authenticated, the module expects HTTP 401. If the user is authenticated, the module expects HTTP 200. Here is an example configuration of the module.

location /private/ {
    auth_request /auth;
    ...
}

location = /auth {
    proxy_pass ...
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
}

Using this concept, a Dotnet Core application can be written that authenticates the user’s token provided from their Google Authenticator application, or any other application that can deal with OTP authentication. In our environment, the code was already written for me to authenticate the user’s tokens and the user base was already using this for UAG/Forefront. If you need a sample of how to setup and create the two factor authentication, a [sample is available here.] (https://medium.freecodecamp.org/how-to-set-up-two-factor-authentication-on-asp-net-core-using-google-authenticator-4b15d0698ec9)

The base application I used was a Dotnet Core application provided by Microsoft named [Cookie Sample.] (https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-2.2) One of the primary things to note in the Dotnet Core application is the use of the cookie domain. This cookie domain allows your DNS entry for your NGINX/authentication app to have the same domain as the site you are protecting. These settings are configured in the Startup.cs file of the project as shown below.

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.Cookie.SameSite = SameSiteMode.None;
                    options.Cookie.Domain = “<primary domain>”;
                    options.SlidingExpiration = true;
                });

Another item to note in the Startup.cs file is to add the status code pages. This will help return the HTTP 401 pages.

 app.UseStatusCodePages(async context =>
            {
                context.HttpContext.Response.ContentType = "text/plain";
                context.HttpContext.Response.StatusCode = 401;
                await context.HttpContext.Response.WriteAsync(
                    "Status code page, status code: 401");
            });

From the cookie sample project, you should modify the Login.cshtml.cs file to add your custom authentication to your application.

 private async Task<ApplicationUser> AuthenticateUser(string username, string token)
        {
            // Code to authenticate token            
	};

This application makes use of the Dotnet Core functions involving claims.

  var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim("Token", user.Token),
                };

                var claimsIdentity = new ClaimsIdentity(
                    claims, CookieAuthenticationDefaults.AuthenticationScheme);

In the login page, you can set specific cookie settings, including cookie expiration time.

  var authProperties = new AuthenticationProperties
        {
	// Specific cookie settings
	};

The following is the function that performs the authentication request.

 await HttpContext.SignInAsync(
                    CookieAuthenticationDefaults.AuthenticationScheme,
                    new ClaimsPrincipal(claimsIdentity),
                    authProperties);

And this snippet of code redirects the user back to the application that called the authentication request in the beginning. The settings for how this is sent to the authentication application is shown in the NGINX configuration section. This allows on application to be used across many different websites.

 if (Request.Query["ReturnUrl"].Count > 0)
                    return Redirect("https://" + Request.Query["ReturnUrl"][0]);
                else
                    return Page();

In the NGINX config, a section is configured to send the /auth request. This URL needs to contain a page that returns either HTTP 401 or HTTP 200. One of the biggest issues I encountered with this page is that it needs to be very minimal. Any additions of javascript that links somewhere else returns HTTP 200 instead of 401. If you’re having trouble with your authentication, you can use the Developer Tools in Chrome (F12), and click on the networking tab. This will show you what HTTP response code has been sent to the caller. The minimalist authentication page should look like the following.

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link rel="shortcut icon" href="~/favicon.ico" />
    <title>AuthTest</title>
</head>
<body>
</body>
</html>

The code back end of this page is also minimal, if the user is not authenticated, HTTP 401 is returned. HTTP 200 does not need to be explicitly named, because the page will return 200 when it loads.

 public void OnGet()
        {
            if (!User.Identity.IsAuthenticated)
            {
                HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
            }
        }

That is the majority of the application. You can publish the application to a file location and then copy the files to your NGINX server using WinSCP. The files will live in the /var/www directory on the server.

Next we will walk through setting up NGINX on the Linux box. The server used was Ubuntu, and these instructions are specific to that, using apt-get. The setup instructions were followed from a tutorial published by Microsoft. To download and install Dotnet Core on Ubuntu, use the following commands.

wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb

sudo add-apt-repository universe
sudo apt-get install apt-transport-https
sudo apt-get update
sudo apt-get install aspnetcore-runtime-2.2

After Dotnet Core is installed, you can install NGINX using the following commands.

sudo apt-get update
sudo apt-get install nginx
sudo service nginx start

NGINX uses files listed in /etc/nginx/sites-available to determine which URLs are protected by it. The default file in this directory is the fall through URL if no other files match the URL. I set the default file to be our 2FA website so all sites would be protected by the authenticator. Here is a sample of the default config on ports 80 and 443.

server 
{	listen 80 default_server;	listen [::]:80 	default_server;
	server_name _;
}

server {	
	listen 443 ssl http2 default_server;
	listen [::]:443 ssl http2 default_server;
	server_name nginx.<domain>;
	ssl_certificate /etc/nginx/chain.pem;
	ssl_certificate_key /etc/nginx/key.pem;

The location / in the config file means everything under the root URL. To setup the Dotnet Core application, use a config similar to the following.

location / {		
	proxy_pass http://localhost:5000;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection keep-alive;				
    proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header Host $host;
	proxy_cache_bypass $http_upgrade;	
} 

For the application you are protecting, you will create a file in the /etc/nginx/sites-available named the URL you are protecting. For example, app.domain. We will walk through the configuration of the file later, but once the file is created you will create a link to the file in /etc/nginx/sites-enabled using the following Linux command.

ln -s /etc/nginx/sites-available/<app-config> /etc/nginx/sites-enabled

After creating the link, you restart the nginx server using the following command.

sudo service nginx restart

Here is a sample of the app’s NGINX config. Note the auth_request /auth and the error_page 401. The error_page location tells the server to redirect to your authentication app and send in the ReturnUrl parameter including the servername and request URI.

location / {		
	auth_request /auth;		
	error_page 401 = @error401;				
    proxy_pass http://<internal ip>;		
    proxy_set_header X-Real-IP $remote_addr;		
    proxy_set_header X-Original-URI $request_uri;	
}

location / {		
	auth_request /auth;		
	error_page 401 = @error401;				
    proxy_pass http://<internal ip>;		
    proxy_set_header X-Real-IP $remote_addr;		
    proxy_set_header X-Original-URI $request_uri;	
}

location @error401 {
	return 302 https://nginx/Account/Login?ReturnUrl=$server_name$request_uri;	
}

The last piece of the puzzle is to create a service on the NGINX server to start the Dotnet Core application. Below is a sample config.

[Unit]
Description=<Display name>
[Service]
WorkingDirectory=/var/www/<proxy folder>
ExecStart=/usr/bin/dotnet /var/www/<proxy folder>/<proxy>.dll
Restart=always
RestartSec=10
SyslogIdentifier=<proxy>
User=Ubuntu
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target

Those are the primary steps to setting up the application. If you have problems and need to troubleshoot, there are a few steps you can try. To determine if the Dotnet Core application is up and running, you can run the curl command from the NGINX server.

curl http://localhost:5000

You can use the NGINX logs located in /var/log/nginx. The access.log file shows successful connections and error.log shows any issues with the NGINX server. If you need to find the Dotnet Core process can kill it, you can use:

sudo netstat –ltnp
sudo kill <pid>

There you have it. These are my thoughts on setting up a Dotnet Core application on an NGINX box to use 2FA in front of a website.

Happy CloudTrails to you.