Added HWSAuth documentation

2026-01-11 22:44:45 +11:00
parent 3e2df6c850
commit cbfb157b72
2 changed files with 419 additions and 1 deletions

415
HWSAuth.md Normal file

@@ -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)

@@ -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