All: Enhance session management and security features
Using proper auth + security on login.jocadbz
parent
876ac33d1b
commit
ef06bf160a
|
|
@ -12,7 +12,7 @@ This, of course, assumes you have a decent understanding of Go.
|
|||
### Configuration Files
|
||||
|
||||
* **config/config.json.sample**:
|
||||
This file provides a template for the main application configuration. It defines critical parameters for the application to run, such as database credentials, domain, and file storage locations.
|
||||
This file provides a template for the main application configuration. It defines critical parameters for the application to run, such as database credentials, domain, file storage locations, and session security.
|
||||
Example content:
|
||||
{
|
||||
"domain_name": "localhost",
|
||||
|
|
@ -21,8 +21,13 @@ This, of course, assumes you have a decent understanding of Go.
|
|||
"db_password": "threadr_password",
|
||||
"db_database": "threadr_db",
|
||||
"db_svr_host": "localhost:3306",
|
||||
"file_storage_dir": "files"
|
||||
"file_storage_dir": "files",
|
||||
"session_secret": "change-me-to-32-byte-random",
|
||||
"session_secure": false
|
||||
}
|
||||
Notes:
|
||||
- `session_secret` should be a 32+ byte random value. At runtime, it is overridden by the `THREADR_SESSION_SECRET` environment variable if present (recommended for production).
|
||||
- `session_secure` controls whether cookies are marked `Secure`; set to `true` in HTTPS environments.
|
||||
|
||||
* **config/config.json**:
|
||||
The active configuration file, copied from `config.json.sample` and modified for the specific deployment. Contains sensitive information like database passwords.
|
||||
|
|
@ -55,19 +60,9 @@ This directory contains the HTTP handler functions that process incoming request
|
|||
* **handlers/app.go**:
|
||||
Defines common application-wide structures and middleware:
|
||||
- `PageData`: A struct holding data passed to HTML templates for rendering common elements (title, navbar state, login status, cookie banner, base paths, current URL).
|
||||
- `Config`: A struct to unmarshal application configuration from `config.json`.
|
||||
Example JSON for `Config`:
|
||||
{
|
||||
"domain_name": "localhost",
|
||||
"threadr_dir": "/threadr",
|
||||
"db_username": "threadr_user",
|
||||
"db_password": "threadr_password",
|
||||
"db_database": "threadr_db",
|
||||
"db_svr_host": "localhost:3306",
|
||||
"file_storage_dir": "files"
|
||||
}
|
||||
- `Config`: A struct to unmarshal application configuration from `config.json` (and env overrides). Fields include DB settings, domain, file storage dir, `session_secret`, and `session_secure`.
|
||||
- `App`: The main application context struct, holding pointers to the database connection, session store, configuration, and templates.
|
||||
- `SessionMW`: Middleware to retrieve or create a new Gorilla session for each request, making the session available in the request context.
|
||||
- `SessionMW`: Middleware to retrieve or create a new Gorilla session for each request, applying secure cookie options (HttpOnly, SameSite=Lax, Secure configurable) and attaching the session to the request context.
|
||||
- `RequireLoginMW`: Middleware to enforce user authentication for specific routes, redirecting unauthenticated users to the login page.
|
||||
|
||||
* **handlers/about.go**:
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@
|
|||
"db_password": "threadr_password",
|
||||
"db_database": "threadr_db",
|
||||
"db_svr_host": "localhost:3306",
|
||||
"file_storage_dir": "files"
|
||||
"file_storage_dir": "files",
|
||||
"session_secret": "change-me-to-32-byte-random",
|
||||
"session_secure": false
|
||||
}
|
||||
|
|
|
|||
5
go.mod
5
go.mod
|
|
@ -6,11 +6,12 @@ require (
|
|||
github.com/go-sql-driver/mysql v1.9.0
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/term v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -10,7 +10,9 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq
|
|||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ type Config struct {
|
|||
DBDatabase string `json:"db_database"`
|
||||
DBServerHost string `json:"db_svr_host"`
|
||||
FileStorageDir string `json:"file_storage_dir"`
|
||||
SessionSecret string `json:"session_secret"`
|
||||
SessionSecure bool `json:"session_secure"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
|
|
@ -42,13 +44,11 @@ func (app *App) SessionMW(next http.HandlerFunc) http.HandlerFunc {
|
|||
session, err := app.Store.Get(r, "session-name")
|
||||
if err != nil {
|
||||
session = sessions.NewSession(app.Store, "session-name")
|
||||
session.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 30, // 30 days
|
||||
HttpOnly: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce secure cookie options on every request.
|
||||
session.Options = app.cookieOptions(r)
|
||||
|
||||
ctx := context.WithValue(r.Context(), "session", session)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
|
|
@ -76,3 +76,21 @@ func (app *App) RequireLoginMW(next http.HandlerFunc) http.HandlerFunc {
|
|||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) cookieOptions(r *http.Request) *sessions.Options {
|
||||
secure := app.Config.SessionSecure
|
||||
if r.TLS != nil {
|
||||
secure = true
|
||||
} // I dunno what I am doing honestly
|
||||
options := &sessions.Options{
|
||||
Path: app.Config.ThreadrDir + "/",
|
||||
MaxAge: 86400 * 30,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
if app.Config.DomainName != "" {
|
||||
options.Domain = app.Config.DomainName
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"threadr/models"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
|
|
@ -24,14 +25,14 @@ func LoginHandler(app *App) http.HandlerFunc {
|
|||
http.Redirect(w, r, app.Config.ThreadrDir+"/login/?error=invalid", http.StatusFound)
|
||||
return
|
||||
}
|
||||
// Regenerate session to avoid fixation
|
||||
session.Options.MaxAge = -1
|
||||
_ = session.Save(r, w)
|
||||
session = sessions.NewSession(app.Store, "session-name")
|
||||
session.Options = app.cookieOptions(r)
|
||||
session.Values["user_id"] = user.ID
|
||||
session.Values["user_ip"] = r.RemoteAddr
|
||||
session.Values["user_agent"] = r.UserAgent()
|
||||
session.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 30, // 30 days
|
||||
HttpOnly: true,
|
||||
}
|
||||
if err := session.Save(r, w); err != nil {
|
||||
log.Printf("Error saving session: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
|
|
|
|||
20
main.go
20
main.go
|
|
@ -294,6 +294,14 @@ func main() {
|
|||
log.Fatal("Error loading config:", err)
|
||||
}
|
||||
|
||||
// Allow environment variable override for the session secret to avoid hardcoding secrets in files.
|
||||
if envSecret := os.Getenv("THREADR_SESSION_SECRET"); envSecret != "" {
|
||||
config.SessionSecret = envSecret
|
||||
}
|
||||
if len(config.SessionSecret) < 32 {
|
||||
log.Fatal("Session secret must be at least 32 bytes; set THREADR_SESSION_SECRET or session_secret in config")
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s", config.DBUsername, config.DBPassword, config.DBServerHost, config.DBDatabase)
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
|
|
@ -359,7 +367,17 @@ func main() {
|
|||
log.Fatal("Error parsing page templates:", err)
|
||||
}
|
||||
|
||||
store := sessions.NewCookieStore([]byte("secret-key")) // Replace with secure key in production
|
||||
store := sessions.NewCookieStore([]byte(config.SessionSecret))
|
||||
store.Options = &sessions.Options{
|
||||
Path: config.ThreadrDir + "/",
|
||||
MaxAge: 86400 * 30,
|
||||
HttpOnly: true,
|
||||
Secure: config.SessionSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
if config.DomainName != "" {
|
||||
store.Options.Domain = config.DomainName
|
||||
}
|
||||
|
||||
app := &handlers.App{
|
||||
DB: db,
|
||||
|
|
|
|||
Loading…
Reference in New Issue