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))
MiddlewareBehavior on miss
AuthReturns 401 — for API routes
RequireAuthRedirects to LoginRedirect
RequireNotAuthRedirects logged-in users to HomeRedirect
OptionalAuthLoads user if present, never blocks
RequireRolesChecks 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.