diff --git a/HWSAuth.md b/HWSAuth.md new file mode 100644 index 0000000..5b04252 --- /dev/null +++ b/HWSAuth.md @@ -0,0 +1,415 @@ +# hwsauth + +JWT-based authentication middleware for the hws web framework. + +## Overview + +`hwsauth` provides a complete authentication solution for hws web applications using JSON Web Tokens (JWT). It handles access tokens, refresh tokens, automatic token rotation, and integrates seamlessly with any database or ORM. + +## Installation + +```bash +go get git.haelnorr.com/h/golib/hwsauth +``` + +## Key Concepts + +### Authentication Flow + +1. **User Login**: Credentials are validated and JWT tokens (access + refresh) are issued +2. **Request Authentication**: Middleware validates tokens on each request +3. **Automatic Refresh**: Expired access tokens are refreshed using valid refresh tokens +4. **Token Freshness**: Sensitive operations require recently issued tokens + +### Type Safety with Generics + +hwsauth uses Go generics for type safety: + +```go +Authenticator[T Model, TX DBTransaction] +``` + +- `T`: Your user model type (must implement `Model` interface) +- `TX`: Your transaction type (must implement `DBTransaction` interface) + +This eliminates type assertions and provides compile-time type checking. + +## Quick Start + +### 1. User Model + +```go +type User struct { + UserID int + Username string + Email string +} + +func (u User) ID() int { + return u.UserID +} +``` + +### 2. Configuration + +Environment variables: + +```env +HWSAUTH_SECRET_KEY=your-secret-key +HWSAUTH_SSL=true +HWSAUTH_TRUSTED_HOST=https://example.com +HWSAUTH_ACCESS_TOKEN_EXPIRY=5 +HWSAUTH_REFRESH_TOKEN_EXPIRY=1440 +HWSAUTH_LANDING_PAGE=/dashboard +``` + +Load config: + +```go +cfg, err := hwsauth.ConfigFromEnv() +``` + +### 3. Create Authenticator + +```go +beginTx := func(ctx context.Context) (hwsauth.DBTransaction, error) { + return db.BeginTx(ctx, nil) +} + +loadUser := func(ctx context.Context, tx *sql.Tx, id int) (User, error) { + var user User + err := tx.QueryRowContext(ctx, + "SELECT id, username, email FROM users WHERE id = $1", id). + Scan(&user.UserID, &user.Username, &user.Email) + return user, err +} + +auth, err := hwsauth.NewAuthenticator[User, *sql.Tx]( + cfg, + loadUser, + server, + beginTx, + logger, + errorPage, +) +``` + +### 4. Apply Middleware + +```go +server.AddMiddleware(auth.Authenticate()) +auth.IgnorePaths("/", "/login", "/register") +``` + +## Core Features + +### Middleware + +**Authenticate()** - Main authentication middleware: +```go +server.AddMiddleware(auth.Authenticate()) +``` + +**IgnorePaths()** - Exclude paths from authentication: +```go +auth.IgnorePaths("/public", "/health") +``` + +### Route Guards + +**LoginReq** - Require authentication: +```go +protectedHandler := auth.LoginReq(myHandler) +``` + +**LogoutReq** - Redirect authenticated users: +```go +loginPageHandler := auth.LogoutReq(showLoginForm) +``` + +**FreshReq** - Require fresh authentication: +```go +sensitiveHandler := auth.FreshReq(changePasswordHandler) +``` + +### Authentication Operations + +**Login** - Authenticate user and set cookies: +```go +err := auth.Login(w, r, user, rememberMe) +``` + +**Logout** - Clear authentication and revoke tokens: +```go +err := auth.Logout(tx, w, r) +``` + +**CurrentModel** - Get authenticated user: +```go +user := auth.CurrentModel(r.Context()) +``` + +**RefreshAuthTokens** - Manually refresh tokens: +```go +err := auth.RefreshAuthTokens(tx, w, r) +``` + +## ORM Integration + +### Standard Library (database/sql) + +```go +beginTx := func(ctx context.Context) (hwsauth.DBTransaction, error) { + return db.BeginTx(ctx, nil) +} + +loadUser := func(ctx context.Context, tx *sql.Tx, id int) (User, error) { + // Use tx to query database + return user, err +} + +auth := hwsauth.NewAuthenticator[User, *sql.Tx](...) +``` + +### GORM + +```go +beginTx := func(ctx context.Context) (hwsauth.DBTransaction, error) { + gormTx := gormDB.WithContext(ctx).Begin() + return gormTx.Statement.ConnPool.(*sql.Tx), nil +} + +loadUser := func(ctx context.Context, tx *gorm.DB, id int) (User, error) { + var user User + err := gormDB.WithContext(ctx).First(&user, id).Error + return user, err +} + +auth := hwsauth.NewAuthenticator[User, *gorm.DB](...) +``` + +### Bun + +```go +beginTx := func(ctx context.Context) (hwsauth.DBTransaction, error) { + return bunDB.BeginTx(ctx, nil) +} + +loadUser := func(ctx context.Context, tx bun.Tx, id int) (User, error) { + var user User + err := tx.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx) + return user, err +} + +auth := hwsauth.NewAuthenticator[User, bun.Tx](...) +``` + +## Configuration Reference + +### Config Struct + +```go +type Config struct { + SSL bool // Enable SSL cookies + TrustedHost string // Trusted host for SSL + SecretKey string // JWT signing key (required) + AccessTokenExpiry int64 // Access token expiry (minutes) + RefreshTokenExpiry int64 // Refresh token expiry (minutes) + TokenFreshTime int64 // Fresh token duration (minutes) + LandingPage string // Logged-in user landing page + DatabaseType string // Database type (postgres, mysql, etc.) + DatabaseVersion string // Database version + JWTTableName string // Custom JWT table name +} +``` + +### Environment Variables + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `HWSAUTH_SSL` | Enable SSL cookies | `false` | No | +| `HWSAUTH_TRUSTED_HOST` | Trusted host | - | If SSL=true | +| `HWSAUTH_SECRET_KEY` | JWT signing key | - | **Yes** | +| `HWSAUTH_ACCESS_TOKEN_EXPIRY` | Access expiry (min) | `5` | No | +| `HWSAUTH_REFRESH_TOKEN_EXPIRY` | Refresh expiry (min) | `1440` | No | +| `HWSAUTH_TOKEN_FRESH_TIME` | Fresh time (min) | `5` | No | +| `HWSAUTH_LANDING_PAGE` | Landing page | `/profile` | No | +| `HWSAUTH_DATABASE_TYPE` | DB type | - | No | +| `HWSAUTH_DATABASE_VERSION` | DB version | - | No | +| `HWSAUTH_JWT_TABLE_NAME` | JWT table name | - | No | + +## Interfaces + +### Model + +User models must implement: + +```go +type Model interface { + ID() int +} +``` + +### DBTransaction + +Transaction types must implement: + +```go +type DBTransaction interface { + Commit() error + Rollback() error +} +``` + +Standard `*sql.Tx` implements this automatically. + +### LoadFunc + +```go +type LoadFunc[T Model, TX DBTransaction] func( + ctx context.Context, + tx TX, + id int, +) (T, error) +``` + +Function to load users from database. + +### BeginTX + +```go +type BeginTX func(ctx context.Context) (DBTransaction, error) +``` + +Function to create database transactions. + +## Security Best Practices + +1. **Use SSL in production**: Set `HWSAUTH_SSL=true` +2. **Strong secret keys**: Generate with `openssl rand -base64 32` +3. **Appropriate expiry times**: Balance security and UX +4. **Fresh tokens for sensitive ops**: Use `FreshReq` middleware +5. **HTTP-only cookies**: Tokens stored securely by default +6. **Parameterized queries**: Prevent SQL injection +7. **Rate limiting**: Protect authentication endpoints +8. **HTTPS only**: Never send tokens over HTTP + +## Common Patterns + +### Protected Dashboard + +```go +func setupRoutes(server *hws.Server, auth *hwsauth.Authenticator[User, *sql.Tx]) { + // Public routes + server.AddRoute("GET", "/", homeHandler) + server.AddRoute("GET", "/login", auth.LogoutReq(loginPageHandler)) + server.AddRoute("POST", "/login", loginSubmitHandler) + + // Protected routes + server.AddRoute("GET", "/dashboard", + auth.LoginReq(dashboardHandler)) + server.AddRoute("GET", "/profile", + auth.LoginReq(profileHandler)) + + // Sensitive operations + server.AddRoute("POST", "/change-password", + auth.LoginReq(auth.FreshReq(changePasswordHandler))) +} +``` + +### Login Handler + +```go +func loginHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + password := r.FormValue("password") + + user, err := validateCredentials(username, password) + if err != nil { + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + rememberMe := r.FormValue("remember_me") == "on" + err = auth.Login(w, r, user, rememberMe) + if err != nil { + http.Error(w, "Login failed", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/dashboard", http.StatusSeeOther) +} +``` + +### Logout Handler + +```go +func logoutHandler(w http.ResponseWriter, r *http.Request) { + tx, _ := db.BeginTx(r.Context(), nil) + defer tx.Rollback() + + if err := auth.Logout(tx, w, r); err != nil { + http.Error(w, "Logout failed", http.StatusInternalServerError) + return + } + + tx.Commit() + http.Redirect(w, r, "/", http.StatusSeeOther) +} +``` + +### Access Current User + +```go +func dashboardHandler(w http.ResponseWriter, r *http.Request) { + user := auth.CurrentModel(r.Context()) + if user.ID() == 0 { + http.Error(w, "Not authenticated", http.StatusUnauthorized) + return + } + + data := DashboardData{ + Username: user.Username, + Email: user.Email, + } + + renderTemplate(w, "dashboard", data) +} +``` + +## Troubleshooting + +### Tokens not persisting + +- Check `HWSAUTH_SSL` matches your environment +- Verify `HWSAUTH_TRUSTED_HOST` is correct +- Ensure cookies are enabled in browser + +### User not authenticated + +- Check middleware is applied: `server.AddMiddleware(auth.Authenticate())` +- Verify path isn't in ignored paths +- Check token hasn't expired + +### Transaction type errors + +- Ensure your `TX` type parameter matches your `beginTx` return type +- Verify `LoadFunc` accepts the correct transaction type +- Check ORM transaction compatibility + +### "Secret key is required" + +- Set `HWSAUTH_SECRET_KEY` environment variable +- Or provide in `Config` struct + +## See Also + +- [hws](./hws.md) - Web server framework +- [jwt](./JWT.md) - JWT token library +- [env](./env.md) - Environment variable loading + +## Links + +- [Source Code](https://git.haelnorr.com/h/golib/hwsauth) +- [Issue Tracker](https://git.haelnorr.com/h/golib/hwsauth/issues) +- [Examples](https://git.haelnorr.com/h/golib/hwsauth/tree/master/examples) diff --git a/Home.md b/Home.md index 80cdb30..321ac6b 100644 --- a/Home.md +++ b/Home.md @@ -7,6 +7,9 @@ Welcome to the golib documentation wiki. This wiki provides comprehensive docume ### [JWT](JWT.md) JWT (JSON Web Token) generation and validation with database-backed token revocation support. Supports multiple database backends and ORMs. +### [HWSAuth](HWSAuth.md) +JWT-based authentication middleware for the hws web framework. Provides complete authentication solution with access tokens, refresh tokens, automatic token rotation, and seamless database/ORM integration. + ## Installation ```bash @@ -15,4 +18,4 @@ go get git.haelnorr.com/h/golib/jwt ## Contributing -For issues, feature requests, or contributions, please visit the [golib repository](https://git.haelnorr.com/h/golib). \ No newline at end of file +For issues, feature requests, or contributions, please visit the [golib repository](https://git.haelnorr.com/h/golib).