initial commit
This commit is contained in:
commit
54576d9d33
61 changed files with 5569 additions and 0 deletions
137
internal/server/passwordauth.go
Normal file
137
internal/server/passwordauth.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/danyel/go-loose/internal/password"
|
||||
)
|
||||
|
||||
type loginAttempt struct {
|
||||
failures int
|
||||
windowStart time.Time
|
||||
lockedUntil time.Time
|
||||
}
|
||||
|
||||
type loginGuard struct {
|
||||
mu sync.Mutex
|
||||
attempts map[string]loginAttempt
|
||||
dummyHash string
|
||||
}
|
||||
|
||||
func newLoginGuard() *loginGuard {
|
||||
return &loginGuard{attempts: make(map[string]loginAttempt)}
|
||||
}
|
||||
|
||||
func (g *loginGuard) allowed(key string, now time.Time) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
attempt := g.attempts[key]
|
||||
return !now.Before(attempt.lockedUntil)
|
||||
}
|
||||
|
||||
func (g *loginGuard) fail(key string, now time.Time) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
attempt := g.attempts[key]
|
||||
if attempt.windowStart.IsZero() || now.Sub(attempt.windowStart) > 10*time.Minute {
|
||||
attempt = loginAttempt{windowStart: now}
|
||||
}
|
||||
attempt.failures++
|
||||
if attempt.failures >= 5 {
|
||||
attempt.lockedUntil = now.Add(15 * time.Minute)
|
||||
}
|
||||
g.attempts[key] = attempt
|
||||
if len(g.attempts) > 10_000 {
|
||||
for itemKey, item := range g.attempts {
|
||||
if now.Sub(item.windowStart) > 30*time.Minute && now.After(item.lockedUntil) {
|
||||
delete(g.attempts, itemKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *loginGuard) success(key string) {
|
||||
g.mu.Lock()
|
||||
delete(g.attempts, key)
|
||||
g.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Server) passwordLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.installed.Load() {
|
||||
writeError(w, http.StatusServiceUnavailable, "Go Loose is not installed")
|
||||
return
|
||||
}
|
||||
if !s.cfg.LocalLogin {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 16<<10)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid login request")
|
||||
return
|
||||
}
|
||||
csrfCookie, cookieErr := r.Cookie("go_loose_login_csrf")
|
||||
csrfValue := r.FormValue("csrf_token")
|
||||
csrfPayload, csrfValid := s.sessions.VerifySigned(csrfValue)
|
||||
if cookieErr != nil || subtle.ConstantTimeCompare([]byte(csrfCookie.Value), []byte(csrfValue)) != 1 ||
|
||||
!csrfValid || !strings.HasPrefix(csrfPayload, "login|") {
|
||||
writeError(w, http.StatusForbidden, "invalid login form; reload the login page")
|
||||
return
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(r.Form.Get("email")))
|
||||
passwordValue := r.Form.Get("password")
|
||||
target := "/"
|
||||
if requested := r.Form.Get("return"); requested != "" {
|
||||
validated, err := s.validateLoginReturn(r.Context(), requested)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid client login return URL")
|
||||
return
|
||||
}
|
||||
target = validated
|
||||
}
|
||||
remoteAddress := remoteIP(r.RemoteAddr)
|
||||
guardKey := remoteAddress + "|" + email
|
||||
now := time.Now()
|
||||
if !s.loginGuard.allowed(guardKey, now) {
|
||||
s.recordPasswordLogin(r, nil, email, remoteAddress, false, "rate_limited")
|
||||
writeError(w, http.StatusTooManyRequests, "too many login attempts; try again later")
|
||||
return
|
||||
}
|
||||
user, encoded, err := s.store.PasswordUser(r.Context(), email)
|
||||
valid := err == nil && password.Verify(encoded, passwordValue)
|
||||
if err != nil {
|
||||
password.Verify(s.loginGuard.dummyHash, passwordValue)
|
||||
}
|
||||
if !valid {
|
||||
s.loginGuard.fail(guardKey, now)
|
||||
var userID *string
|
||||
if user.ID != "" {
|
||||
userID = &user.ID
|
||||
}
|
||||
s.recordPasswordLogin(r, userID, email, remoteAddress, false, "invalid_credentials")
|
||||
writeError(w, http.StatusUnauthorized, "invalid email or password")
|
||||
return
|
||||
}
|
||||
s.loginGuard.success(guardKey)
|
||||
s.recordPasswordLogin(r, &user.ID, user.Email, remoteAddress, true, "authenticated")
|
||||
s.finishLogin(w, r, "local:"+user.Email, user.Email, user.DisplayName, target, false)
|
||||
}
|
||||
|
||||
func (s *Server) recordPasswordLogin(r *http.Request, userID *string, email, remoteAddress string, successful bool, reason string) {
|
||||
if err := s.store.RecordLogin(r.Context(), userID, email, remoteAddress, successful, reason); err != nil {
|
||||
s.logger.Error("record password login", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func remoteIP(address string) string {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err == nil {
|
||||
return host
|
||||
}
|
||||
return address
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue