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:
- Ubuntu/Debian VPS
- A domain,
mysite.com
, that points to IP of our VPS machine - Static content for the built frontend located in
/var/www/mysite
- API calls from the frontend going to
mysite.com/v0
- A backend running on the VPS as a server, available locally on the port 3000 at
127.0.0.1:3000
-
Install
nginx
1sudo apt install nginx
-
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
-
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; } }
-
Enable new config
1sudo ln -s /etc/nginx/sistes-available/mysite /etc/nginx/sistes-enabled/mysite
-
Apply the new config
1sudo systemctl restart nginx
Navigate to
http://mysite.com
via you browser, enter credentials formyuser
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.