added new basic auth endpoint
This commit is contained in:
parent
804842e7c5
commit
8c9ab3b8d2
10 changed files with 249 additions and 51 deletions
|
|
@ -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
119
pkg/server/auth.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue