Files
godoxy/internal/auth/README.md

350 lines
9.4 KiB
Markdown

# Authentication
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.
## Overview
The auth package implements authentication middleware and login handlers that integrate with GoDoxy's HTTP routing system. It provides flexible authentication that can be enabled/disabled based on configuration and supports multiple authentication providers.
### Primary consumers
- `internal/route/rules` - Authentication middleware for routes
- `internal/api/v1/auth` - Login and session management endpoints
- `internal/homepage` - WebUI login page
### Non-goals
- ACL or authorization (see `internal/acl`)
- User management database
- Multi-factor authentication
- Rate limiting (basic OIDC rate limiting only)
### Stability
Stable internal package. Public API consists of the `Provider` interface and initialization functions.
## Public API
### Exported types
```go
type Provider interface {
CheckToken(r *http.Request) error
LoginHandler(w http.ResponseWriter, r *http.Request)
PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request)
LogoutHandler(w http.ResponseWriter, r *http.Request)
}
```
### OIDC Provider
```go
type OIDCProvider struct {
oauthConfig *oauth2.Config
oidcProvider *oidc.Provider
oidcVerifier *oidc.IDTokenVerifier
endSessionURL *url.URL
allowedUsers []string
allowedGroups []string
rateLimit *rate.Limiter
}
```
### Username/Password Provider
```go
type UserPassAuth struct {
username string
pwdHash []byte
secret []byte
tokenTTL time.Duration
}
```
### Exported functions
```go
func Initialize() error
```
Sets up authentication providers based on environment configuration. Returns error if OIDC issuer is configured but cannot be reached.
```go
func IsEnabled() bool
```
Returns whether authentication is enabled. Checks `DEBUG_DISABLE_AUTH`, `API_JWT_SECRET`, and `OIDC_ISSUER_URL`.
```go
func IsOIDCEnabled() bool
```
Returns whether OIDC authentication is configured.
```go
func GetDefaultAuth() Provider
```
Returns the configured authentication provider.
```go
func AuthCheckHandler(w http.ResponseWriter, r *http.Request)
```
HTTP handler that checks if the request has a valid token. Returns 200 if valid, invokes login handler otherwise.
```go
func AuthOrProceed(w http.ResponseWriter, r *http.Request) bool
```
Authenticates request or proceeds if valid. Returns `false` if login handler was invoked, `true` if authenticated.
```go
func ProceedNext(w http.ResponseWriter, r *http.Request)
```
Continues to the next handler after successful authentication.
```go
func NewUserPassAuth(username, password string, secret []byte, tokenTTL time.Duration) (*UserPassAuth, error)
```
Creates a new username/password auth provider with bcrypt password hashing.
```go
func NewUserPassAuthFromEnv() (*UserPassAuth, error)
```
Creates username/password auth from environment variables `API_USER`, `API_PASSWORD`, `API_JWT_SECRET`.
```go
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error)
```
Creates a new OIDC provider. Returns error if issuer cannot be reached or no allowed users/groups are configured.
```go
func NewOIDCProviderFromEnv() (*OIDCProvider, error)
```
Creates OIDC provider from environment variables `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, etc.
## Architecture
### Core components
```mermaid
graph TD
A[HTTP Request] --> B{Auth Enabled?}
B -->|No| C[Proceed Direct]
B -->|Yes| D[Check Token]
D -->|Valid| E[Proceed]
D -->|Invalid| F[Login Handler]
G[OIDC Provider] --> H[Token Validation]
I[UserPass Provider] --> J[Credential Check]
F --> K{OIDC Configured?}
K -->|Yes| G
K -->|No| I
subgraph Cookie Management
L[Token Cookie]
M[State Cookie]
N[Session Cookie]
end
```
### OIDC authentication flow
```mermaid
sequenceDiagram
participant User
participant App
participant IdP
User->>App: Access Protected Resource
App->>App: Check Token
alt No valid token
App-->>User: Redirect to /auth/
User->>IdP: Login & Authorize
IdP-->>User: Redirect with Code
User->>App: /auth/callback?code=...
App->>IdP: Exchange Code for Token
IdP-->>App: Access Token + ID Token
App->>App: Validate Token
App->>App: Check allowed users/groups
App-->>User: Protected Resource
else Valid token exists
App-->>User: Protected Resource
end
```
### Username/password flow
```mermaid
sequenceDiagram
participant User
participant App
User->>App: POST /auth/callback
App->>App: Validate credentials
alt Valid
App->>App: Generate JWT
App-->>User: Set token cookie, redirect to /
else Invalid
App-->>User: 401 Unauthorized
end
```
## Configuration Surface
### Environment variables
| Variable | Description |
| ------------------------ | ----------------------------------------------------------- |
| `DEBUG_DISABLE_AUTH` | Set to "true" to disable auth for debugging |
| `API_JWT_SECRET` | Secret key for JWT token validation (enables userpass auth) |
| `API_USER` | Username for userpass authentication |
| `API_PASSWORD` | Password for userpass authentication |
| `API_JWT_TOKEN_TTL` | Token TTL duration (default: 24h) |
| `OIDC_ISSUER_URL` | OIDC provider URL (enables OIDC) |
| `OIDC_CLIENT_ID` | OIDC client ID |
| `OIDC_CLIENT_SECRET` | OIDC client secret |
| `OIDC_REDIRECT_URL` | OIDC redirect URL |
| `OIDC_ALLOWED_USERS` | Comma-separated list of allowed users |
| `OIDC_ALLOWED_GROUPS` | Comma-separated list of allowed groups |
| `OIDC_SCOPES` | Comma-separated OIDC scopes (default: openid,profile,email) |
| `OIDC_RATE_LIMIT` | Rate limit requests (default: 10) |
| `OIDC_RATE_LIMIT_PERIOD` | Rate limit period (default: 1m) |
### Hot-reloading
Authentication configuration requires restart. No dynamic reconfiguration is supported.
## Dependency and Integration Map
### Internal dependencies
- `internal/common` - Environment variable access
### External dependencies
- `golang.org/x/crypto/bcrypt` - Password hashing
- `github.com/coreos/go-oidc/v3/oidc` - OIDC protocol
- `golang.org/x/oauth2` - OAuth2/OIDC implementation
- `github.com/golang-jwt/jwt/v5` - JWT token handling
- `golang.org/x/time/rate` - OIDC rate limiting
### Integration points
```go
// Route middleware uses AuthOrProceed
routeHandler := func(w http.ResponseWriter, r *http.Request) {
if !auth.AuthOrProceed(w, r) {
return // Auth failed, login handler was invoked
}
// Continue with authenticated request
}
```
## Observability
### Logs
- OIDC provider initialization errors
- Token validation failures
- Rate limit exceeded events
### Metrics
No metrics are currently exposed.
## Security Considerations
- JWT tokens use HS512 signing for userpass auth
- OIDC tokens are validated against the issuer
- Session tokens are scoped by client ID to prevent conflicts
- Passwords are hashed with bcrypt (cost 10)
- OIDC rate limiting prevents brute-force attacks
- State parameter prevents CSRF attacks
- Refresh tokens are stored and invalidated on logout
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| ------------------------ | ------------------------------ | ----------------------------- |
| OIDC issuer unreachable | Initialize returns error | Fix network/URL configuration |
| Invalid JWT secret | Initialize uses API_JWT_SECRET | Provide correct secret |
| Token expired | CheckToken returns error | User must re-authenticate |
| User not in allowed list | Returns ErrUserNotAllowed | Add user to allowed list |
| Rate limit exceeded | Returns 429 Too Many Requests | Wait for rate limit reset |
## Usage Examples
### Basic setup
```go
// Initialize authentication during startup
err := auth.Initialize()
if err != nil {
log.Fatal(err)
}
// Check if auth is enabled
if auth.IsEnabled() {
log.Println("Authentication is enabled")
}
// Check OIDC status
if auth.IsOIDCEnabled() {
log.Println("OIDC authentication configured")
}
```
### Using AuthOrProceed middleware
```go
func protectedHandler(w http.ResponseWriter, r *http.Request) {
if !auth.AuthOrProceed(w, r) {
return // Auth failed, login handler was invoked
}
// Continue with authenticated request
}
```
### Using AuthCheckHandler
```go
http.HandleFunc("/api/", auth.AuthCheckHandler(apiHandler))
```
### Custom OIDC provider
```go
provider, err := auth.NewOIDCProvider(
"https://your-idp.com",
"your-client-id",
"your-client-secret",
[]string{"user1", "user2"},
[]string{"group1"},
)
if err != nil {
log.Fatal(err)
}
```
### Custom userpass provider
```go
provider, err := auth.NewUserPassAuth(
"admin",
"password123",
[]byte("jwt-secret-key"),
24*time.Hour,
)
if err != nil {
log.Fatal(err)
}
```