From 78d532a88b96986f8ddde23d13fea0dd24da98e7 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 18 Feb 2023 16:39:18 -0700 Subject: [PATCH] fixes --- build.sh | 2 +- cmd/simpleauth/main.go | 86 ++++++++++++++++++++--------------------- pkg/token/token.go | 60 ++++++++++++++-------------- pkg/token/token_test.go | 26 +++++++++++++ web/login.html | 62 ++++++++++++++--------------- 5 files changed, 130 insertions(+), 106 deletions(-) create mode 100644 pkg/token/token_test.go diff --git a/build.sh b/build.sh index 4990407..bbc61a2 100755 --- a/build.sh +++ b/build.sh @@ -4,4 +4,4 @@ set -e tag=git.woozle.org/neale/simpleauth:latest -docker buildx --push --tag $tag $(dirname $0)/. +docker buildx build --push --tag $tag $(dirname $0)/. diff --git a/cmd/simpleauth/main.go b/cmd/simpleauth/main.go index beb35fc..2708831 100644 --- a/cmd/simpleauth/main.go +++ b/cmd/simpleauth/main.go @@ -12,9 +12,9 @@ import ( "strings" "time" + "git.woozle.org/neale/simpleauth/pkg/token" "github.com/GehirnInc/crypt" _ "github.com/GehirnInc/crypt/sha256_crypt" - "git.woozle.org/neale/simpleauth/pkg/token" ) const CookieName = "simpleauth-token" @@ -34,23 +34,46 @@ func authenticationValid(username, password string) bool { return false } -func rootHandler(w http.ResponseWriter, req *http.Request) { +func usernameIfAuthenticated(req *http.Request) string { if cookie, err := req.Cookie(CookieName); err == nil { t, _ := token.ParseString(cookie.Value) if t.Valid(secret) { - fmt.Print(w, "Valid token") - return + return t.Username } } - acceptsHtml := false - if strings.Contains(req.Header.Get("Accept"), "text/html") { - acceptsHtml = true + authUsername, authPassword, ok := req.BasicAuth() + if ok { + if authenticationValid(authUsername, authPassword) { + return authUsername + } } - authenticated := false - if username, password, ok := req.BasicAuth(); ok { - authenticated = authenticationValid(username, password) + return "" +} + +func rootHandler(w http.ResponseWriter, req *http.Request) { + var status string + username := usernameIfAuthenticated(req) + if username == "" { + status = "failed" + } else { + status = "succeeded" + w.Header().Set("X-Simpleauth-Username", username) + + if req.Header.Get("X-Simpleauth-Login") != "true" { + // This is the only time simpleauth returns 200 + // That will cause Caddy to proceed with the original request + http.Error(w, "Success", http.StatusOK) + return + } + // Send back a token; this will turn into a cookie + t := token.New(secret, username, time.Now().Add(lifespan)) + w.Header().Set("X-Simpleauth-Cookie", fmt.Sprintf("%s=%s", CookieName, t.String())) + w.Header().Set("X-Simpleauth-Token", t.String()) + // Fall through to the 401 response, though, + // so that Caddy will send our response back to the client, + // which needs these headers to set the cookie and try again. } // Log the request @@ -58,40 +81,17 @@ func rootHandler(w http.ResponseWriter, req *http.Request) { if clientIP == "" { clientIP = req.RemoteAddr } - log.Printf("%s %s %s [auth:%v]", clientIP, req.Method, req.URL, authenticated) + log.Println(clientIP, req.Method, req.URL, status, username) - 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) - return + 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") { + // Make browsers use our login form instead of basic auth + w.Header().Add("WWW-Authenticate", "Basic realm=\"simpleauth\"") } - - // 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") + w.WriteHeader(http.StatusUnauthorized) + w.Write(loginHtml) } func main() { @@ -154,7 +154,7 @@ func main() { } defer f.Close() l, err := f.Read(secret) - if l < 8 { + if l < 64 { log.Fatalf("Secret file provided %d bytes. That's not enough bytes!", l) } else if err != nil { log.Fatal(err) diff --git a/pkg/token/token.go b/pkg/token/token.go index 529b250..97a5296 100644 --- a/pkg/token/token.go +++ b/pkg/token/token.go @@ -5,38 +5,47 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" - "encoding/binary" + "encoding/gob" "log" "time" ) type T struct { - expiration time.Time - mac []byte + Expiration time.Time + Username string + Mac []byte } func (t T) computeMac(secret []byte) []byte { + zt := t + zt.Mac = nil + mac := hmac.New(sha256.New, secret) - binary.Write(mac, binary.BigEndian, t.expiration) + mac.Write(zt.Bytes()) return mac.Sum([]byte{}) } -// String returns the string encoding of the token -func (t T) String() string { +// Bytes encodes the token +func (t T) Bytes() []byte { f := new(bytes.Buffer) - if err := binary.Write(f, binary.BigEndian, t.expiration.Unix()); err != nil { + enc := gob.NewEncoder(f) + if err := enc.Encode(t); err != nil { log.Fatal(err) } - f.Write(t.mac) - return base64.StdEncoding.EncodeToString(f.Bytes()) + return f.Bytes() +} + +// String returns the ASCII string encoding of the token +func (t T) String() string { + return base64.StdEncoding.EncodeToString(t.Bytes()) } // Valid returns true iff the token is valid for the given secret and current time func (t T) Valid(secret []byte) bool { - if time.Now().After(t.expiration) { + if time.Now().After(t.Expiration) { return false } - if !hmac.Equal(t.mac, t.computeMac(secret)) { + if !hmac.Equal(t.Mac, t.computeMac(secret)) { return false } @@ -44,36 +53,25 @@ func (t T) Valid(secret []byte) bool { } // New returns a new token -func New(secret []byte, expiration time.Time) T { +func New(secret []byte, username string, expiration time.Time) T { t := T{ - expiration: expiration, + Username: username, + Expiration: expiration, } - t.mac = t.computeMac(secret) + t.Mac = t.computeMac(secret) return t } // Parse returns a new token from the given bytes func Parse(b []byte) (T, error) { - t := T{ - mac: make([]byte, sha256.Size), - } + var t T f := bytes.NewReader(b) - { - var sec int64 - if err := binary.Read(f, binary.BigEndian, &sec); err != nil { - return t, err - } - t.expiration = time.Unix(sec, 0) - } - if n, err := f.Read(t.mac); err != nil { - return t, err - } else { - t.mac = t.mac[:n] - } - return t, nil + dec := gob.NewDecoder(f) + err := dec.Decode(&t) + return t, err } -// ParseString parses a base64-encoded string, as created by T.String() +// ParseString parses an ASCII-encoded string, as created by T.String() func ParseString(s string) (T, error) { b, err := base64.StdEncoding.DecodeString(s) if err != nil { diff --git a/pkg/token/token_test.go b/pkg/token/token_test.go new file mode 100644 index 0000000..efe6905 --- /dev/null +++ b/pkg/token/token_test.go @@ -0,0 +1,26 @@ +package token + +import ( + "testing" + "time" +) + +func TestToken(t *testing.T) { + secret := []byte("bloop") + username := "rodney" + token := New(secret, username, time.Now().Add(10*time.Second)) + + if token.Username != username { + t.Error("Wrong username") + } + if !token.Valid(secret) { + t.Error("Not valid") + } + + tokenStr := token.String() + if nt, err := ParseString(tokenStr); err != nil { + t.Error("ParseString", err) + } else if nt.Username != token.Username { + t.Error("Decoded username wrong") + } +} diff --git a/web/login.html b/web/login.html index 5903348..77af808 100644 --- a/web/login.html +++ b/web/login.html @@ -19,45 +19,45 @@