Add complete auth subsystem with OAuth 2.1 authorization server using ory/fosite, local user accounts with bcrypt password hashing, session management, and HTTP handlers for the Web UI. Components: - User store with bcrypt hashing (configurable cost, default 12), CRUD, validation (username 3-64 chars alphanumeric+underscore, password 8-72 bytes) - Session store with secure random IDs, configurable lifetime (default 24h), expiration cleanup, and per-user invalidation - OAuth client store with client_id/secret generation and bcrypt verification - Fosite storage adapter implementing CoreStorage, TokenRevocationStorage, and PKCERequestStorage backed by SQLite - OAuth provider configured with authorization code (PKCE S256 mandatory), client credentials, refresh token rotation, and token introspection - HTTP handlers: POST /auth/register, POST /auth/login, POST /auth/logout, GET /auth/me, PUT /auth/password, GET /oauth/authorize, POST /oauth/token, POST /oauth/introspect - Middleware: RequireSession (cookie), RequireBearer (access token), RequireAuth (either), RequireAdmin (role check) - Structured auth event logging (login, token issuance, session lifecycle) - Schema migration 002_auth.sql extending users, oauth_clients, oauth_tokens tables and adding sessions, oauth_authorization_codes tables - Initial admin user auto-created on first run with random password printed to stdout - All tests pass with CGO_ENABLED=0, zero external runtime dependencies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
64 lines
1.5 KiB
Go
64 lines
1.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
)
|
|
|
|
// Auth event type constants.
|
|
const (
|
|
EventLoginSuccess = "login_success"
|
|
EventLoginFailure = "login_failure"
|
|
EventTokenIssued = "token_issued"
|
|
EventTokenRefreshed = "token_refreshed"
|
|
EventTokenRevoked = "token_revoked"
|
|
EventSessionCreated = "session_created"
|
|
EventSessionDestroyed = "session_destroyed"
|
|
EventUserCreated = "user_created"
|
|
EventPasswordChanged = "password_changed"
|
|
)
|
|
|
|
// AuthEvent represents a structured auth event for logging.
|
|
type AuthEvent struct {
|
|
Type string
|
|
UserID int64
|
|
Username string
|
|
ClientID string
|
|
RemoteIP string
|
|
Details map[string]any
|
|
}
|
|
|
|
// LogAuthEvent logs an authentication event with structured fields.
|
|
func LogAuthEvent(ctx context.Context, logger *slog.Logger, event AuthEvent) {
|
|
attrs := []any{
|
|
"event", event.Type,
|
|
}
|
|
|
|
if event.UserID > 0 {
|
|
attrs = append(attrs, "user_id", event.UserID)
|
|
}
|
|
if event.Username != "" {
|
|
attrs = append(attrs, "username", event.Username)
|
|
}
|
|
if event.ClientID != "" {
|
|
attrs = append(attrs, "client_id", event.ClientID)
|
|
}
|
|
if event.RemoteIP != "" {
|
|
attrs = append(attrs, "remote_ip", event.RemoteIP)
|
|
}
|
|
for k, v := range event.Details {
|
|
attrs = append(attrs, k, v)
|
|
}
|
|
|
|
logger.InfoContext(ctx, "auth event", attrs...)
|
|
}
|
|
|
|
// remoteIP extracts the remote IP from an HTTP request.
|
|
func remoteIP(r *http.Request) string {
|
|
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
return forwarded
|
|
}
|
|
return r.RemoteAddr
|
|
}
|