Security
Authentication
Argon2id passwords, cookie sessions, and pluggable subject loaders.
HAMR's pkg/auth package gives you everything you need for session-based authentication: Argon2id password hashing, secure cookie sessions, and a token generator for invites and password resets. Auth middleware lives in pkg/middleware.
Password hashing
HAMR uses Argon2id, the winner of the Password Hashing Competition. The defaults are tuned for modern hardware and pass OWASP guidance.
auth service
import "github.com/FyrmForge/hamr/pkg/auth"
func (s *AuthService) Register(ctx context.Context, email, password, name string) (*repo.User, error) {
hash, err := auth.HashPassword(password)
if err != nil {
return nil, err
}
user := &repo.User{
Email: email,
Password: hash,
Name: name,
}
if err := s.store.CreateUser(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *AuthService) Authenticate(ctx context.Context, email, password string) (*repo.User, error) {
user, err := s.store.GetUserByEmail(ctx, email)
if err != nil {
return nil, ErrInvalidCredentials
}
if !auth.VerifyPassword(password, user.Password) {
return nil, ErrInvalidCredentials
}
return user, nil
}Sessions
Sessions are stored server-side (in your SessionStore) and identified by an opaque cookie. The SessionManager handles cookie reading, writing, expiration, and rotation.
cmd/site/main.go
import "github.com/FyrmForge/hamr/pkg/auth"
sessionManager := auth.NewSessionManager(store,
auth.WithCookieSecure(!devMode),
auth.WithCookieDomain(baseDomain),
auth.WithSessionTTL(24*time.Hour),
)Login handler
internal/web/handler/auth/handler.go
func (h *handler) Login(c echo.Context) error {
email := c.FormValue("email")
password := c.FormValue("password")
user, err := h.auth.Authenticate(c.Request().Context(), email, password)
if err != nil {
return respond.Error(c, echo.NewHTTPError(http.StatusUnauthorized,
"invalid email or password"))
}
if err := h.sessions.Create(c, user.ID); err != nil {
return respond.Error(c, err)
}
return respond.Redirect(c, "/dashboard")
}Auth middleware
HAMR's auth middleware uses a SubjectLoader callback so the framework never imports your User type:
internal/web/server.go
authCfg := hamrmw.AuthConfig{
SessionManager: deps.SessionManager,
SubjectLoader: func(ctx context.Context, id string) (any, error) {
return deps.Store.GetUserByID(ctx, id)
},
LoginRedirect: "/login",
HomeRedirect: "/",
}
site.Use(hamrmw.OptionalAuth(authCfg)) // load user if logged in
// ...
site.GET("/dashboard", dash.Index, hamrmw.RequireAuth(authCfg))
site.GET("/login", auth.LoginPage, hamrmw.RequireNotAuth(authCfg))| Middleware | Behavior on miss |
|---|---|
| Auth | Returns 401 — for API routes |
| RequireAuth | Redirects to LoginRedirect |
| RequireNotAuth | Redirects logged-in users to HomeRedirect |
| OptionalAuth | Loads user if present, never blocks |
| RequireRoles | Checks role via callback, returns 403 |
Logout
logout handler
func (h *handler) Logout(c echo.Context) error {
if err := h.sessions.Destroy(c); err != nil {
return respond.Error(c, err)
}
return respond.Redirect(c, "/login")
}Always wire CSRF
Authentication is paired with CSRF protection by default. The
CSRF middleware adds a token to your forms, and the layout's HTMX configuration injects it on every HTMX request. Never disable this.