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.

last updated: