Polish it up + readme work
This commit is contained in:
parent
78d532a88b
commit
53d04746b3
174
README.md
174
README.md
|
@ -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 {
|
||||||
uri /
|
forward_auth localhost:8080 {
|
||||||
copy_headers X-Simpleauth-Token
|
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,
|
`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.
|
|
||||||
|
|
|
@ -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\"")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue