Thursday | 21 NOV 2024
[ previous ]
[ next ]

HTTP Basic Auth in JavaScript

Title:
Date: 2024-02-27
Tags:  

Table of Contents

  1. The Code

This is version 2 of a similar [post I made before].

I have a feeling that authentication should be simple but isn't and http basic auth might be the answer. The idea of it is simple, in the header of each request, pass the username and password and then server can valdiate each request. The problem originally was that the data was sent in the clear and so basic authentication wasn't good. Passing a session token in the clear is bad but at least session tokens can be invalidated and the user can log in again. If the password leaks, the user needs to change their password and possible in many places.

This drawback goes away when you use https. Now the credentials are encrypted and can't be easily seen. Basic auth seems like a valid option now as it is safe. It still has the drawback that in the case that passing the username and password combo with every request means that if it's ever broken you need to change passwords.

Regardless I like the idea of basic auth using it requires just a little set up in nginx. The big gain is that I don't have to set up a backend server to handle authentication.

I however also don't want to rely on the browser's login prompt when basic auth is triggered. I think being able to log in with a form is much better but there is no way to have the form work with basic auth directly. Instead we need to use javascript to disable the form and submit the request using ajax.

I originally used fetch but fetch seems to only send the header properly on the request it is on. Any future requests don't get the authentication header attached properly. Switching this to use XMLHttpRequest got it to work pretty smoothly.

The Code

The first thing to do is to create a htpasswd and add a user.

htpasswd -c /etc/nginx/htpasswd username 

This creates the file. If the file already exists, it will be overwritten.

To append to the file, aka adding a user:

htpasswd /etc/nginx/htpasswd newUser

Now we can update the nginx configuration:

   location /directory {
      auth_basic "Protected Area";
      auth_basic_user_file /etc/nginx/htpasswd;
   }

Restart nginx and we should now get a browser prompt when trying to visit /directory.

The next step is to create an index page with a form that will redirect to /directory after logging in.

<!DOCTYPE html>
<script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.es.js?module'
    
    let form = null;
    
    const urlParams = new URLSearchParams(window.location.search);
    const err = urlParams.get("error");
    
    if (err) {
        form = { error: "Invalid login." };
    }
    
    async function login() {
        let url = "/directory";
        
        var http = new XMLHttpRequest();
        http.open("get", url, false, this.username, this.password);
        http.send("");
        
        if (http.status === 200) {
            document.cookie = `username=${this.username}`;
            window.location = "/directory";
        } else {
            this.form = { error: "Invalid login." } 
        }
    }
    
    createApp({
        username: "guest",
        password: "nt1",
        login,
        form,
    }).mount("#app");
</script>

<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Login</title>
        <link rel="icon" href="data:;base64,=">
    </head>
    <body id="app">
        <form @submit.prevent="login">
            <div>
                <label for="username">Username</label>
                <input type="text" name="username" v-model="username">
            </div>
 
            <div>
                <label for="password">Password</label>
                <input type="password" name="password" v-model="password">
            </div>
            <div class="flex">
                <div class="error flex-1">
                    <span v-if="form" v-cloak>{{ form.error }}</span>
                </div>
                <button class="button">Log In</button>
            </div>
        </form>
    </body>
</html>

This uses petitevue but the logic should work regardless.

We have the form for the username and password and we prevent the submit. Instead of submitting the form, we will call the javascript function login.

The login function will create an xhr request and supply the username and password and this will get authenticated by nginx. Once it has been authenticated and we get a ok response from the server we can then redirect to the protected route.

This is the core idea of using http basic authentication with javascript and it's a simple way of doing logins without setting up a whole backend.

Logout is similar:

    async function logout() {
        let url = "/stream-v5/v/";
        
        const username = 'fakeUser';
        const password = 'randomPassword';
        
        var http = new XMLHttpRequest();
        http.open("get", url, false, username, password);
        http.send("");
        
        document.cookie = "";
        window.location = "/stream-v5/";
    }

Instead of passing the correct credentials, we send an incorrect one and this serves to change the header to a user that doesn't exist.

The entire system is quite finnicky though so I would never use this in production but I think it has value as a toy, especially when you want to quickly get something going.

This did make me want to write a very simple authentication server in something like go. A simple binary that sets up users and creates sessions for them and can then be dropped into nginx would be great. Nginx has the ability to use subsrequests for authentication so you can protect a route by specifying the server that is to be used for auth.

Likely that someone has already done it and I just need to find it.

Overall though this works quite well as you can now have a regular login page and use http basic auth without relying on the browser's log in prompt.