no submod
This commit is contained in:
251
work/notea/server/authenticate.go
Normal file
251
work/notea/server/authenticate.go
Normal file
@@ -0,0 +1,251 @@
|
||||
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) {
|
||||
var cookie *http.Cookie
|
||||
cookies := r.Cookies()
|
||||
for i := range cookies {
|
||||
if cookies[i].Name == key && (cookies[i].Expires.IsZero() || time.Now().Before(cookies[i].Expires)) {
|
||||
cookie = cookies[i]
|
||||
}
|
||||
}
|
||||
if cookie == nil {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user