password protecting static web pages

Static site generators, by and large, do not have support for password protecting individual pages. This post shows a strategy that works for any SSG and does not require server access controls.

PageCrypt is the tool I went with. It’s a TypeScript library that uses the native Web Crypto API to encrypt html pages, stuff the encrypted gobbledigook behind a simple password form, and decrypt the page when the correct key is given.

I’m using the CLI version, but there are also native TypeScript and Node.js versions.

The basic idea is this, taken from their docs:

npx pagecrypt <src> <dest> [password] [options]

Below is an excerpt from a package.json for a Hugo site I maintain. First I build the site, then run the postbuild command, which stitches together a piece of code that creates a list of all the pages we’d like to encrypt and another bit that does the encryption proper.

{
    "make-pw-files-file": "grep -rlF 'password_required: true' content | sed s+\\.md+\\/index.html+g | sed s+content/+public/+g > pw_file",
    "protect-files": "cat pw_file | while read f || [[ -n $f ]]; do npx pagecrypt $f $f $PAGECRYPT; done",
    "postbuild": "npm run -s make-pw-files-file && npm run -s protect-files ",
}

make-pw-files-file

grep -rlF 'password_required: true' content | sed s+\\.md+\\/index.html+g | sed s+content/+public/+g > pw_file

This part uses a few standard Unix commands, the ubiquitous grep and sed.

First it creates a list of markdown source files that have the header password_required: true.

Here’s how that part works:

grep -rlF "text you're looking for" path/to/files/of/interest

This will output something like:

content/file0.md
content/file7.md
content/yet_another_file.md

Next it passes that to sed, which replaces .md with /index.html* resulting in:

content/file/index.html
content/file7/index.html
content/yet_another_file/index.html

* you’d have to adjust this command if you aren’t using a page-per-folder style

Then it switches out content/ with public/, leading to the final list:

public/file/index.html
public/file7/index.html
public/yet_another_file/index.html

and sends those to a file.

Sending the list to a file is not necessary, could just pipe it again to the stuff in protect-files, but I was getting itchy at how long the command was getting.


protect-files

cat pw_file | while read f || [[ -n $f ]]; do npx pagecrypt $f $f $PAGECRYPT; done

This part emits the contents of pw_file, then loops through each line (while read f || [[ -n $f ]]) and encrypts the corresponding file (do npx pagecrypt), finally saving it back to itself ($f $f).

$PAGECRYPT is an environment variable that specifies the password.

You could also set a password-per-page, either auto-generated (but you’d have to figure out how to get that password to your users), or using something like another header value in the .md files that you grep for and save somewhere. For my use case, this is simpler and scales well.


and that’s it

As always, let me know if you have any questions or know of a better way.