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](JWT.md)
|
||||||
JWT (JSON Web Token) generation and validation with database-backed token revocation support. Supports multiple database backends and ORMs.
|
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
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -15,4 +18,4 @@ go get git.haelnorr.com/h/golib/jwt
|
|||||||
|
|
||||||
## Contributing
|
## 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