added cookie login
This commit is contained in:
+14
-3
@@ -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)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
site:
|
||||
title: "Amsterdam Web Communities System"
|
||||
topRefresh: 300
|
||||
loginCookieName: AmsterdamAuth
|
||||
loginCookieAge: 365
|
||||
userAgreement:
|
||||
title: "Amsterdam User Agreement"
|
||||
text: >
|
||||
|
||||
+84
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user