Polish it up + readme work

This commit is contained in:
Neale Pickett 2023-02-18 17:40:35 -07:00
parent 78d532a88b
commit 53d04746b3
3 changed files with 115 additions and 69 deletions

172
README.md
View File

@ -1,90 +1,132 @@
The canonical home for this project is
https://git.woozle.org/neale/simpleauth
# Simple Auth # Simple Auth
All this does is present a login page. This is a stateless forward-auth provider.
Upon successful login, the browser gets a cookie, I tested it with Caddy, but it should work fine with Traefik.
and further attempts to access will get the success page.
I made this to use with the Traefik forward-auth middleware. # Theory of Operation
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.
## 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, * Username
until there's a Go library that supports everything. * 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. This will use `/dev/urandom` to generate a 64-byte secret key.
Make sure it can read your `passwd` file,
which you set up in the previous section.
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 { private.example.com {
forward_auth localhost:8080 {
uri / uri /
copy_headers X-Simpleauth-Token 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, `header_down` sets the
I had it set up like this: `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 ### Traefik
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"
secrets: I need someone to send me equivalent
password: traefik
file: password configuration,
name: password-v1 to include here.
```
# How It Works
Simpleauth uses a token cookie, in addition to HTTP Basic authentication. ### nginx
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.
If you don't want keys to persist across service invocations / reboots, I need someone to send me equivalent
you can pass in `-secret /dev/urandom`. 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.

View File

@ -55,13 +55,16 @@ func usernameIfAuthenticated(req *http.Request) string {
func rootHandler(w http.ResponseWriter, req *http.Request) { func rootHandler(w http.ResponseWriter, req *http.Request) {
var status string var status string
username := usernameIfAuthenticated(req) username := usernameIfAuthenticated(req)
login := req.Header.Get("X-Simpleauth-Login") == "true"
browser := strings.Contains(req.Header.Get("Accept"), "text/html")
if username == "" { if username == "" {
status = "failed" status = "failed"
} else { } else {
status = "succeeded" status = "succeeded"
w.Header().Set("X-Simpleauth-Username", username) 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 // This is the only time simpleauth returns 200
// That will cause Caddy to proceed with the original request // That will cause Caddy to proceed with the original request
http.Error(w, "Success", http.StatusOK) 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("Content-Type", "text/html")
w.Header().Set("X-Simpleauth-Authentication", status) w.Header().Set("X-Simpleauth-Authentication", status)
w.Header().Set("WWW-Authenticate", "Simpleauth-Login") 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 // Make browsers use our login form instead of basic auth
w.Header().Add("WWW-Authenticate", "Basic realm=\"simpleauth\"") w.Header().Add("WWW-Authenticate", "Basic realm=\"simpleauth\"")
} }

View File

@ -51,8 +51,9 @@
} }
document.cookie = cookieStr // JavaScript butt-magic! document.cookie = cookieStr // JavaScript butt-magic!
location.reload() location.reload()
error("Success - your page should reload now")
return
} }
console.log(req.headers)
error(req.statusText || "Authentication failed") error(req.statusText || "Authentication failed")
} }