diff --git a/README.md b/README.md index ecae632..aa6a9f1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ 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 It's just like `/etc/shadow`. @@ -22,11 +21,25 @@ until there's a Go library that supports everything. There's a program included called `crypt` that will output lines for this file. +## Installation with Caddy + +Run simpleauth as a service. +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: + +``` +forward_auth simpleauth:8080 { + uri / + copy_headers X-Simpleauth-Token +} +``` + ## Installation with Traefik -You need to have traefik forward the Path `/` to this application. - -I only use docker swarm. You'd do something like the following: +I don't use Traefik any longer, but when I did, +I had it set up like this: ```yaml services: @@ -53,10 +66,20 @@ secrets: name: password-v1 ``` -## Note +# How It Works -For some reason that I haven't bothered looking into, -I have to first load `/` in the browser. -I think it has something to do with cookies going through traefik simpleauth, -and I could probably fix it with some JavaScript, -but this is good enough for me. +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, +but also means that if the server restarts, +everybody has to log in again. + +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 0616306..faba890 100644 --- a/cmd/simpleauth/main.go +++ b/cmd/simpleauth/main.go @@ -17,23 +17,18 @@ import ( "github.com/nealey/simpleauth/pkg/token" ) -const CookieName = "auth" +const CookieName = "simpleauth-token" var secret []byte = make([]byte, 256) var lifespan time.Duration var cryptedPasswords map[string]string var loginHtml []byte -var successHtml []byte func authenticationValid(username, password string) bool { c := crypt.SHA256.New() - fmt.Println("checking", username, password) if crypted, ok := cryptedPasswords[username]; ok { - fmt.Println(username, password, crypted) if err := c.Verify(crypted, []byte(password)); err == nil { return true - } else { - log.Println(err) } } return false @@ -43,23 +38,19 @@ func rootHandler(w http.ResponseWriter, req *http.Request) { if cookie, err := req.Cookie(CookieName); err == nil { t, _ := token.ParseString(cookie.Value) if t.Valid(secret) { - // Bypass logging and cookie setting: - // otherwise there is a torrent of logs - w.Write(successHtml) + fmt.Print(w, "Valid token") return } } - authenticated := "" - - if username, password, ok := req.BasicAuth(); ok { - if authenticationValid(username, password) { - authenticated = "HTTP-Basic" - } + acceptsHtml := false + if strings.Contains(req.Header.Get("Accept"), "text/html") { + acceptsHtml = true } - if authenticationValid(req.FormValue("username"), req.FormValue("password")) { - authenticated = "Form" + authenticated := false + if username, password, ok := req.BasicAuth(); ok { + authenticated = authenticationValid(username, password) } // Log the request @@ -67,23 +58,40 @@ func rootHandler(w http.ResponseWriter, req *http.Request) { if clientIP == "" { clientIP = req.RemoteAddr } - log.Printf("%s %s %s [%s]", clientIP, req.Method, req.URL, authenticated) + log.Printf("%s %s %s [auth:%v]", clientIP, req.Method, req.URL, authenticated) - if authenticated == "" { + if !authenticated { + w.Header().Set("Content-Type", "text/html") + if !acceptsHtml { + w.Header().Set("WWW-Authenticate", "Basic realm=\"simpleauth\"") + } w.WriteHeader(http.StatusUnauthorized) w.Write(loginHtml) - } else { - t := token.New(secret, time.Now().Add(lifespan)) - http.SetCookie(w, &http.Cookie{ - Name: CookieName, - Value: t.String(), - Path: "/", - Secure: true, - SameSite: http.SameSiteStrictMode, - }) - w.WriteHeader(http.StatusOK) - w.Write(successHtml) + return } + + // Set Cookie + t := token.New(secret, time.Now().Add(lifespan)) + http.SetCookie(w, &http.Cookie{ + Name: CookieName, + Value: t.String(), + Path: "/", + Secure: true, + SameSite: http.SameSiteStrictMode, + }) + + // Set cookie value in our fancypants header + w.Header().Set("X-Simpleauth-Token", t.String()) + + if req.Header.Get("X-Simpleauth-Login") != "" { + // Caddy treats any response <300 as "please serve original content", + // so we'll use 302 (Found). + // According to RFC9110, the server SHOULD send a Location header with 302. + // We don't do that, because we don't know where to send you. + // It's possible 300 is a less incorrect code to use here. + w.WriteHeader(http.StatusFound) + } + fmt.Fprintln(w, "Authenticated") } func main() { @@ -127,7 +135,6 @@ func main() { if len(parts) >= 2 { username := parts[0] password := parts[1] - fmt.Println(username, password) cryptedPasswords[username] = password } } @@ -139,10 +146,6 @@ func main() { if err != nil { log.Fatal(err) } - successHtml, err = ioutil.ReadFile(path.Join(*htmlPath, "success.html")) - if err != nil { - log.Fatal(err) - } // Read in secret f, err := os.Open(*secretPath) diff --git a/static/login.html b/static/login.html index beb0b33..ae8efb3 100644 --- a/static/login.html +++ b/static/login.html @@ -3,7 +3,6 @@