192 lines
4.0 KiB
Go
192 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"hash/crc32"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
var cookieSecret = os.Getenv("COOKIE_SECRET")
|
|
|
|
type User struct {
|
|
User string
|
|
Groups []string
|
|
}
|
|
|
|
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 {
|
|
return nil, false, err
|
|
} else if done {
|
|
return nil, true, nil
|
|
}
|
|
|
|
if ok, err := needsLogin(r); err != nil {
|
|
return nil, false, err
|
|
} else if ok {
|
|
promptLogin(w)
|
|
return nil, true, nil
|
|
}
|
|
|
|
user, _ := loginCookie(r)
|
|
namespace, _ := namespaceCookie(r)
|
|
return server.WithLoggedIn(user.User, namespace, 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")
|
|
}
|
|
setLoginCookie(w, r, User{
|
|
User: username,
|
|
Groups: groups,
|
|
})
|
|
setNamespaceCookie(w, r, groups[0])
|
|
return false, nil
|
|
}
|
|
|
|
func needsLogin(r *http.Request) (bool, error) {
|
|
user, ok := loginCookie(r)
|
|
if !ok {
|
|
return true, nil
|
|
}
|
|
group, ok := namespaceCookie(r)
|
|
if !ok {
|
|
return true, nil
|
|
}
|
|
for i := range user.Groups {
|
|
if 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),
|
|
}
|
|
w.Header().Add("Set-Cookie", cookie.String())
|
|
r.AddCookie(cookie)
|
|
}
|
|
|
|
func loginCookie(r *http.Request) (User, bool) {
|
|
c, ok := getCookie("login", r)
|
|
if !ok {
|
|
return User{}, false
|
|
}
|
|
return decodeUserCookie(c)
|
|
}
|
|
|
|
func setNamespaceCookie(w http.ResponseWriter, r *http.Request, s string) {
|
|
cookie := &http.Cookie{
|
|
Name: "namespace",
|
|
Value: s,
|
|
Expires: time.Now().Add(time.Hour * 24),
|
|
}
|
|
w.Header().Add("Set-Cookie", cookie.String())
|
|
r.AddCookie(cookie)
|
|
}
|
|
|
|
func namespaceCookie(r *http.Request) (string, bool) {
|
|
return getCookie("namespace", r)
|
|
}
|
|
|
|
func getCookie(key string, r *http.Request) (string, bool) {
|
|
cookies := r.Cookies()
|
|
for i := range cookies {
|
|
if cookies[i].Name == key && (cookies[i].Expires.IsZero() || time.Now().Before(cookies[i].Expires)) {
|
|
return cookies[i].Value, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
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
|
|
}
|