auth
The auth package provides session-based authentication with username/password credentials, bundled as a self-contained domain.
Core Components:
Domain: Bundles repository, service, HTTP handlers, and middleware together. Implementsapplication.Domaininterface for registration with anApplication.Service: Core authentication logic for user registration, login/logout, password changes, and user deletion.Repository: PostgreSQL storage for users with automatic schema migrations.User: User model with ID, username, hashed password, salt, timestamps, and status.AuthenticationMiddleware: HTTP middleware that validates session cookies and injects the authenticated user into request context.UserCleanupJob: Job struct for enqueueing post-deletion cleanup tasks.UserFromContext: Helper function to retrieve the authenticated user from request context.
Full package docs at pkg.go.dev
Step-by-step guide
Section titled “Step-by-step guide”-
Set up the database and session storage
db, err := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable")if err != nil {log.ErrorContext(ctx, "failed to connect to database", "error", err)return}sessionDomain := session.New(db.Connection())The auth package requires a database connection and session storage. The
sessionpackage provides a compatible session storage implementation. -
Create the auth domain
authDomain := auth.New(db.Connection(), // database connectionsessionDomain.Service, // session storage"session_id", // cookie name for sessionsnil, // username validator (nil uses default)nil, // password validator (nil uses default)nil, // cleanup job enqueuer (optional))Default validators require usernames to be 5-20 characters and passwords to be 8-100 characters. Pass custom validator functions to override.
-
Register domains with the application
app := application.New()app.RegisterDatabase("main", db)app.RegisterDomain("session", "main", sessionDomain)app.RegisterDomain("auth", "main", authDomain)Registering domains with a database name automatically registers their repositories for migrations.
-
Create an HTTP server and mount the auth endpoints
api := httpserver.New("8080", 3*time.Second)api.HandleGroup("/auth", authDomain.HandleGroup)This exposes the following endpoints:
POST /auth/register- Create a new userPOST /auth/login- Login and receive session cookiePOST /auth/logout- Clear sessionGET /auth/me- Get current user infoPOST /auth/change-password- Change password (requires auth)DELETE /auth/me- Delete user account (requires auth)
-
Protect routes with authentication middleware
protectedGroup := httpserver.NewHandlerGroup()protectedGroup.Use(authDomain.Middleware)protectedGroup.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {user := auth.UserFromContext(r.Context())if user != nil {w.Write([]byte("Hello, " + user.Username))}})api.HandleGroup("/api", protectedGroup)The
AuthenticationMiddlewarereturns 401 Unauthorized if no valid session is found. Useauth.UserFromContext()to access the authenticated user. -
Register the server and run the application
app.RegisterService("api", api)if err := app.Run(ctx); err != nil {log.ErrorContext(ctx, "app finished with error", "error", err)}Expected output:
time=2025-01-01T12:00:00.000+00:00 level=INFO msg="running migrations" database=maintime=2025-01-01T12:00:00.010+00:00 level=INFO msg="starting service" serviceName=apitime=2025-01-01T12:00:00.010+00:00 level=INFO msg="starting http server" address=:8080
Using with Application
Section titled “Using with Application”The auth.Domain implements the application.Domain interface, so it integrates seamlessly with an Application:
app := application.New()
// Set up databasedb, _ := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable")app.RegisterDatabase("main", db)
// Set up session and auth domainssessionDomain := session.New(db.Connection())app.RegisterDomain("session", "main", sessionDomain)
authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, nil)app.RegisterDomain("auth", "main", authDomain)
// Set up HTTP server with auth endpointsapi := httpserver.New("8080", 3*time.Second)api.HandleGroup("/auth", authDomain.HandleGroup)app.RegisterService("api", api)
app.Run(ctx)HTTP endpoints
Section titled “HTTP endpoints”| Endpoint | Method | Auth Required | Description |
|---|---|---|---|
/register | POST | No | Create new user with {"login": "...", "password": "..."} |
/login | POST | No | Login with {"login": "...", "password": "..."}, sets session cookie |
/logout | POST | No | Clears session cookie |
/me | GET | No | Returns {"username": "..."} if authenticated, 401 otherwise |
/change-password | POST | Yes | Change password with {"currentPassword": "...", "newPassword": "..."} |
/me | DELETE | Yes | Delete user account and all sessions |
Custom validators
Section titled “Custom validators”Override the default username and password validation:
usernameValidator := func(username string) error { if len(username) < 3 { return errors.New("username must be at least 3 characters") } if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(username) { return errors.New("username can only contain letters, numbers, and underscores") } return nil}
passwordValidator := func(password string) error { if len(password) < 12 { return errors.New("password must be at least 12 characters") } return nil}
authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", usernameValidator, passwordValidator, nil)User cleanup jobs
Section titled “User cleanup jobs”When a user is deleted, you can enqueue cleanup jobs to handle related data. The queue.Processor implements the required interface directly:
// Create a processor for cleanup jobscleanupHandler := queue.HandlerFunc[auth.UserCleanupJob](func(ctx context.Context, job auth.UserCleanupJob) { log.InfoContext(ctx, "cleaning up user data", "user_id", job.UserID, "deleted_at", job.DeletedAt) // Delete user's files, posts, comments, etc.})
cleanupQueue := queue.NewChanQueue[auth.UserCleanupJob](100, 5*time.Second)cleanupProcessor := queue.New(cleanupHandler, cleanupQueue, 2, 10*time.Second)
// Pass processor directly to auth.New()authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, cleanupProcessor)
// Register processor as a serviceapp.RegisterService("cleanup", cleanupProcessor)The UserCleanupJob contains UserID and DeletedAt fields for processing cleanup tasks asynchronously.
Error types
Section titled “Error types”The package exports these errors for handling specific cases:
ErrUserNotFound- User does not existErrWrongUserOrPassword- Invalid credentials during loginErrInvalidUsername/ErrShortUsername/ErrLongUsername- Username validation failedErrInvalidPassword/ErrShortPassword/ErrLongPassword- Password validation failedErrCurrentPasswordIncorrect- Current password wrong during password change
Complete example
Section titled “Complete example”package main
import ( "context" "net/http" "time"
"github.com/platforma-dev/platforma/application" "github.com/platforma-dev/platforma/auth" "github.com/platforma-dev/platforma/database" "github.com/platforma-dev/platforma/httpserver" "github.com/platforma-dev/platforma/log" "github.com/platforma-dev/platforma/session")
func main() { ctx := context.Background()
db, err := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable") if err != nil { log.ErrorContext(ctx, "failed to connect to database", "error", err) return }
sessionDomain := session.New(db.Connection()) authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, nil)
app := application.New() app.RegisterDatabase("main", db) app.RegisterDomain("session", "main", sessionDomain) app.RegisterDomain("auth", "main", authDomain)
api := httpserver.New("8080", 3*time.Second) api.Use(log.NewTraceIDMiddleware(nil, "")) api.Use(httpserver.NewRecoverMiddleware())
api.HandleGroup("/auth", authDomain.HandleGroup)
protected := httpserver.NewHandlerGroup() protected.Use(authDomain.Middleware) protected.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) w.Write([]byte("Welcome, " + user.Username)) }) api.HandleGroup("/api", protected)
api.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) })
app.RegisterService("api", api)
if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) }}