From 53d04746b3b048d52f015afadfee2cb2479d01a8 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 18 Feb 2023 17:40:35 -0700 Subject: [PATCH] Polish it up + readme work --- README.md | 174 +++++++++++++++++++++++++---------------- cmd/simpleauth/main.go | 7 +- web/login.html | 3 +- 3 files changed, 115 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 18140e3..478b2aa 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,132 @@ -The canonical home for this project is -https://git.woozle.org/neale/simpleauth - # Simple Auth -All this does is present a login page. -Upon successful login, the browser gets a cookie, -and further attempts to access will get the success page. +This is a stateless forward-auth provider. +I tested it with Caddy, but it should work fine with Traefik. -I made this to use with the Traefik forward-auth middleware. -I now use Caddy: it works with that too. -All I need is a simple password, that's easy to fill with a password manager. -This checks those boxes. +# Theory of Operation -## Format of the `passwd` file +This issues cryptographically signed authentication tokens to the client. +Some JavaScript stores the token in a cookie. -It's just like `/etc/shadow`. +When a client presents an authentication token in a cookie, +they are allowed in if the token was properly signed, +and has not expired. - username:crypted-password +Authentication tokens consist of: -We use sha256, -until there's a Go library that supports everything. +* Username +* Expiration date +* Hashed Message Authentication Code (HMAC) -There's a program included called `crypt` that will output lines for this file. +Simpleauth also works with HTTP Basic authentication. + +# Setup + +Simpleauth needs two (2) files: + +* A secret key, to sign authentication tokens +* A list of usernames and hashed passwords -## Installation with Caddy +## Create secret key -Run simpleauth as a service. -Make sure it can read your `passwd` file, -which you set up in the previous section. +This will use `/dev/urandom` to generate a 64-byte secret key. -You'll want a section like this in your Caddyfile: +```sh +SASECRET=/run/secrets/simpleauth.key # Set to wherever you want your secret to live +dd if=/dev/urandom of=$SASECRET bs=1 count=64 +``` + + +## Create password file + +It's just a text file with hashed passwords. +Each line is of the format `username:password_hash` + +```sh +alias sacrypt="docker run --rm --entrypoint=/crypt git.woozle.org/neale/simpleauth" +SAPASSWD=/run/secrets/passwd # Set to wherever you want your password file to live +: > $SAPASSWD # Reset password file +sacrypt user1 password1 >> $SAPASSWD +sacrypt user2 password2 >> $SAPASSWD +sacrypt user3 password3 >> $SAPASSWD +``` + + +## Start it + +Turning this into the container orchestration system you prefer +(Docker Swarm, Kubernetes, Docker Compose) +is left as an exercise for the reader. + +```sh +docker run \ + --name=simpleauth \ + --detach \ + --restart=always \ + --port 8080:8080 \ + --volume $SASECRET:/run/secrets/simpleauth.key:ro \ + --volume $SAPASSWD:/run/secrets/passwd:ro \ + git.woozle.org/neale/simpleauth +``` + +## Make your web server use it + +### Caddy + +You'll want a `forward-auth` section like this: ``` -forward_auth simpleauth:8080 { - uri / - copy_headers X-Simpleauth-Token +private.example.com { + forward_auth localhost:8080 { + uri / + copy_headers X-Simpleauth-Username + header_down X-Simpleauth-Domain example.com # Set cookie for all of example.com + } + respond "Hello, friend!" } ``` -## Installation with Traefik +The `copy_headers` directive tells Caddy to pass +Simpleauth's `X-Simpleauth-Username` header +along in the HTTP request. +If you are reverse proxying to some other app, +it can look at this header to determine who's logged in. -I don't use Traefik any longer, but when I did, -I had it set up like this: +`header_down` sets the +`X-Simpleauth-Domain` header in HTTP responses. +The only time a client would get an HTTP response is when it is not yet authenticated. +The built-in JavaScript login page uses this header to set the cookie domain: +this way, you can protect multiple sites within a single cookie -```yaml -services: - my-cool-service: - # All your cool stuff here - deploy: - labels: - # Keep all your existing traefik stuff - traefik.http.routers.dashboard.middlewares: forward-auth - traefik.http.middlewares.forward-auth.forwardauth.address: http://simpleauth:8080/ - simpleauth: - image: ghcr.io/nealey/simpleauth - secrets: - - password - - simpleauth.key - deploy: - labels: - traefik.enable: "true" - traefik.http.routers.simpleauth.rules: "PathPrefix(`/`)" - traefik.http.services.simpleauth.loadbalancer.server.port: "8080" +### Traefik -secrets: - password: - file: password - name: password-v1 -``` +I need someone to send me equivalent +traefik +configuration, +to include here. -# How It Works -Simpleauth uses a token cookie, in addition to HTTP Basic authentication. -The token is an HMAC digest of an expiration timestamp, -plus the timestamp. -When the HMAC is good, and the timestamp is in the future, -the token is a valid authentication. -This technique means there is no persistent server storage. +### nginx -If you don't want keys to persist across service invocations / reboots, -you can pass in `-secret /dev/urandom`. +I need someone to send me equivalent +nginx +configuration, +to include here. + + +# Why not some other thing? + +The main reason is that I couldn't get the freedesktop.org +WebDAV client code to work with anything else I found. + +* Authelia - I like it, but I couldn't get WebDAV to work. Also, it used 4.8GB of RAM and wanted a Redis server. +* Authentik - Didn't try it, looked too complicated. +* Keycloak - Didn't try it, looked way too complicated. + + +# Project Home + +The canonical home for this project is +https://git.woozle.org/neale/simpleauth -Some things, -like WebDAV, -will only ever use HTTP Basic auth. -That's okay: -Simpleauth will issue a new token for every request, -and the client will ignore it. diff --git a/cmd/simpleauth/main.go b/cmd/simpleauth/main.go index 2708831..8733155 100644 --- a/cmd/simpleauth/main.go +++ b/cmd/simpleauth/main.go @@ -55,13 +55,16 @@ func usernameIfAuthenticated(req *http.Request) string { func rootHandler(w http.ResponseWriter, req *http.Request) { var status string username := usernameIfAuthenticated(req) + login := req.Header.Get("X-Simpleauth-Login") == "true" + browser := strings.Contains(req.Header.Get("Accept"), "text/html") + if username == "" { status = "failed" } else { status = "succeeded" w.Header().Set("X-Simpleauth-Username", username) - if req.Header.Get("X-Simpleauth-Login") != "true" { + if !login { // This is the only time simpleauth returns 200 // That will cause Caddy to proceed with the original request http.Error(w, "Success", http.StatusOK) @@ -86,7 +89,7 @@ func rootHandler(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/html") w.Header().Set("X-Simpleauth-Authentication", status) w.Header().Set("WWW-Authenticate", "Simpleauth-Login") - if !strings.Contains(req.Header.Get("Accept"), "text/html") { + if !login && !browser { // Make browsers use our login form instead of basic auth w.Header().Add("WWW-Authenticate", "Basic realm=\"simpleauth\"") } diff --git a/web/login.html b/web/login.html index 77af808..5d62fc2 100644 --- a/web/login.html +++ b/web/login.html @@ -51,8 +51,9 @@ } document.cookie = cookieStr // JavaScript butt-magic! location.reload() + error("Success - your page should reload now") + return } - console.log(req.headers) error(req.statusText || "Authentication failed") }