Added HWSAuth documentation
415
HWSAuth.md
Normal file
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)
|
||||
5
Home.md
5
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).
|
||||
For issues, feature requests, or contributions, please visit the [golib repository](https://git.haelnorr.com/h/golib).
|
||||
|
||||
Reference in New Issue
Block a user