Posted on 3 minutes read

Previously, we configured nginx to list directories and files in html. Now, let’s add authentication to restrict access to specific directories and files.

Basic authentication

The simplest web authentication method is basic authentication. It requires an Authorization header in each request. However, browsers don’t support setting a header on requests without javascript, and with javascript, there’s no sane way to stream file downloads to the browser’s download manager.

So, once the initial basic auth login is validated, we'll use cookies, which browsers automatically include for each request on the same site.

Let’s start by creating a login endpoint with nginx basic auth to verify the user/password pair and set a cookie for users with valid credentials.

...
server {
    ...
    location @login_success {
        add_header Set-Cookie "ngxp=???; max-age=15552000; path=/; SameSite=Lax; Secure; HttpOnly";
        return 200 "login success";
    }
    location ^~ /___ngxp/login {
        auth_basic "Login";
        auth_delay 1s;
        auth_basic_user_file /opt/ngxp/basic.htpasswd;
        try_files _ @login_success;
    }
}

To create entries in basic.htpasswd, we use openssl[1]:

printf '%s:%s\n' "$user" "$(openssl passwd -5 "$password")" >> basic.htpasswd

We can successfully login using basic auth on the login endpoint!

Cookie set

To check that the user is authorized to access the current path and that cookies cannot be hand-crafted, we need to store the following in the cookie:

  • username
  • a secret, so a cookie cannot be hand crafted
  • the path base the user has access to

Using nginx map module, we’ll store this information for each user:

#     input     output
map $username $user_cookie {
    default "";
    admin   admin:d31d6b17bbc4df3ccb78d99db085e9d347b3e042e65d5878:/;
    alice   alice:2406b10bfd4c356394a647d5657bde937124f602198f20f3:/alicefiles;
}

Later, with map's include directive we can store it into a file.

In @login_success, assign the ngxp cookie value. When $username is set with the basic auth username ($remote_user), $user_cookie is set with the mapped value.

    location @login_success {
        set $username $remote_user; # $remote_user is the basic_auth user in Authorization http header
        add_header Set-Cookie "ngxp=$user_cookie; max-age=15552000; path=/; SameSite=Lax; Secure; HttpOnly";
        return 200 "login success";
    }

Cookie check

The only thing left to do is to check if our cookie is valid and authorized for the current uri when listing files. $cookie_ngxp contains the http request provided cookie (from the browser).

# get username from http request's cookie
map $cookie_ngxp $username {
    ~^(?<var>[^:]+):.* $var;
    default            "";
}

It's an untrusted user input, so we should check that it matches exactly with the one set for the username.

Since we just set $username, $user_cookie has the trusted cookie value for the user (if it exists), so we check that both variables are equal:

map ${cookie_ngxp}?${user_cookie} $user_cookie_equal {
    ~^([^\?]+)\?\1$   1; # if $cookie_ngxp == $user_cookie
    default           0;
}

By modifying location / slightly, we can already restrict listing and download to requests with a valid cookie:

...
server {
    ...
    location / {
        if ($user_cookie_equal = 0) { return 401; }
        ...
    }
}

Cookie access

We also want to have per path access rights, we first extract the path from $user_cookie:

...
map $user_cookie $user_cookie_access {
    ~^([^:]+):([^:]+):(?<var>[^:]+)$ $var;
    default                          "";
}

Using a map with two variable as input, we can check if $uri starts with the extracted path

...
map ${user_cookie_access}?${uri} $uri_access {
    ~^([^\?]+)\?\1.*$ 1; # if $uri startswith $user_cookie_access
    default           0;
}

Cookie authorized

Finally, ensure that both the cookie and path-based access rights are validated:

...
map ${user_cookie_equal}${uri_access} $user_authorized {
    11          1; # everything good authorize
    default     0;
}

In the file listing endpoint, check $user_authorized:

...
server {
    ...
    location / {
        if ($user_authorized = 0) { return 401; }
        ...
    }
}

We now have per user and path accesses!

Next time we'll see how to upload files.


  1. You can also use htpasswd. ↩