TRON Lives

WebAuthN Passkey Authentication through Ghost/NGINX/Redis "He fights for the user..."
Modern authentication shouldn't require passwords, and thanks to WebAuthn, it's now possible to implement secure, phishing-resistant login systems that rely on public-key cryptography and user presence (like biometrics or hardware keys). In this post, I walk through how I integrated WebAuthn-based passwordless 2FA into my Ghost Blog to protect sensitive content—without altering Ghost's core.
This setup allows me to secure any URI prefixed with /secure/
, or /premium/
, effectively adding a second layer of access control on top of Ghost’s existing magic link system.
🔧 Tech Stack
- Ghost: Content platform and primary authentication via magic link
- NGINX: Reverse proxy and URI-level gatekeeper
- Redis: In-memory store for ephemeral session verification
- FastAPI: Handles WebAuthn registration and authentication handshake
Ghosts authentication is already passwordless. Ghost uses a "magic link" sent to your email to set cookies for authenticating users. For those of us needing a second-layer of authentication, I want to keep it password-less, a second thing you have to HAVE to prove who you are. ...a device that uses biometrics to provide access to a private key which I can automate the challenge/response/verification process.
NGINX + Redis + FastAPI = WebAuthN
The first step is going through the creation of content structure on your blog site such that NGINX can use a location module for primary routing. This article takes you through the process of creating a protected section of your site using private tags and routing. Once you've created your "/secure/", "/premium/", or whatever you want to call your protected area, you simply create location module in nginx.conf with an "access_by_lua_*" block that uses your favorite automation style to check Redis for session state. Is the user auth'd already?
location /secure {
allow all;
set $backend "";
access_by_lua_file /etc/nginx/lua/secure.lua;
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Again, secure.lua
can be any flavor of automation you desire. The only thing it needs to do is check Redis for session state and set the $backend
to the blog site (authenticated) or your server side implementation of WebAuthN.
For my backend, I chose to use a forked/modded version of Duo-security's python implementation of WebAuthN. I also chose FastAPI for my back end. I prefer the ease of routing/function mapping and even the builtin documentation that comes with it. Duo's implementation includes four easy-to-understand methods for handling the registration/authentication process:
generate_registration_options()
verify_registration_response()
generate_authentication_options()
verify_authentication_response()
To make things seamless, you need the right front end. I lightly customized/modded the contributions of SimpleWebAuthN which has a FIDO conformant implementation ready for you to use as your front-end. All that's needed is the inclusion of javascript for SimpleWebAuthN and your implementation of the two methods below (which they've done and you can modify).
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
startRegistration()
startAuthentication()
I wanted to include a cool video to show the ease of use, but alas, security constraints restrict accessing passkeys while the screen is recording. lol. If you need help with security, authentication, automation, AI reach out to us at DragonBox Solar!