added new basic auth endpoint

This commit is contained in:
ston1th 2022-08-21 14:59:31 +02:00
commit 8c9ab3b8d2
10 changed files with 249 additions and 51 deletions

View file

@ -7,11 +7,10 @@ import (
"time"
)
func validatePassword(pw string) (b []byte) {
func validatePassword(pw string) []byte {
hash := sha256.New()
hash.Write(b)
b = hash.Sum(nil)
return
hash.Write([]byte(pw))
return hash.Sum(nil)
}
const timeFmt = "2006-01-02 15:04:05"

119
pkg/server/auth.go Normal file
View file

@ -0,0 +1,119 @@
// Copyright (C) 2022 Marius Schellenberger
package server
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"io"
weakrand "math/rand"
"sync"
"time"
"git.giftfish.de/ston1th/goacc/pkg/db"
)
type AuthValidator interface {
Login(username, password string) (auth bool)
}
const cacheSize = 100
func init() {
weakrand.Seed(time.Now().UnixNano())
}
type DBLoginValidator struct {
db *db.DB
}
func NewDBLoginValidator(db *db.DB) *DBLoginValidator {
return &DBLoginValidator{db}
}
func (val *DBLoginValidator) Login(username, password string) bool {
_, err := val.db.Login(username, password)
return err == nil
}
type cache struct {
mu sync.RWMutex
m map[string]bool
}
func newCache() *cache {
return &cache{m: make(map[string]bool)}
}
func (c *cache) get(k string) (v, ok bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok = c.m[k]
return
}
func (c *cache) set(k string, v bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.makeRoom()
c.m[k] = v
}
func (c *cache) makeRoom() {
n := len(c.m)
if n < cacheSize {
return
}
del := n / 10
if del < 1 {
del = 1
}
for i := 0; i <= del; i++ {
rnd := weakrand.Intn(len(c.m))
j := 0
for k := range c.m {
if j == rnd {
delete(c.m, k)
break
}
j++
}
}
}
type BasicAuth struct {
mu sync.Mutex
auth AuthValidator
cache *cache
key []byte
}
func NewBasicAuth(auth AuthValidator) (*BasicAuth, error) {
key := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, key)
return &BasicAuth{
auth: auth,
cache: newCache(),
key: key,
}, err
}
func (a *BasicAuth) genkey(username, password string) string {
h := hmac.New(sha256.New, a.key)
h.Write(append([]byte(username), []byte(password)...))
return hex.EncodeToString(h.Sum(nil))
}
func (a *BasicAuth) Auth(username, password string) (auth bool) {
k := a.genkey(username, password)
auth, ok := a.cache.get(k)
if !ok {
a.mu.Lock()
auth = a.auth.Login(username, password)
a.mu.Unlock()
a.cache.set(k, auth)
}
return
}

View file

