package main import ( "context" "encoding/base64" "encoding/json" "errors" "hash/crc32" "log" "net/http" "os" "time" "github.com/google/uuid" ) var cookieSecret = os.Getenv("COOKIE_SECRET") type User struct { User string Group string Groups []string } func (user User) Is(other User) bool { for i := range user.Groups { if i >= len(other.Groups) || user.Groups[i] != other.Groups[i] { return false } } return user.User == other.User && user.Group == other.Group && len(user.Groups) == len(other.Groups) } type Cookie struct { Hash string Salt string Value string } func (server *Server) authenticate(w http.ResponseWriter, r *http.Request) (*Server, bool, error) { if done, err := server.parseLogin(w, r); err != nil { log.Printf("error parsing login: %v", err) return nil, false, err } else if done { log.Printf("login rendered body") return nil, true, nil } if ok, err := needsLogin(r); err != nil { log.Printf("error checking if login needed: %v", err) return nil, false, err } else if ok { log.Printf("needs login") promptLogin(w) return nil, true, nil } if done, err := changeNamespace(w, r); err != nil { return nil, false, err } else if done { return nil, true, nil } user, _ := loginCookie(r) return server.WithUser(user.User, user.Group, user.Groups), false, nil } func promptLogin(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", "Basic") w.WriteHeader(http.StatusUnauthorized) } func (server *Server) parseLogin(w http.ResponseWriter, r *http.Request) (bool, error) { username, password, ok := r.BasicAuth() if !ok { return false, nil } ok, err := server.auth.Login(username, password) if err != nil { return false, err } if !ok { promptLogin(w) return true, nil } groups, err := server.auth.Groups(username) if err != nil { return false, err } if len(groups) == 0 { return false, errors.New("user has no groups") } user := User{ User: username, Groups: groups, Group: groups[0], } olduser, _ := loginCookie(r) for i := range groups { if groups[i] == olduser.Group { user.Group = olduser.Group } } log.Printf("%+v => %+v", olduser, user) setLoginCookie(w, r, user) return false, nil } func changeNamespace(w http.ResponseWriter, r *http.Request) (bool, error) { want := r.URL.Query().Get("namespace") if want == "" { return false, nil } user, ok := loginCookie(r) if !ok { promptLogin(w) return true, nil } if user.Group == want { return false, nil } for i := range user.Groups { if want == user.Groups[i] { user.Group = want setLoginCookie(w, r, user) return false, nil } } return false, nil } func needsLogin(r *http.Request) (bool, error) { user, ok := loginCookie(r) if !ok { return true, nil } for i := range user.Groups { if user.Group == user.Groups[i] { return false, nil } } return true, nil } func setLoginCookie(w http.ResponseWriter, r *http.Request, user User) { cookie := &http.Cookie{ Name: "login", Value: encodeUserCookie(user), Expires: time.Now().Add(time.Hour * 24), Path: "/", } if was, ok := requestLoginCookie(r); !ok || !was.Is(user) { w.Header().Set("Set-Cookie", cookie.String()) } log.Printf("setting login cookie: %+v", user) *r = *r.WithContext(context.WithValue(r.Context(), "LOGIN_COOKIE", cookie.Value)) } func loginCookie(r *http.Request) (User, bool) { if v := r.Context().Value("LOGIN_COOKIE"); v != nil { log.Printf("login cookie from ctx") return decodeUserCookie(v.(string)) } return requestLoginCookie(r) } func requestLoginCookie(r *http.Request) (User, bool) { c, ok := getCookie("login", r) log.Printf("request login cookie: %v, %v", c, ok) if !ok { return User{}, false } return decodeUserCookie(c) } func getCookie(key string, r *http.Request) (string, bool) { cookie, err := r.Cookie(key) if err != nil { log.Printf("err getting cookie %s: %v: %+v", key, err, r.Cookies()) return "", false } return cookie.Value, cookie.Expires.IsZero() || time.Now().Before(cookie.Expires) } func decodeUserCookie(raw string) (User, bool) { decoded, ok := decodeCookie(raw) if !ok { return User{}, ok } var user User err := json.Unmarshal([]byte(decoded), &user) return user, err == nil } func encodeUserCookie(user User) string { b, err := json.Marshal(user) if err != nil { panic(err) } return encodeCookie(string(b)) } func encodeCookie(s string) string { cookie := Cookie{ Salt: uuid.New().String(), Value: s, } hash := crc32.NewIEEE() hash.Write([]byte(cookieSecret)) hash.Write([]byte(cookie.Salt)) hash.Write([]byte(cookie.Value)) cookie.Hash = base64.StdEncoding.EncodeToString(hash.Sum(nil)) b, err := json.Marshal(cookie) if err != nil { panic(err) } return base64.StdEncoding.EncodeToString(b) } func decodeCookie(s string) (string, bool) { b, err := base64.StdEncoding.DecodeString(s) if err != nil { return "", false } var cookie Cookie if err := json.Unmarshal(b, &cookie); err != nil { return "", false } hash := crc32.NewIEEE() hash.Write([]byte(cookieSecret)) hash.Write([]byte(cookie.Salt)) hash.Write([]byte(cookie.Value)) if got := base64.StdEncoding.EncodeToString(hash.Sum(nil)); cookie.Hash != got { return "", false } return cookie.Value, true }