React SPA + nginx + basic auth + SSL

· udik's blog


Often, there is a need to restrict access for a simple project (SPA + api). Instead of adding authorization methods to the API (JWT, sessions, etc.), we can use an old but reliable method - HTTP Basic Auth. Let's assume we have:

  1. Install nginx

    1sudo apt install nginx
    
  2. Create the Password File. Be sure apache2-utils has been installed (Debian, Ubuntu)

    1sudo htpasswd -c /etc/nginx/htpasswd myuser
    

    We have created a new user for the site with the username myuser and the password we typed. If we want to add more users, we should omit the -c flag because the file already exists:

    1sudo htpasswd /etc/nginx/htpasswd notmyuser
    
  3. Create a new file in the /etc/nginx/sites-available

    1sudo nano /etc/nginx/sistes-available/mysite
    

    with the following content:

    server {
        listen 80;
        # listen [::]:80; # uncomment if you want IPv6 as well
        root /var/www/tradelink;
        index index.html;
        server_name mysite.com;
    
        # serve static content (frontend)
        location / {
            auth_basic "Administrator’s Area";
            auth_basic_user_file /etc/nginx/htpasswd;
    
            # try serve files and in case of 404 server index.html - for React router
            try_files $uri /index.html;
        }
    
        # proxy pass to the backend api server
        location /v0 {
            auth_basic "Administrator’s Area";
            auth_basic_user_file /etc/nginx/htpasswd;
            proxy_pass http://127.0.0.1:3000;
        }
    }		
    
  4. Enable new config

    1sudo ln -s /etc/nginx/sistes-available/mysite /etc/nginx/sistes-enabled/mysite
    
  5. Apply the new config

    1sudo systemctl restart nginx
    

    Navigate to http://mysite.com via you browser, enter credentials for myuser and your SPA now should work as expected.

Adding SSL #

Obtaining SSL sertificates #

Using plain http is a bad practice. Moreover, every request with HTTP Basic Auth includes an unencrypted Authorization header, which could be easily eavesdropped on (basically the value of the Authorization header is base64-encoded string like username:password). You have to use SSL with HTTP Basic Auth EVRY TIME! Nowadays we have a great Lets's Encrypt. Let's use its certbot to obtain an SSL certificate for our mysite.com.

1sudo apt update
2sudo apt install python3 python3-venv libaugeas0
3sudo python3 -m venv /opt/certbot/
4sudo /opt/certbot/bin/pip install --upgrade pip
5sudo /opt/certbot/bin/pip install certbot certbot-nginx
6sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot
1sudo certbot certonly --nginx

After answering a couple of questions we should successfully obtain our certificates:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/mysite.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/mysite.com/privkey.pem

Setting up Nginx to work with SSL

Lets modify /etc/nginx/sites-available:

server {
    listen 80;
    listen 433 ssl;

	# SSL config
	ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
	ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # If you need more security use use only TLSv1.2, dont use SSLv3/TLSv1/TLSv1.1


	root /var/www/tradelink;
	index index.html;
	server_name mysite.com;

	# serve static content (frontend)
	location / {
		auth_basic "Administrator’s Area";
		auth_basic_user_file /etc/nginx/htpasswd;

		# try serve files and in case of 404 server index.html - for React router
		try_files $uri /index.html;
	}

	# proxy pass to the backend api server
	location /v0 {
		auth_basic "Administrator’s Area";
		auth_basic_user_file /etc/nginx/htpasswd;
		proxy_pass http://127.0.0.1:3000;
	}
}	

Restart nginx and your app should be available on both http://mysite.com and https://mysite.com sachems. Now lets do some tweaks to finish our setup.

Redirect from http to https #

To make an app more user-friendly and secure, we should set up automatic redirect from http to https if a user somehow navigates to http version of the app. To do that lets create a new file:

1sudo nano /etc/nginx/sistes-available/http2https

with the following content:

server {
    listen  80 default_server;
    # listen  [::]:80 default_server; # uncomment if you want IPv6
    server_name _;

    location / {
        return 301 https://$host$request_uri;
    }
}

These lines tells nginx to listen 80-th port (default http port) as a default_server for all domains in the request (server_name _; line). It returns 301 (Moved permanently) to https version of the original URI.

Don't forget to enable this config:

1sudo ln -s /etc/nginx/sistes-available/http2https /etc/nginx/sistes-enabled/http2https

And modify /etc/nginx/sistes-available/mysite deleting listen 80 line:

server {
    listen 433 ssl;

	# SSL config
	ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
	ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # If you need more security use use only TLSv1.2, dont use SSLv3/TLSv1/TLSv1.1


	root /var/www/tradelink;
	index index.html;
	server_name mysite.com;

	# serve static content (frontend)
	location / {
		auth_basic "Administrator’s Area";
		auth_basic_user_file /etc/nginx/htpasswd;

		# try serve files and in case of 404 server index.html - for React router
		try_files $uri /index.html;
	}

	# proxy pass to the backend api server
	location /v0 {
		auth_basic "Administrator’s Area";
		auth_basic_user_file /etc/nginx/htpasswd;
		proxy_pass http://127.0.0.1:3000;
	}
}	

Restart nginx again. Now the app serves only by https protocol. Great.

P.S. You can always check if you have not fucked up config with:

1nginx -t

Automatically renewing certificates #

Currently Let's Encrypt issues SSL certificate only for 3 months period. And you have to manually renew them. But don't waste your time! Add this line to you crontab (crontab -e):

00 04 01 */3 * certbot renew --nginx

This will automatically renew certificate every 3 months.

Conclusion #

Sometimes an SPA needs restricted access, but if you don't want to manage it through authorization mechanisms in your backend, using Nginx's HTTP Basic Auth could be a good option. If the backend still needs to know which user sent the request, it can use the Authorization header in the request, which contains user's login and password.