added cookie login

This commit is contained in:
2025-10-04 12:49:04 -06:00
parent f728eb21b0
commit 3ef8d6b9a6
7 changed files with 172 additions and 15 deletions
+14 -3
View File
@@ -47,9 +47,11 @@ func (*AmCLI) Version() string {
// AmConfig holds the configuration of the application as read from YAML.
type AmConfig struct {
Site struct {
Title string `yaml:"title"`
TopRefresh int `yaml:"topRefresh"`
UserAgreement struct {
Title string `yaml:"title"`
TopRefresh int `yaml:"topRefresh"`
LoginCookieName string `yaml:"loginCookieName"`
LoginCookieAge int `yaml:"loginCookieAge"`
UserAgreement struct {
Title string `yaml:"title"`
Text string `yaml:"text"`
} `yaml:"userAgreement"`
@@ -106,6 +108,13 @@ func overlayString(loaded string, defaulted string) string {
return loaded
}
/* overlayInt is a helper that takes a loaded or defaulted integer and returns it.
* Parameters:
* loaded - The integer loaded from a configuration file.
* defaulted - The default value of this integer.
* Returns:
* loaded if it's not empty, otherwise defaulted.
*/
func overlayInt(loaded int, defaulted int) int {
if loaded != 0 {
return loaded
@@ -122,6 +131,8 @@ func overlayInt(loaded int, defaulted int) int {
func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) {
dest.Site.Title = overlayString(loaded.Site.Title, defaults.Site.Title)
dest.Site.TopRefresh = overlayInt(loaded.Site.TopRefresh, defaults.Site.TopRefresh)
dest.Site.LoginCookieName = overlayString(loaded.Site.LoginCookieName, defaults.Site.LoginCookieName)
dest.Site.LoginCookieAge = overlayInt(loaded.Site.LoginCookieAge, defaults.Site.LoginCookieAge)
dest.Site.UserAgreement.Title = overlayString(loaded.Site.UserAgreement.Title, defaults.Site.UserAgreement.Title)
dest.Site.UserAgreement.Text = overlayString(loaded.Site.UserAgreement.Text, defaults.Site.UserAgreement.Text)
dest.Database.Driver = overlayString(loaded.Database.Driver, defaults.Database.Driver)
+2
View File
@@ -9,6 +9,8 @@
site:
title: "Amsterdam Web Communities System"
topRefresh: 300
loginCookieName: AmsterdamAuth
loginCookieAge: 365
userAgreement:
title: "Amsterdam User Agreement"
text: >
+84 -6
View File
@@ -15,6 +15,8 @@ import (
"errors"
"fmt"
"hash/crc32"
"strconv"
"strings"
"sync"
"time"
@@ -76,6 +78,9 @@ func (u *User) ContactInfo() (*ContactInfo, error) {
* Standard Go error status.
*/
func (u *User) NewAuthToken() (string, error) {
if u.IsAnon {
return "", errors.New("cannot generate token for anonymous user")
}
u.Mutex.Lock()
defer u.Mutex.Unlock()
newToken := util.GenerateRandomAuthString()
@@ -217,7 +222,7 @@ func touchUser(user *User) {
* The User pointer if authenticated, or nil if not.
* Standard Go error status.
*/
func AmAuthenticateUser(name string, password string, remote_ip string) (*User, error) {
func AmAuthenticateUser(name string, password string, remoteIP string) (*User, error) {
log.Debugf("AmAuthenicate() authenticating user %s...", name)
var ar *AuditRecord = nil
defer func() {
@@ -229,27 +234,100 @@ func AmAuthenticateUser(name string, password string, remote_ip string) (*User,
user, err := AmGetUserByName(name)
if err != nil {
log.Error("...user not found")
ar = AmNewAudit(AuditLoginFail, 0, remote_ip, fmt.Sprintf("Bad username: %s", name))
ar = AmNewAudit(AuditLoginFail, 0, remoteIP, fmt.Sprintf("Bad username: %s", name))
return nil, errors.New("the user account you have specified does not exist; please try again")
}
if user.IsAnon {
log.Error("...user is the Anonymous Honyak, can't explicitly log in")
ar = AmNewAudit(AuditLoginFail, user.Uid, remote_ip, "Anonymous user")
ar = AmNewAudit(AuditLoginFail, user.Uid, remoteIP, "Anonymous user")
return nil, errors.New("this account cannot be explicitly logged into; please try again")
}
if user.Lockout {
log.Error("...user is locked out by the admin")
ar = AmNewAudit(AuditLoginFail, user.Uid, remote_ip, "Account locked out")
ar = AmNewAudit(AuditLoginFail, user.Uid, remoteIP, "Account locked out")
return nil, errors.New("this account has been administratively locked; please contact the system administrator for assistance")
}
h := hashPassword(password)
if h != user.Passhash {
log.Warn("...invalid password")
ar = AmNewAudit(AuditLoginFail, user.Uid, remote_ip, "Bad password")
ar = AmNewAudit(AuditLoginFail, user.Uid, remoteIP, "Bad password")
return nil, errors.New("the password you have specified is incorrect; please try again")
}
log.Debug("...authenticated")
touchUser(user)
ar = AmNewAudit(AuditLoginOK, user.Uid, remote_ip)
ar = AmNewAudit(AuditLoginOK, user.Uid, remoteIP)
return user, nil
}
// crackAuthString validates an auth string and returns its UID and auth token.
func crackAuthString(authString string) (int32, string, error) {
log.Debug("Decoding authString " + authString)
if !strings.HasPrefix(authString, "AQAT:") {
return 0, "", errors.New("prefix not valid")
}
parms := strings.Split(authString[5:], "|")
n1, err := strconv.ParseInt(parms[0], 10, 32)
if err != nil {
return 0, "", fmt.Errorf("invalid UID field: %v", err)
}
uid := int32(n1)
n2, err2 := strconv.ParseUint(parms[2], 10, 32)
if err2 != nil {
return 0, "", fmt.Errorf("invalid checkvalue field: %v", err2)
}
cv1 := uint32(n2)
cv2 := uint32(uid) ^ crc32.ChecksumIEEE([]byte(parms[1]))
if cv1 != cv2 {
return 0, "", errors.New("checkvalues do not match")
}
return uid, parms[1], nil
}
/* AmAuthenticateUserByToken authenticates a user via the stored cookie authentication string.
* Parameters:
* authString - The stored cookie authentication string.
* remoteIP - The remote IP address wheter trhe user is logging in from.
* Returns:
* Pointer to the authenticated User, or nil.
* Standard Go error status.
*/
func AmAuthenticateUserByToken(authString string, remoteIP string) (*User, error) {
var ar *AuditRecord = nil
defer func() {
if ar != nil {
go ar.Store()
}
}()
uid, token, err := crackAuthString(authString)
if err != nil {
return nil, fmt.Errorf("authString not valid, ignored: %v", err)
}
var user *User
user, err = AmGetUser(uid)
if err != nil {
log.Error("...user not found")
ar = AmNewAudit(AuditLoginFail, 0, remoteIP, fmt.Sprintf("Bad uid: %d", uid))
return nil, fmt.Errorf("uid %d not found, ignore: %v", uid, err)
}
log.Debugf("AmAuthenicateUserByToken() authenticating user %d...", uid)
if user.IsAnon {
log.Error("...user is the Anonymous Honyak, can't explicitly log in")
ar = AmNewAudit(AuditLoginFail, user.Uid, remoteIP, "Anonymous user")
return nil, errors.New("this account cannot be explicitly logged into; please try again")
}
if user.Lockout {
log.Error("...user is locked out by the admin")
ar = AmNewAudit(AuditLoginFail, user.Uid, remoteIP, "Account locked out")
return nil, errors.New("this account has been administratively locked; please contact the system administrator for assistance")
}
if user.Tokenauth == nil || *(user.Tokenauth) != token {
log.Error("...token mismatch")
ar = AmNewAudit(AuditLoginFail, user.Uid, remoteIP, "Token mismatch")
return nil, errors.New("token mismatch")
}
log.Debug("...authenticated")
touchUser(user)
ar = AmNewAudit(AuditLoginOK, user.Uid, remoteIP)
return user, nil
}
+10 -2
View File
@@ -16,6 +16,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/gommon/log"
)
/* LoginForm renders the Amsterdam login form.
@@ -108,7 +109,14 @@ func Login(ctxt ui.AmContext) (string, any, error) {
}
ctxt.ReplaceUser(user)
if dlg.Field("saveme").IsChecked() {
// TODO: cookie set
// create and save an authentication token
authString, cerr := user.NewAuthToken()
if cerr == nil {
ctxt.SetLoginCookie(authString)
} else {
log.Errorf("unable to generate auth string for uid %d: %v", user.Uid, cerr)
}
}
// TODO: bounce to E-mail verify if we can do so
return "redirect", target, nil
@@ -135,7 +143,7 @@ func Logout(ctxt ui.AmContext) (string, any, error) {
}
if !ctxt.CurrentUser().IsAnon {
// TODO: erase login cookie
ctxt.ClearLoginCookie()
ctxt.ClearSession()
}
return "redirect", target, nil
+29 -2
View File
@@ -14,7 +14,9 @@ import (
"bytes"
"net/http"
"strconv"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/CloudyKit/jet/v6"
"github.com/gorilla/sessions"
@@ -25,6 +27,7 @@ import (
// AmContext is the interface for Amsterdam's wrapper context that exposes the required functionality.
type AmContext interface {
ClearLoginCookie()
ClearSession()
CurrentUser() *database.User
CurrentUserId() int32
@@ -39,6 +42,7 @@ type AmContext interface {
ReplaceUser(*database.User)
SaveSession() error
SubRender(string) ([]byte, error)
SetLoginCookie(string)
SetOutputType(string)
SetRC(int)
GetScratch(string) any
@@ -57,12 +61,22 @@ type amContext struct {
session *sessions.Session
}
// ClearLoginCookie overwrites and removes the login cookie.
func (c *amContext) ClearLoginCookie() {
cookie := new(http.Cookie)
cookie.Name = config.GlobalConfig.Site.LoginCookieName
cookie.Value = ""
cookie.Path = "/"
cookie.Expires = time.Now()
c.echoContext.SetCookie(cookie)
}
// ClearSession clears the current session.
func (c *amContext) ClearSession() {
for k := range c.session.Values {
delete(c.session.Values, k)
}
SetupAmSession(c.session)
setupAmSession(c.session)
}
// CurrentUser returns the current user from the session.
@@ -191,6 +205,19 @@ func (c *amContext) SubRender(name string) ([]byte, error) {
return buf.Bytes(), err
}
/* SetLoginCookie adds the login cookie to the result output.
* Parameters:
* auth - The auth string to set.
*/
func (c *amContext) SetLoginCookie(auth string) {
cookie := new(http.Cookie)
cookie.Name = config.GlobalConfig.Site.LoginCookieName
cookie.Value = auth
cookie.Path = "/"
cookie.Expires = time.Now().AddDate(0, 0, config.GlobalConfig.Site.LoginCookieAge)
c.echoContext.SetCookie(cookie)
}
// SetOutputType sets the MIME output type for the current operation.
func (c *amContext) SetOutputType(typ string) {
c.outputType = typ
@@ -253,7 +280,7 @@ func NewAmContext(ctxt echo.Context) (AmContext, error) {
rc.session = sess
sess.Options = defoptions
if sess.IsNew {
SetupAmSession(sess)
setupAmSession(sess)
} else {
log.Debugf("took the not-new-session path")
}
+31
View File
@@ -14,8 +14,10 @@ import (
"fmt"
"net/http"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
)
func sendPageData(ctxt echo.Context, amctxt AmContext, command string, data any) error {
@@ -63,11 +65,14 @@ func ErrorPage(ctxt AmContext, input_err error) (string, any, error) {
*/
func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
return func(ctxt echo.Context) error {
// Create the AmContext.
amctxt, aerr := NewAmContext(ctxt)
if aerr != nil {
ctxt.Logger().Errorf("Session creation error: %v", aerr)
return aerr
}
// Check IP banning.
banmsg, banerr := database.AmTestIPBan(ctxt.RealIP())
if banerr != nil {
ctxt.Logger().Warnf("address %s could not be tested: %v", ctxt.RealIP(), banerr)
@@ -75,8 +80,33 @@ func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
} else if banmsg != "" {
amctxt.VarMap().Set("amsterdam_pageTitle", "IP Address Banned")
amctxt.VarMap().Set("message", banmsg)
amctxt.SetRC(http.StatusForbidden)
return sendPageData(ctxt, amctxt, "framed_template", "ipban.jet")
}
// Check for cookie login.
if amctxt.CurrentUser().IsAnon {
cookie, cerr := ctxt.Cookie(config.GlobalConfig.Site.LoginCookieName)
if cerr == nil {
var user *database.User
user, cerr = database.AmAuthenticateUserByToken(cookie.Value, ctxt.RealIP())
if cerr == nil {
// log the user in and rotate login cookie
amctxt.ReplaceUser(user)
var newToken string
if newToken, cerr = user.NewAuthToken(); cerr == nil {
amctxt.SetLoginCookie(newToken)
} else {
log.Warnf("unable to rotate login cookie: %v", cerr)
}
} else {
log.Errorf("login cookie bogus, do not use: %v", cerr)
amctxt.ClearLoginCookie()
}
}
}
// Exec the wrapped function.
what, rc, err := myfunc(amctxt)
if err == nil {
if err = amctxt.SaveSession(); err != nil {
@@ -90,6 +120,7 @@ func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
} else {
ctxt.Logger().Errorf("Page function error: %v", err)
_, rc, _ = ErrorPage(amctxt, err)
amctxt.SetRC(http.StatusInternalServerError)
newerr := sendPageData(ctxt, amctxt, "framed_template", rc)
err = newerr
}
+2 -2
View File
@@ -26,8 +26,8 @@ func SetupSessionManager() {
SessionStore = memstore.NewMemStore([]byte(config.GlobalConfig.Rendering.CookieKey))
}
// SetupAmSession sets up a newly created Amsterdam session.
func SetupAmSession(session *sessions.Session) {
// setupAmSession sets up a newly created Amsterdam session.
func setupAmSession(session *sessions.Session) {
u, err := database.AmGetAnonUser()
if err == nil {
session.Values["user_id"] = u.Uid