diff --git a/.gitignore b/.gitignore index da6f209..cc66455 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ k8s/*-secret.yaml # Development files dev-* + +*.log diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e7806fe..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,73 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Changed -- License changed from MIT to Apache 2.0 for better patent protection and enterprise adoption - -### Added -- Initial release of Passkey Auth -- WebAuthn (FIDO2) authentication support -- SQLite database with user management -- Email-based user identification -- Admin approval workflow for new users -- Email allowlist support -- Session management with secure cookies -- Kubernetes deployment configuration -- nginx ingress auth backend support -- Docker containerization with Debian base -- Web-based admin interface -- User registration and login flows -- Real-time WebAuthn credential management - -### Features -- **Authentication**: WebAuthn/FIDO2 passkey authentication -- **Database**: SQLite with user and credential storage -- **Admin Interface**: Web UI for user management and approval -- **Email Allowlist**: Restrict registration to specific email addresses -- **Session Management**: Secure session handling with configurable secrets -- **Docker Support**: Multi-stage builds with Debian base for SQLite compatibility -- **Kubernetes Ready**: Complete deployment manifests and ingress configuration -- **nginx Integration**: Auth backend for protecting upstream services - -### Security -- WebAuthn challenge/response authentication -- Secure session cookie handling -- Admin approval workflow -- Email-based access control -- HTTPS-ready configuration - -### Documentation -- Comprehensive README with setup instructions -- API endpoint documentation -- Kubernetes deployment guide -- Docker usage examples -- Troubleshooting guide -- Contributing guidelines - -### Technical Details -- Go 1.21+ with CGO for SQLite -- Base64URL encoding/decoding for WebAuthn data -- CORS support for cross-origin requests -- Environment variable configuration -- Health check endpoints -- Structured logging - -## [0.1.0] - 2025-08-04 - -### Added -- Initial project structure -- Core WebAuthn implementation -- SQLite database integration -- Basic web interface -- Docker containerization -- Kubernetes manifests - ---- - -**Note**: This project follows semantic versioning. For upgrade instructions and breaking changes, see the README.md file. diff --git a/config.example.yaml b/config.example.yaml index 6b3deb7..c18db6a 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -53,6 +53,10 @@ auth: # - "user1@yourcompany.com" # - "user2@yourcompany.com" + # Admin email - user with this email will be auto-approved and can access admin panel + # Can also be set via ADMIN_EMAIL environment variable + admin_email: "" # Set this to your admin email address + # Environment-specific overrides can be set via environment variables: # - PORT: Server port # - HOST: Server host @@ -60,3 +64,4 @@ auth: # - DATABASE_PATH: Database file path # - SESSION_SECRET: Session encryption secret # - ALLOWED_EMAILS: Comma-separated list of allowed emails +# - ADMIN_EMAIL: Admin email address diff --git a/config.yaml b/config.yaml index dc57e2e..8732e15 100644 --- a/config.yaml +++ b/config.yaml @@ -22,3 +22,5 @@ auth: allowed_emails: # - "admin@example.com" # - "user@example.com" + # Admin email - user with this email will be auto-approved and can access admin panel + admin_email: "" # Set this to your admin email or use ADMIN_EMAIL environment variable diff --git a/internal/config/config.go b/internal/config/config.go index ebc922f..6cce6d5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,6 +40,7 @@ type AuthConfig struct { SessionSecret string `yaml:"session_secret"` RequireApproval bool `yaml:"require_approval"` AllowedEmails []string `yaml:"allowed_emails"` + AdminEmail string `yaml:"admin_email"` } func Load() (*Config, error) { @@ -113,6 +114,9 @@ func Load() (*Config, error) { config.Auth.AllowedEmails[i] = strings.TrimSpace(email) } } + if adminEmail := os.Getenv("ADMIN_EMAIL"); adminEmail != "" { + config.Auth.AdminEmail = adminEmail + } return config, nil } @@ -135,6 +139,11 @@ func (c *Config) IsEmailAllowed(email string) bool { return false } +// IsAdmin checks if an email address is the admin email +func (c *Config) IsAdmin(email string) bool { + return c.Auth.AdminEmail != "" && email == c.Auth.AdminEmail +} + // validateConfigPath ensures the config path is safe and doesn't allow path traversal func validateConfigPath(path string) error { // Clean the path and check for path traversal attempts diff --git a/internal/database/database.go b/internal/database/database.go index 6299e2f..c43982d 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -31,7 +31,7 @@ type Credential struct { } func New(dbPath string) (*DB, error) { - conn, err := sql.Open("sqlite3", dbPath+"?_fk=1") + conn, err := sql.Open("sqlite", dbPath+"?_fk=1") if err != nil { return nil, err } @@ -98,6 +98,23 @@ func (db *DB) CreateUser(email, displayName string) (*User, error) { return db.GetUser(int(id)) } +func (db *DB) CreateUserWithApproval(email, displayName string, approved bool) (*User, error) { + result, err := db.conn.Exec( + "INSERT INTO users (email, display_name, approved) VALUES (?, ?, ?)", + email, displayName, approved, + ) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return db.GetUser(int(id)) +} + func (db *DB) GetUser(id int) (*User, error) { var user User err := db.conn.QueryRow( diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 6571180..ccec451 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -94,13 +94,18 @@ func (h *Handlers) BeginRegistration(w http.ResponseWriter, r *http.Request) { } // Create new user - user, err := h.db.CreateUser(req.Email, req.DisplayName) + isAdmin := h.config.IsAdmin(req.Email) + user, err := h.db.CreateUserWithApproval(req.Email, req.DisplayName, isAdmin) if err != nil { logrus.Errorf("Failed to create user: %v", err) h.writeError(w, "Failed to create user", http.StatusInternalServerError) return } + if isAdmin { + logrus.Infof("Admin user auto-approved: %s", req.Email) + } + webAuthnUser := &auth.WebAuthnUser{} webAuthnUser.SetUser(user) @@ -190,7 +195,17 @@ func (h *Handlers) FinishRegistration(w http.ResponseWriter, r *http.Request) { return } - // Clear session + // Set authenticated session after successful registration + authSession, _ := h.store.Get(r, "auth-session") + authSession.Values["authenticated"] = true + authSession.Values["user_id"] = user.ID + authSession.Values["user_email"] = user.Email + if err := authSession.Save(r, w); err != nil { + h.writeError(w, "Failed to save auth session", http.StatusInternalServerError) + return + } + + // Clear webauthn session session.Values["challenge"] = nil session.Values["user_id"] = nil if err := session.Save(r, w); err != nil { @@ -364,11 +379,55 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +// GetAuthStatus returns the current authentication status +func (h *Handlers) GetAuthStatus(w http.ResponseWriter, r *http.Request) { + session, err := h.store.Get(r, "auth-session") + if err != nil { + h.writeError(w, "Failed to get session", http.StatusInternalServerError) + return + } + + userEmail, ok := session.Values["user_email"].(string) + if !ok || userEmail == "" { + h.writeError(w, "Not authenticated", http.StatusUnauthorized) + return + } + + // Get user details from database + user, err := h.db.GetUserByEmail(userEmail) + if err != nil { + h.writeError(w, "User not found", http.StatusNotFound) + return + } + + // Check if user is approved + if !user.Approved { + h.writeError(w, "User not approved", http.StatusForbidden) + return + } + + response := map[string]interface{}{ + "authenticated": true, + "user": map[string]interface{}{ + "id": user.ID, + "email": user.Email, + "display_name": user.DisplayName, + "approved": user.Approved, + "is_admin": h.config.IsAdmin(user.Email), + }, + } + + h.writeJSON(w, response) +} + // Admin endpoints // ListUsers returns all users (admin endpoint) func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) { - // TODO: Add admin authentication check + if !h.requireAdmin(w, r) { + return + } + users, err := h.db.ListUsers() if err != nil { logrus.Errorf("Failed to list users: %v", err) @@ -381,7 +440,10 @@ func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) { // CreateUser creates a new user (admin endpoint) func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { - // TODO: Add admin authentication check + if !h.requireAdmin(w, r) { + return + } + var req struct { Email string `json:"email"` DisplayName string `json:"display_name"` @@ -399,7 +461,7 @@ func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { return } - user, err := h.db.CreateUser(req.Email, req.DisplayName) + user, err := h.db.CreateUserWithApproval(req.Email, req.DisplayName, req.Approved) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { h.writeError(w, "User already exists", http.StatusConflict) @@ -410,18 +472,15 @@ func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { return } - if req.Approved { - if err := h.db.ApproveUser(user.ID); err != nil { - logrus.Errorf("Failed to approve user: %v", err) - } - } - h.writeJSON(w, user) } // UpdateUser updates a user (admin endpoint) func (h *Handlers) UpdateUser(w http.ResponseWriter, r *http.Request) { - // TODO: Add admin authentication check + if !h.requireAdmin(w, r) { + return + } + vars := mux.Vars(r) idStr, ok := vars["id"] if !ok { @@ -467,7 +526,10 @@ func (h *Handlers) UpdateUser(w http.ResponseWriter, r *http.Request) { // DeleteUser deletes a user (admin endpoint) func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) { - // TODO: Add admin authentication check + if !h.requireAdmin(w, r) { + return + } + vars := mux.Vars(r) idStr, ok := vars["id"] if !ok { @@ -489,3 +551,25 @@ func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) { h.writeJSON(w, map[string]string{"status": "success"}) } + +func (h *Handlers) isAdmin(r *http.Request) bool { + session, err := h.store.Get(r, "auth-session") + if err != nil { + return false + } + + userEmail, ok := session.Values["user_email"].(string) + if !ok { + return false + } + + return h.config.IsAdmin(userEmail) +} + +func (h *Handlers) requireAdmin(w http.ResponseWriter, r *http.Request) bool { + if !h.isAdmin(r) { + h.writeError(w, "Admin access required", http.StatusForbidden) + return false + } + return true +} diff --git a/main.go b/main.go index 8622c65..e6a8d7a 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,7 @@ func main() { api.HandleFunc("/login/begin", h.BeginLogin).Methods("POST") api.HandleFunc("/login/finish", h.FinishLogin).Methods("POST") api.HandleFunc("/logout", h.Logout).Methods("POST") + api.HandleFunc("/auth/status", h.GetAuthStatus).Methods("GET") api.HandleFunc("/users", h.ListUsers).Methods("GET") api.HandleFunc("/users", h.CreateUser).Methods("POST") api.HandleFunc("/users/{id}", h.UpdateUser).Methods("PUT") diff --git a/web/index.html b/web/index.html index b94bf65..1184732 100644 --- a/web/index.html +++ b/web/index.html @@ -1,263 +1,195 @@ - + - Passkey Auth - Admin + + Passkey Authentication + -
-
-

🔐 Passkey Auth

-

Admin Dashboard

-
+
+
+
+

🔐 Passkey Authentication

+

Secure passwordless authentication

+
+ +
-
- - - -
+ +
+
+
+

Sign in to your account

+

Use your passkey for secure authentication

+
+ +
+ + + +
+ +
+ +
+
+
- -
-

Register New User

-
-
- - + +
+
+
+

Welcome back!

+

You're successfully authenticated

+
+ +
+ +
+ +
+
+
+ + +
+
+
+

User Management

+

Manage user accounts and permissions

+
+ + + +
+
Loading users...
-
- - -
- - -
- - -
-

Test Login

-
-
- - -
- -
-
-
- - -
-

Manage Users

- -
-
Loading users...
-
-
+ + +
-
+