@ -154,28 +154,46 @@ func (c *Context) Error(i interface{}) {
func (c *Context) NotFound() {
c.Template("notFoundHandler")
c.Data = webData{Title: "404"}
c.Status = http.StatusNotFound
c.Response.WriteHeader(http.StatusNotFound)
c.setStatus(http.StatusNotFound)
c.Exec()
}
func (c *Context) Forbidden() {
c.Template("forbiddenHandler")
c.Data = webData{Title: "403"}
c.Status = http.StatusForbidden
c.Response.WriteHeader(http.StatusForbidden)
c.setStatus(http.StatusForbidden)
c.Exec()
}
func (c *Context) setStatus(status int) {
c.Status = status
c.Response.WriteHeader(status)
}
func (c *Context) plainResponse(msg string, status int) {
defer c.accessLog()
c.Status = status
http.Error(c.Response, msg, status)
}
func (c *Context) PlainUnauthorized() {
c.SetHeader("WWW-Authenticate", `Basic realm="auth/basic", charset="UTF-8"`)
c.plainResponse("unauthorized", http.StatusUnauthorized)
}
func (c *Context) PlainForbidden() {
c.SetHeader("WWW-Authenticate", `Basic realm="auth/basic", charset="UTF-8"`)
c.plainResponse("forbidden", http.StatusForbidden)
}
func (c *Context) PlainOK() {
c.plainResponse("ok", http.StatusOK)
}
func (c *Context) Template(name string) {
c.T = c.Srv.ts.Get(name)
}
func (c *Context) Write(buf []byte) (err error) {
_, err = c.Response.Write(buf)
return
}
func (c *Context) SetHeader(name, value string) {
c.Response.Header().Set(name, value)
}

View file

@ -576,3 +576,20 @@ func logoutHandler(ctx *Context) {
ctx.SetCookie(nil, 0)
ctx.Redirect(root, http.StatusFound)
}
func authBasicHandler(ctx *Context) {
if ctx.Method() != "GET" {
ctx.PlainUnauthorized()
return
}
username, password, ok := ctx.Request.BasicAuth()
if !ok {
ctx.PlainUnauthorized()
return
}
if ctx.Srv.BasicAuth.Auth(username, password) {
ctx.PlainOK()
return
}
ctx.PlainForbidden()
}

View file

@ -16,6 +16,7 @@ const (
consent = root + "consent"
users = root + "users"
logout = root + "logout"
auth = root + "auth/"
uroot = root + "user/"
userNew = uroot + "new"
@ -24,6 +25,8 @@ const (
userTotp = uroot + "totp/{user:[a-zA-Z0-9]+$}"
userSessions = uroot + "sessions/{user:[a-zA-Z0-9]+$}"
userUnlock = uroot + "unlock/{user:[a-zA-Z0-9]+$}"
authBasic = auth + "basic"
)
var routes = []route{
@ -115,4 +118,9 @@ var routes = []route{
userUnlockHandler)),
[]string{"POST"},
},
{
authBasic,
authBasicHandler,
[]string{"GET"},
},
}

View file

@ -28,9 +28,10 @@ type Server struct {
srv *http.Server
API *client.APIClient
DB *db.DB
JWT *jwt.JWT
API *client.APIClient
DB *db.DB
JWT *jwt.JWT
BasicAuth *BasicAuth
ts *TemplateStore
}
@ -72,6 +73,10 @@ func (s *Server) Start(dblog logr.Logger) (err error) {
if err != nil {
return errors.New("server: " + err.Error())
}
s.BasicAuth, err = NewBasicAuth(NewDBLoginValidator(s.DB))
if err != nil {
return errors.New("server: " + err.Error())
}
go func() {
time.Sleep(time.Second * 2)
s.srv.ListenAndServe()

View file

@ -1,7 +1,7 @@
{{define "body"}}
{{if .Data}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="col-lg-6 offset-3">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
@ -14,21 +14,27 @@
<div class="card-body">
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.Username}}" disabled>
<div class="row">
<div class="col-lg-5">
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.Username}}" disabled>
</div>
<div class="form-group">
<label class="col-form-label" for="email">Email</label>
<input class="form-control input-sm" type="text" id="email" name="email" value="{{.Data.Email}}" disabled>
</div>
</div>
<div class="form-group">
<label class="col-form-label" for="email">Email</label>
<input class="form-control input-sm" type="text" id="email" name="email" value="{{.Data.Email}}" disabled>
<div class="col-lg-5">
<div class="form-group">
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
</div>
<div class="form-group">
<label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat">
</div>
</div>
<div class="form-group">
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
</div>
<div class="form-group">
<label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat">
</div>
{{if .Admin}}
<div class="form-group">

View file

@ -1,6 +1,6 @@
{{define "body"}}
<div class="row">
<div class="col-xs-4 offset-4">
<div class="col-lg-6 offset-3">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
@ -8,21 +8,27 @@
<div class="card-body">
<form class="form-horizontal" action="/user/new" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
<div class="row">
<div class="col-lg-5">
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
</div>
<div class="form-group">
<label class="col-form-label" for="email">Email</label>
<input class="form-control input-sm" type="text" id="email" name="email" autocomplete="off" required>
</div>
</div>
<div class="form-group">
<label class="col-form-label" for="email">Email</label>
<input class="form-control input-sm" type="text" id="email" name="email" autocomplete="off" required>
<div class="col-lg-5">
<div class="form-group">
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
</div>
</div>
<div class="form-group">
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">