Junie: security cleanups.
This commit is contained in:
parent
95d96732d2
commit
23c7a65799
43
README.org
43
README.org
|
@ -10,10 +10,10 @@
|
|||
1. User password authentication.
|
||||
2. User token authentication.
|
||||
3. Database credential authentication.
|
||||
4. TOTP (Time-based One-Time Password) authentication.
|
||||
|
||||
Future work should consider adding support for:
|
||||
1. TOTP (Time-based One-Time Password)
|
||||
2. Policy management for fine-grained access control.
|
||||
1. Policy management for fine-grained access control.
|
||||
|
||||
* Documentation
|
||||
|
||||
|
@ -29,12 +29,12 @@
|
|||
|
||||
1. Initialize the database:
|
||||
#+begin_src bash
|
||||
go run main.go init --db ./mcias.db
|
||||
go run cmd/mcias/main.go init --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
2. Start the server:
|
||||
#+begin_src bash
|
||||
go run main.go server --db ./mcias.db
|
||||
go run cmd/mcias/main.go server --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
3. The server will listen on port 8080 by default.
|
||||
|
@ -52,55 +52,72 @@
|
|||
|
||||
Start the MCIAS server:
|
||||
#+begin_src bash
|
||||
go run main.go server [--db <path>] [--addr <address>]
|
||||
go run cmd/mcias/main.go server [--db <path>] [--addr <address>]
|
||||
#+end_src
|
||||
|
||||
** Init Command
|
||||
|
||||
Initialize the database:
|
||||
#+begin_src bash
|
||||
go run main.go init [--db <path>]
|
||||
go run cmd/mcias/main.go init [--db <path>]
|
||||
#+end_src
|
||||
|
||||
** User Commands
|
||||
|
||||
Add a new user:
|
||||
#+begin_src bash
|
||||
go run main.go user add --username <username> --password <password>
|
||||
go run cmd/mcias/main.go user add --username <username> --password <password>
|
||||
#+end_src
|
||||
|
||||
List all users:
|
||||
#+begin_src bash
|
||||
go run main.go user list
|
||||
go run cmd/mcias/main.go user list
|
||||
#+end_src
|
||||
|
||||
** Token Commands
|
||||
|
||||
Add a new token for a user:
|
||||
#+begin_src bash
|
||||
go run main.go token add --username <username> [--duration <hours>]
|
||||
go run cmd/mcias/main.go token add --username <username> [--duration <hours>]
|
||||
#+end_src
|
||||
|
||||
List all tokens:
|
||||
#+begin_src bash
|
||||
go run main.go token list
|
||||
go run cmd/mcias/main.go token list
|
||||
#+end_src
|
||||
|
||||
** TOTP Commands
|
||||
|
||||
Enable TOTP for a user:
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go totp enable --username <username>
|
||||
#+end_src
|
||||
|
||||
Add a TOTP token with QR code generation:
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go totp add --username <username> --qr-output <path/to/qrcode.png> [--issuer <issuer>]
|
||||
#+end_src
|
||||
|
||||
Validate a TOTP code:
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go totp validate --username <username> --code <totp_code>
|
||||
#+end_src
|
||||
|
||||
** Migrate Commands
|
||||
|
||||
Apply database migrations:
|
||||
#+begin_src bash
|
||||
go run main.go migrate up [--migrations <dir>] [--steps <n>]
|
||||
go run cmd/mcias/main.go migrate up [--migrations <dir>] [--steps <n>]
|
||||
#+end_src
|
||||
|
||||
Revert database migrations:
|
||||
#+begin_src bash
|
||||
go run main.go migrate down [--migrations <dir>] [--steps <n>]
|
||||
go run cmd/mcias/main.go migrate down [--migrations <dir>] [--steps <n>]
|
||||
#+end_src
|
||||
|
||||
Show current migration version:
|
||||
#+begin_src bash
|
||||
go run main.go migrate version [--migrations <dir>]
|
||||
go run cmd/mcias/main.go migrate version [--migrations <dir>]
|
||||
#+end_src
|
||||
|
||||
* API Overview
|
||||
|
|
240
api/auth.go
240
api/auth.go
|
@ -1,14 +1,18 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/data"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
|
@ -63,48 +67,51 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check password only first
|
||||
// Check password only
|
||||
if !user.CheckPassword(&req.Login) {
|
||||
s.sendError(w, "Invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// If TOTP is enabled and a code was provided, verify it
|
||||
if user.HasTOTP() {
|
||||
if req.Login.TOTPCode == "" {
|
||||
// TOTP is enabled but no code was provided
|
||||
// Return a special response indicating TOTP is required
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if err := json.NewEncoder(w).Encode(ErrorResponse{
|
||||
Error: "TOTP code required",
|
||||
}); err != nil {
|
||||
s.Logger.Printf("Error encoding response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the TOTP code
|
||||
valid, validErr := user.ValidateTOTPCode(req.Login.TOTPCode)
|
||||
if validErr != nil || !valid {
|
||||
s.sendError(w, "Invalid TOTP code", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Password is correct, create a token regardless of TOTP status
|
||||
token, expires, err := s.createToken(user.ID)
|
||||
if err != nil {
|
||||
s.Logger.Printf("Token creation error: %v", err)
|
||||
// Log the security event
|
||||
details := map[string]string{
|
||||
"reason": "Token creation error",
|
||||
"error": err.Error(),
|
||||
}
|
||||
s.LogSecurityEvent(r, "login_attempt", user.ID, user.User, false, details)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If user has TOTP enabled, include this information in the response
|
||||
totpEnabled := user.HasTOTP()
|
||||
|
||||
// Log successful login
|
||||
details := map[string]string{
|
||||
"token_expires": time.Unix(expires, 0).UTC().Format(time.RFC3339),
|
||||
"totp_enabled": fmt.Sprintf("%t", totpEnabled),
|
||||
}
|
||||
s.LogSecurityEvent(r, "login_success", user.ID, user.User, true, details)
|
||||
|
||||
// Include TOTP status in the response
|
||||
response := struct {
|
||||
TokenResponse
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
}{
|
||||
TokenResponse: TokenResponse{
|
||||
Token: token,
|
||||
Expires: expires,
|
||||
},
|
||||
TOTPEnabled: totpEnabled,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(TokenResponse{
|
||||
Token: token,
|
||||
Expires: expires,
|
||||
}); err != nil {
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
s.Logger.Printf("Error encoding response: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -147,20 +154,61 @@ func (s *Server) handleTokenLogin(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (s *Server) sendError(w http.ResponseWriter, message string, status int) {
|
||||
// Log the detailed error message for debugging
|
||||
s.Logger.Printf("Error response: %s (Status: %d)", message, status)
|
||||
|
||||
// Create a generic error message based on status code
|
||||
publicMessage := "An error occurred processing your request"
|
||||
errorCode := "E" + string(status)
|
||||
|
||||
// Customize public messages for common status codes
|
||||
// but don't leak specific details about the error
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
publicMessage = "Invalid request format"
|
||||
case http.StatusUnauthorized:
|
||||
publicMessage = "Authentication required"
|
||||
case http.StatusForbidden:
|
||||
publicMessage = "Insufficient permissions"
|
||||
case http.StatusNotFound:
|
||||
publicMessage = "Resource not found"
|
||||
case http.StatusTooManyRequests:
|
||||
publicMessage = "Rate limit exceeded"
|
||||
case http.StatusInternalServerError:
|
||||
publicMessage = "Internal server error"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(ErrorResponse{Error: message}); err != nil {
|
||||
|
||||
response := struct {
|
||||
Error string `json:"error"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
}{
|
||||
Error: publicMessage,
|
||||
ErrorCode: errorCode,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
s.Logger.Printf("Error encoding error response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getUserByUsername(username string) (*data.User, error) {
|
||||
query := `SELECT id, created, user, password, salt, totp_secret FROM users WHERE user = ?`
|
||||
row := s.DB.QueryRow(query, username)
|
||||
// Use squirrel to build the query safely
|
||||
query, args, err := squirrel.Select("id", "created", "user", "password", "salt", "totp_secret").
|
||||
From("users").
|
||||
Where(squirrel.Eq{"user": username}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row := s.DB.QueryRow(query, args...)
|
||||
|
||||
user := &data.User{}
|
||||
var totpSecret sql.NullString
|
||||
err := row.Scan(&user.ID, &user.Created, &user.User, &user.Password, &user.Salt, &totpSecret)
|
||||
err = row.Scan(&user.ID, &user.Created, &user.User, &user.Password, &user.Salt, &totpSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -170,12 +218,17 @@ func (s *Server) getUserByUsername(username string) (*data.User, error) {
|
|||
user.TOTPSecret = totpSecret.String
|
||||
}
|
||||
|
||||
rolesQuery := `
|
||||
SELECT r.role FROM roles r
|
||||
JOIN user_roles ur ON r.id = ur.rid
|
||||
WHERE ur.uid = ?
|
||||
`
|
||||
rows, err := s.DB.Query(rolesQuery, user.ID)
|
||||
// Use squirrel to build the roles query safely
|
||||
rolesQuery, rolesArgs, err := squirrel.Select("r.role").
|
||||
From("roles r").
|
||||
Join("user_roles ur ON r.id = ur.rid").
|
||||
Where(squirrel.Eq{"ur.uid": user.ID}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := s.DB.Query(rolesQuery, rolesArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -195,12 +248,28 @@ func (s *Server) getUserByUsername(username string) (*data.User, error) {
|
|||
}
|
||||
|
||||
func (s *Server) createToken(userID string) (string, int64, error) {
|
||||
token := ulid.Make().String()
|
||||
// Generate 16 bytes of random data
|
||||
tokenBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// Hex encode the random bytes to get a 32-character string
|
||||
token := hex.EncodeToString(tokenBytes)
|
||||
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
|
||||
tokenID := ulid.Make().String()
|
||||
_, err := s.DB.Exec(query, tokenID, userID, token, expires)
|
||||
|
||||
// Use squirrel to build the insert query safely
|
||||
query, args, err := squirrel.Insert("tokens").
|
||||
Columns("id", "uid", "token", "expires").
|
||||
Values(tokenID, userID, token, expires).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
_, err = s.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
@ -209,14 +278,22 @@ func (s *Server) createToken(userID string) (string, int64, error) {
|
|||
}
|
||||
|
||||
func (s *Server) verifyToken(username, token string) (string, error) {
|
||||
query := `
|
||||
SELECT t.uid, t.expires FROM tokens t
|
||||
JOIN users u ON t.uid = u.id
|
||||
WHERE u.user = ? AND t.token = ?
|
||||
`
|
||||
// Use squirrel to build the query safely
|
||||
query, args, err := squirrel.Select("t.uid", "t.expires").
|
||||
From("tokens t").
|
||||
Join("users u ON t.uid = u.id").
|
||||
Where(squirrel.And{
|
||||
squirrel.Eq{"u.user": username},
|
||||
squirrel.Eq{"t.token": token},
|
||||
}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var userID string
|
||||
var expires int64
|
||||
err := s.DB.QueryRow(query, username, token).Scan(&userID, &expires)
|
||||
err = s.DB.QueryRow(query, args...).Scan(&userID, &expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -230,21 +307,38 @@ func (s *Server) verifyToken(username, token string) (string, error) {
|
|||
|
||||
func (s *Server) renewToken(username, token string) (int64, error) {
|
||||
// First, verify the token exists and get the token ID
|
||||
query := `
|
||||
SELECT t.id FROM tokens t
|
||||
JOIN users u ON t.uid = u.id
|
||||
WHERE u.user = ? AND t.token = ?
|
||||
`
|
||||
// Use squirrel to build the query safely
|
||||
query, args, err := squirrel.Select("t.id").
|
||||
From("tokens t").
|
||||
Join("users u ON t.uid = u.id").
|
||||
Where(squirrel.And{
|
||||
squirrel.Eq{"u.user": username},
|
||||
squirrel.Eq{"t.token": token},
|
||||
}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var tokenID string
|
||||
err := s.DB.QueryRow(query, username, token).Scan(&tokenID)
|
||||
err = s.DB.QueryRow(query, args...).Scan(&tokenID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Update the token's expiry time
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
updateQuery := `UPDATE tokens SET expires = ? WHERE id = ?`
|
||||
_, err = s.DB.Exec(updateQuery, expires, tokenID)
|
||||
|
||||
// Use squirrel to build the update query safely
|
||||
updateQuery, updateArgs, err := squirrel.Update("tokens").
|
||||
Set("expires", expires).
|
||||
Where(squirrel.Eq{"id": tokenID}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = s.DB.Exec(updateQuery, updateArgs...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -277,6 +371,11 @@ func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Check if TOTP is enabled for the user
|
||||
if !user.HasTOTP() {
|
||||
// Log the security event
|
||||
details := map[string]string{
|
||||
"reason": "TOTP not enabled for user",
|
||||
}
|
||||
s.LogSecurityEvent(r, "totp_verification_attempt", user.ID, user.User, false, details)
|
||||
s.sendError(w, "TOTP not enabled for user", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
@ -284,6 +383,11 @@ func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
|
|||
// Validate the TOTP code
|
||||
valid, validErr := user.ValidateTOTPCode(req.TOTPCode)
|
||||
if validErr != nil || !valid {
|
||||
// Log the security event
|
||||
details := map[string]string{
|
||||
"reason": "Invalid TOTP code",
|
||||
}
|
||||
s.LogSecurityEvent(r, "totp_verification_attempt", user.ID, user.User, false, details)
|
||||
s.sendError(w, "Invalid TOTP code", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
@ -292,10 +396,22 @@ func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
|
|||
token, expires, err := s.createToken(user.ID)
|
||||
if err != nil {
|
||||
s.Logger.Printf("Token creation error: %v", err)
|
||||
// Log the security event
|
||||
details := map[string]string{
|
||||
"reason": "Token creation error",
|
||||
"error": err.Error(),
|
||||
}
|
||||
s.LogSecurityEvent(r, "totp_verification_attempt", user.ID, user.User, false, details)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Log successful TOTP verification
|
||||
details := map[string]string{
|
||||
"token_expires": time.Unix(expires, 0).UTC().Format(time.RFC3339),
|
||||
}
|
||||
s.LogSecurityEvent(r, "totp_verification_success", user.ID, user.User, true, details)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(TokenResponse{
|
||||
|
@ -356,8 +472,18 @@ func (s *Server) handleDatabaseCredentials(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
// Retrieve database credentials
|
||||
query := `SELECT id, host, port, name, user, password FROM database LIMIT 1`
|
||||
row := s.DB.QueryRow(query)
|
||||
// Use squirrel to build the query safely
|
||||
query, args, err := squirrel.Select("id", "host", "port", "name", "user", "password").
|
||||
From("database").
|
||||
Limit(1).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
s.Logger.Printf("Query building error: %v", err)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
row := s.DB.QueryRow(query, args...)
|
||||
|
||||
var id string
|
||||
var creds DatabaseCredentials
|
||||
|
|
187
api/server.go
187
api/server.go
|
@ -2,28 +2,134 @@ package api
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/data"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// client represents a client with rate limiting information
|
||||
type client struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
// SecurityEvent represents a security-related event
|
||||
type SecurityEvent struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
EventType string `json:"event_type"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Success bool `json:"success"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// RateLimiter manages rate limiting for clients
|
||||
type RateLimiter struct {
|
||||
clients map[string]*client
|
||||
mu sync.Mutex
|
||||
// Requests per second, burst size
|
||||
rate rate.Limit
|
||||
burst int
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a new rate limiter
|
||||
func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
clients: make(map[string]*client),
|
||||
rate: r,
|
||||
burst: b,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLimiter returns a rate limiter for a client
|
||||
func (rl *RateLimiter) GetLimiter(ip string) *rate.Limiter {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
c, exists := rl.clients[ip]
|
||||
if !exists {
|
||||
c = &client{
|
||||
limiter: rate.NewLimiter(rl.rate, rl.burst),
|
||||
lastSeen: time.Now(),
|
||||
}
|
||||
rl.clients[ip] = c
|
||||
} else {
|
||||
c.lastSeen = time.Now()
|
||||
}
|
||||
|
||||
return c.limiter
|
||||
}
|
||||
|
||||
// CleanupClients removes old clients
|
||||
func (rl *RateLimiter) CleanupClients() {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
for ip, client := range rl.clients {
|
||||
if time.Since(client.lastSeen) > 1*time.Hour {
|
||||
delete(rl.clients, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
DB *sql.DB
|
||||
Router *http.ServeMux
|
||||
Logger *log.Logger
|
||||
Auth *data.AuthorizationService
|
||||
DB *sql.DB
|
||||
Router *http.ServeMux
|
||||
Logger *log.Logger
|
||||
Auth *data.AuthorizationService
|
||||
RateLimiter *RateLimiter
|
||||
}
|
||||
|
||||
// getClientIP extracts the client IP address from the request
|
||||
func getClientIP(r *http.Request) string {
|
||||
// Check for X-Forwarded-For header first (for clients behind proxy)
|
||||
ip := r.Header.Get("X-Forwarded-For")
|
||||
if ip != "" {
|
||||
// X-Forwarded-For can contain multiple IPs, use the first one
|
||||
ips := net.ParseIP(ip)
|
||||
if ips != nil {
|
||||
return ips.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to RemoteAddr
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewServer(db *sql.DB, logger *log.Logger) *Server {
|
||||
// Create a rate limiter with 10 requests per second and burst of 30
|
||||
rateLimiter := NewRateLimiter(10, 30)
|
||||
|
||||
s := &Server{
|
||||
DB: db,
|
||||
Router: http.NewServeMux(),
|
||||
Logger: logger,
|
||||
Auth: data.NewAuthorizationService(db),
|
||||
DB: db,
|
||||
Router: http.NewServeMux(),
|
||||
Logger: logger,
|
||||
Auth: data.NewAuthorizationService(db),
|
||||
RateLimiter: rateLimiter,
|
||||
}
|
||||
|
||||
// Start a goroutine to clean up old clients
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Hour)
|
||||
rateLimiter.CleanupClients()
|
||||
}
|
||||
}()
|
||||
|
||||
s.registerRoutes()
|
||||
|
||||
return s
|
||||
|
@ -36,11 +142,76 @@ func (s *Server) registerRoutes() {
|
|||
s.Router.HandleFunc("GET /v1/database/credentials", s.handleDatabaseCredentials)
|
||||
}
|
||||
|
||||
// sendRateLimitExceeded sends a rate limit exceeded response
|
||||
func (s *Server) sendRateLimitExceeded(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
|
||||
response := map[string]string{
|
||||
"error": "Rate limit exceeded. Please try again later.",
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
s.Logger.Printf("Error encoding rate limit response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Get client IP
|
||||
clientIP := getClientIP(r)
|
||||
|
||||
// Get rate limiter for this client
|
||||
limiter := s.RateLimiter.GetLimiter(clientIP)
|
||||
|
||||
// Check if rate limit is exceeded
|
||||
if !limiter.Allow() {
|
||||
s.Logger.Printf("Rate limit exceeded for IP: %s, URI: %s", clientIP, r.RequestURI)
|
||||
s.sendRateLimitExceeded(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply stricter rate limiting for authentication endpoints
|
||||
if r.URL.Path == "/v1/login/password" || r.URL.Path == "/v1/login/token" || r.URL.Path == "/v1/login/totp" {
|
||||
// Use a separate limiter with lower rate for auth endpoints
|
||||
authLimiter := rate.NewLimiter(rate.Limit(1), 5) // 1 request per second, burst of 5
|
||||
if !authLimiter.Allow() {
|
||||
s.Logger.Printf("Auth rate limit exceeded for IP: %s, URI: %s", clientIP, r.RequestURI)
|
||||
s.sendRateLimitExceeded(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with the request
|
||||
s.Router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// LogSecurityEvent logs a security-related event
|
||||
func (s *Server) LogSecurityEvent(r *http.Request, eventType string, userID, username string, success bool, details map[string]string) {
|
||||
event := SecurityEvent{
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
EventType: eventType,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
IPAddress: getClientIP(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
RequestURI: r.RequestURI,
|
||||
Success: success,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
// Convert to JSON for structured logging
|
||||
eventJSON, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
s.Logger.Printf("Error marshaling security event: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the security event
|
||||
s.Logger.Printf("SECURITY_EVENT: %s", eventJSON)
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) error {
|
||||
s.Logger.Printf("Starting server on %s", addr)
|
||||
s.Logger.Printf("Note: This server is designed to run behind a reverse proxy that handles TLS")
|
||||
return http.ListenAndServe(addr, s)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
@ -76,7 +78,14 @@ func addToken() {
|
|||
logger.Fatalf("Failed to check if user exists: %v", err)
|
||||
}
|
||||
|
||||
token := ulid.Make().String()
|
||||
// Generate 16 bytes of random data
|
||||
tokenBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
logger.Fatalf("Failed to generate random token: %v", err)
|
||||
}
|
||||
|
||||
// Hex encode the random bytes to get a 32-character string
|
||||
token := hex.EncodeToString(tokenBytes)
|
||||
|
||||
expires := time.Now().Add(time.Duration(tokenDuration) * time.Hour).Unix()
|
||||
|
||||
|
|
22
data/user.go
22
data/user.go
|
@ -81,25 +81,11 @@ func (u *User) CheckPassword(login *Login) bool {
|
|||
return subtle.ConstantTimeCompare(derived, u.Password) == validCompareResult
|
||||
}
|
||||
|
||||
// Check is a legacy method that now only checks the password
|
||||
// It's kept for backward compatibility but is equivalent to CheckPassword
|
||||
func (u *User) Check(login *Login) bool {
|
||||
// First check username and password
|
||||
if !u.CheckPassword(login) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If TOTP is enabled for the user, validate the TOTP code
|
||||
if u.TOTPSecret != emptyString && login.TOTPCode != emptyString {
|
||||
// Use the ValidateTOTPCode method to validate the TOTP code
|
||||
valid, validErr := u.ValidateTOTPCode(login.TOTPCode)
|
||||
if validErr != nil || !valid {
|
||||
return false
|
||||
}
|
||||
} else if u.TOTPSecret != emptyString && login.TOTPCode == emptyString {
|
||||
// TOTP is enabled but no code was provided
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
// Only check username and password, TOTP verification is now a separate flow
|
||||
return u.CheckPassword(login)
|
||||
}
|
||||
|
||||
func (u *User) Register(login *Login) error {
|
||||
|
|
67
docs/api.md
67
docs/api.md
|
@ -12,7 +12,7 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
|
|||
|
||||
**Endpoint**: `POST /v1/login/password`
|
||||
|
||||
**Description**: Authenticates a user using username and password credentials.
|
||||
**Description**: Authenticates a user using username and password credentials. This endpoint does not require TOTP verification, even if TOTP is enabled for the user.
|
||||
|
||||
**Request Format**:
|
||||
```json
|
||||
|
@ -34,13 +34,15 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
|
|||
```json
|
||||
{
|
||||
"token": "authentication_token",
|
||||
"expires": 1621234567
|
||||
"expires": 1621234567,
|
||||
"totp_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields**:
|
||||
- `token`: Authentication token to be used for subsequent requests
|
||||
- `expires`: Unix timestamp when the token expires
|
||||
- `totp_enabled`: Boolean indicating whether TOTP is enabled for the user
|
||||
|
||||
**Error Responses**:
|
||||
- 400 Bad Request: Invalid request format or missing required fields
|
||||
|
@ -86,6 +88,43 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
|
|||
- 401 Unauthorized: Invalid or expired token
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
#### TOTP Verification
|
||||
|
||||
**Endpoint**: `POST /v1/login/totp`
|
||||
|
||||
**Description**: Verifies a TOTP code for a user and issues a token upon successful verification. This endpoint is used as a separate flow from password authentication.
|
||||
|
||||
**Request Format**:
|
||||
```json
|
||||
{
|
||||
"version": "v1",
|
||||
"username": "username",
|
||||
"totp_code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields**:
|
||||
- `version`: Must be "v1"
|
||||
- `username`: Username
|
||||
- `totp_code`: Time-based One-Time Password code
|
||||
|
||||
**Response Format** (Success - 200 OK):
|
||||
```json
|
||||
{
|
||||
"token": "authentication_token",
|
||||
"expires": 1621234567
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields**:
|
||||
- `token`: Authentication token to be used for subsequent requests
|
||||
- `expires`: Unix timestamp when the token expires
|
||||
|
||||
**Error Responses**:
|
||||
- 400 Bad Request: Invalid request format, missing required fields, or TOTP not enabled for user
|
||||
- 401 Unauthorized: Invalid TOTP code
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
### Database Credentials
|
||||
|
||||
**Endpoint**: `/v1/credentials/database` (Not yet implemented)
|
||||
|
@ -112,14 +151,34 @@ Common HTTP status codes:
|
|||
|
||||
## Authentication Flow
|
||||
|
||||
### Password Authentication Flow
|
||||
|
||||
1. **Initial Authentication**:
|
||||
- Client sends username and password to `/v1/login/password`
|
||||
- Server validates credentials and returns a token
|
||||
- The response includes a `totp_enabled` flag indicating whether TOTP is enabled for the user
|
||||
|
||||
2. **Subsequent Requests**:
|
||||
- Client uses the token for authentication by sending it to `/v1/login/token`
|
||||
- Server validates the token and issues a new token
|
||||
|
||||
3. **Token Expiration**:
|
||||
### TOTP Authentication Flow
|
||||
|
||||
1. **TOTP Verification** (separate from password authentication):
|
||||
- Client sends username and TOTP code to `/v1/login/totp`
|
||||
- Server validates the TOTP code and returns a token if valid
|
||||
|
||||
### Token Management
|
||||
|
||||
1. **Token Expiration**:
|
||||
- Tokens expire after 24 hours
|
||||
- Clients should request a new token before expiration
|
||||
- Clients should request a new token before expiration
|
||||
|
||||
### Multi-Factor Authentication
|
||||
|
||||
For users with TOTP enabled, a complete multi-factor authentication flow would involve:
|
||||
1. Authenticate with username and password using `/v1/login/password`
|
||||
2. Check the `totp_enabled` flag in the response
|
||||
3. If TOTP is enabled, prompt the user for their TOTP code
|
||||
4. Verify the TOTP code using `/v1/login/totp` to get a second token
|
||||
5. Use either token for subsequent requests (both are valid)
|
||||
|
|
81
docs/api.org
81
docs/api.org
|
@ -91,10 +91,74 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
|
|||
|
||||
*** Database Credentials
|
||||
|
||||
*Endpoint*: =/v1/credentials/database= (Not yet implemented)
|
||||
*Endpoint*: =GET /v1/database/credentials=
|
||||
|
||||
*Description*: Retrieves database credentials for authorized users.
|
||||
|
||||
*Request Parameters*:
|
||||
- =username=: Username of the authenticated user
|
||||
|
||||
*Headers*:
|
||||
- =Authorization=: Bearer token for authentication
|
||||
|
||||
*Response Format* (Success - 200 OK):
|
||||
#+begin_src json
|
||||
{
|
||||
"host": "database_host",
|
||||
"port": 5432,
|
||||
"name": "database_name",
|
||||
"user": "database_user",
|
||||
"password": "database_password"
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Error Responses*:
|
||||
- 400 Bad Request: Invalid request format or missing required parameters
|
||||
- 401 Unauthorized: Invalid or expired token
|
||||
- 403 Forbidden: Insufficient permissions to access database credentials
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
*** TOTP Authentication
|
||||
|
||||
*Endpoint*: =POST /v1/login/totp=
|
||||
|
||||
*Description*: Authenticates a user using TOTP (Time-based One-Time Password) in addition to username and password.
|
||||
|
||||
*Request Format*:
|
||||
#+begin_src json
|
||||
{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"password": "secret_password",
|
||||
"totp": "123456"
|
||||
}
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Required Fields*:
|
||||
- =version=: Must be "v1"
|
||||
- =login.user=: Username
|
||||
- =login.password=: User's password
|
||||
- =login.totp=: 6-digit TOTP code from authenticator app
|
||||
|
||||
*Response Format* (Success - 200 OK):
|
||||
#+begin_src json
|
||||
{
|
||||
"token": "authentication_token",
|
||||
"expires": 1621234567
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Response Fields*:
|
||||
- =token=: Authentication token to be used for subsequent requests
|
||||
- =expires=: Unix timestamp when the token expires
|
||||
|
||||
*Error Responses*:
|
||||
- 400 Bad Request: Invalid request format or missing required fields
|
||||
- 401 Unauthorized: Invalid username, password, or TOTP code
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
** Error Handling
|
||||
|
||||
All error responses follow a standard format:
|
||||
|
@ -118,11 +182,20 @@ Common HTTP status codes:
|
|||
1. *Initial Authentication*:
|
||||
- Client sends username and password to =/v1/login/password=
|
||||
- Server validates credentials and returns a token
|
||||
- If TOTP is enabled for the user, authentication with password alone will fail
|
||||
|
||||
2. *Subsequent Requests*:
|
||||
2. *TOTP Authentication*:
|
||||
- For users with TOTP enabled, client sends username, password, and TOTP code to =/v1/login/totp=
|
||||
- Server validates all credentials and returns a token
|
||||
|
||||
3. *Subsequent Requests*:
|
||||
- Client uses the token for authentication by sending it to =/v1/login/token=
|
||||
- Server validates the token and issues a new token
|
||||
|
||||
3. *Token Expiration*:
|
||||
4. *Token Expiration*:
|
||||
- Tokens expire after 24 hours
|
||||
- Clients should request a new token before expiration
|
||||
- Clients should request a new token before expiration
|
||||
|
||||
5. *Database Credential Access*:
|
||||
- Client sends a GET request to =/v1/database/credentials= with the token in the Authorization header
|
||||
- Server validates the token and returns the database credentials if the user has sufficient permissions
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
# MCIAS Deployment Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides guidance on deploying the Metacircular Identity and Access System (MCIAS) in a production environment. MCIAS is designed to be deployed behind a reverse proxy that handles TLS termination and security headers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux server with systemd
|
||||
- Nginx or another reverse proxy for TLS termination
|
||||
- User and group `mcias` created on the system
|
||||
- Go 1.23 or later for building from source
|
||||
|
||||
## Installation
|
||||
|
||||
1. Build the MCIAS binary:
|
||||
```bash
|
||||
go build -o mcias ./cmd/mcias
|
||||
```
|
||||
|
||||
2. Create the installation directory:
|
||||
```bash
|
||||
sudo mkdir -p /opt/mcias
|
||||
```
|
||||
|
||||
3. Copy the binary and service file:
|
||||
```bash
|
||||
sudo cp mcias /opt/mcias/
|
||||
sudo cp mcias.service /etc/systemd/system/
|
||||
```
|
||||
|
||||
4. Set appropriate permissions:
|
||||
```bash
|
||||
sudo chown -R mcias:mcias /opt/mcias
|
||||
sudo chmod 755 /opt/mcias/mcias
|
||||
```
|
||||
|
||||
5. Initialize the database:
|
||||
```bash
|
||||
sudo -u mcias /opt/mcias/mcias init --db /opt/mcias/mcias.db
|
||||
```
|
||||
|
||||
6. Enable and start the service:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable mcias
|
||||
sudo systemctl start mcias
|
||||
```
|
||||
|
||||
## Reverse Proxy Configuration
|
||||
|
||||
MCIAS is designed to run behind a reverse proxy that handles TLS termination and security headers. Below is an example Nginx configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name mcias.example.com;
|
||||
|
||||
# SSL configuration
|
||||
ssl_certificate /etc/letsencrypt/live/mcias.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/mcias.example.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Content-Security-Policy "default-src 'self';" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Proxy to MCIAS
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name mcias.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **TLS Configuration**: The reverse proxy should use modern TLS protocols (TLSv1.2 and TLSv1.3) and strong cipher suites.
|
||||
|
||||
2. **Security Headers**: The reverse proxy should add security headers to all responses, as shown in the Nginx configuration example.
|
||||
|
||||
3. **Firewall Configuration**: Configure your firewall to only allow connections to the MCIAS server from the reverse proxy.
|
||||
|
||||
4. **Regular Updates**: Keep the MCIAS software, operating system, and reverse proxy up to date with security patches.
|
||||
|
||||
5. **Monitoring**: Set up monitoring for the MCIAS service and review logs regularly for security events.
|
||||
|
||||
## Logging
|
||||
|
||||
MCIAS logs to the systemd journal by default. You can view logs using:
|
||||
|
||||
```bash
|
||||
sudo journalctl -u mcias
|
||||
```
|
||||
|
||||
Security events are logged with the prefix `SECURITY_EVENT:` and are in JSON format for easy parsing.
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
Regularly back up the MCIAS database file (/opt/mcias/mcias.db) to ensure you can recover in case of data loss.
|
||||
|
||||
Example backup script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/var/backups/mcias"
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
mkdir -p $BACKUP_DIR
|
||||
sqlite3 /opt/mcias/mcias.db ".backup $BACKUP_DIR/mcias_$TIMESTAMP.db"
|
||||
```
|
|
@ -31,7 +31,7 @@ go mod download
|
|||
MCIAS uses SQLite for data storage. To initialize the database:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -init -db ./mcias.db
|
||||
go run cmd/mcias/main.go init --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
This command creates a new SQLite database file and initializes it with the schema defined in =schema.sql=.
|
||||
|
@ -43,37 +43,76 @@ This command creates a new SQLite database file and initializes it with the sche
|
|||
To start the MCIAS server with default settings:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -db ./mcias.db
|
||||
go run cmd/mcias/main.go server --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
By default, the server listens on port 8080.
|
||||
|
||||
*** Configuration Options
|
||||
|
||||
MCIAS supports the following command-line options:
|
||||
MCIAS supports the following command-line options for the server:
|
||||
|
||||
- =-db <path>=: Path to the SQLite database file (default: =mcias.db=)
|
||||
- =-addr <address>=: Address to listen on (default: =:8080=)
|
||||
- =-init=: Initialize the database and exit
|
||||
- =--db <path>=: Path to the SQLite database file (default: =mcias.db=)
|
||||
- =--addr <address>=: Address to listen on (default: =:8080=)
|
||||
|
||||
Example with custom port:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -db ./mcias.db -addr :9000
|
||||
go run cmd/mcias/main.go server --db ./mcias.db --addr :9000
|
||||
#+end_src
|
||||
|
||||
** Managing Users and Authentication
|
||||
|
||||
*** Adding a New User
|
||||
|
||||
To add a new user to the system:
|
||||
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go user add --username <username> --password <password>
|
||||
#+end_src
|
||||
|
||||
*** Managing TOTP Authentication
|
||||
|
||||
To enable TOTP for a user:
|
||||
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go totp enable --username <username>
|
||||
#+end_src
|
||||
|
||||
This will generate a TOTP secret for the user and display it. The user should save this secret in their authenticator app.
|
||||
|
||||
To add a TOTP token with QR code generation:
|
||||
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go totp add --username <username> --qr-output <path/to/qrcode.png>
|
||||
#+end_src
|
||||
|
||||
To validate a TOTP code:
|
||||
|
||||
#+begin_src bash
|
||||
go run cmd/mcias/main.go totp validate --username <username> --code <totp_code>
|
||||
#+end_src
|
||||
|
||||
** Building from Source
|
||||
|
||||
To build a binary:
|
||||
To build the server binary:
|
||||
|
||||
#+begin_src bash
|
||||
cd cmd/mcias
|
||||
go build -o mcias
|
||||
#+end_src
|
||||
|
||||
Then run the binary:
|
||||
|
||||
#+begin_src bash
|
||||
./mcias -db ./mcias.db
|
||||
./mcias server --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
To build the client binary:
|
||||
|
||||
#+begin_src bash
|
||||
cd cmd/mcias-client
|
||||
go build -o mcias-client
|
||||
#+end_src
|
||||
|
||||
** Development
|
||||
|
@ -128,6 +167,32 @@ curl -X POST http://localhost:8080/v1/login/token \
|
|||
}'
|
||||
#+end_src
|
||||
|
||||
*** Authentication with TOTP
|
||||
|
||||
To authenticate a user with a password and TOTP code:
|
||||
|
||||
#+begin_src bash
|
||||
curl -X POST http://localhost:8080/v1/login/totp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"password": "password",
|
||||
"totp": "123456"
|
||||
}
|
||||
}'
|
||||
#+end_src
|
||||
|
||||
*** Retrieving Database Credentials
|
||||
|
||||
To retrieve database credentials:
|
||||
|
||||
#+begin_src bash
|
||||
curl -X GET "http://localhost:8080/v1/database/credentials?username=username" \
|
||||
-H "Authorization: Bearer your_token"
|
||||
#+end_src
|
||||
|
||||
** Troubleshooting
|
||||
|
||||
*** Common Issues
|
||||
|
@ -170,4 +235,4 @@ go run main.go -db ./mcias.db > mcias.log 2>&1
|
|||
3. *User Management*:
|
||||
- Implement strong password policies
|
||||
- Regularly rotate tokens
|
||||
- Monitor for suspicious authentication attempts
|
||||
- Monitor for suspicious authentication attempts
|
||||
|
|
|
@ -11,10 +11,10 @@ The system currently provides:
|
|||
1. User password authentication
|
||||
2. User token authentication
|
||||
3. Database credential authentication
|
||||
4. TOTP (Time-based One-Time Password) authentication
|
||||
|
||||
Future planned features include:
|
||||
1. TOTP (Time-based One-Time Password) authentication
|
||||
2. Policy management for fine-grained access control
|
||||
1. Policy management for fine-grained access control
|
||||
|
||||
** System Architecture
|
||||
|
||||
|
@ -57,7 +57,8 @@ CREATE TABLE users (
|
|||
created integer,
|
||||
user text not null,
|
||||
password blob not null,
|
||||
salt blob not null
|
||||
salt blob not null,
|
||||
totp_secret text
|
||||
);
|
||||
#+end_src
|
||||
|
||||
|
@ -108,6 +109,24 @@ CREATE TABLE user_roles (
|
|||
);
|
||||
#+end_src
|
||||
|
||||
*** Permissions Tables
|
||||
#+begin_src sql
|
||||
CREATE TABLE permissions (
|
||||
id TEXT PRIMARY KEY,
|
||||
resource TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE role_permissions (
|
||||
id TEXT PRIMARY KEY,
|
||||
rid TEXT NOT NULL,
|
||||
pid TEXT NOT NULL,
|
||||
FOREIGN KEY(rid) REFERENCES roles(id),
|
||||
FOREIGN KEY(pid) REFERENCES permissions(id)
|
||||
);
|
||||
#+end_src
|
||||
|
||||
** Security Considerations
|
||||
|
||||
MCIAS implements several security best practices:
|
||||
|
@ -130,4 +149,4 @@ MCIAS implements several security best practices:
|
|||
|
||||
4. *Database Security*
|
||||
- Parameterized queries to prevent SQL injection
|
||||
- Foreign key constraints to maintain data integrity
|
||||
- Foreign key constraints to maintain data integrity
|
||||
|
|
4
go.mod
4
go.mod
|
@ -5,12 +5,15 @@ go 1.23.8
|
|||
require github.com/gokyle/twofactor v1.0.1
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/golang-migrate/migrate/v4 v4.18.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
|
@ -27,6 +30,7 @@ require (
|
|||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
|
9
go.sum
9
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -16,6 +18,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
|
|||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
|
@ -40,6 +46,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
|||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
|
@ -55,6 +62,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
|||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
[Unit]
|
||||
Description=Metacircular Identity and Access System
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=mcias
|
||||
Group=mcias
|
||||
WorkingDirectory=/opt/mcias
|
||||
ExecStart=/opt/mcias/mcias server --db /opt/mcias/mcias.db
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=mcias
|
||||
|
||||
# Security settings
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
NoNewPrivileges=true
|
||||
ReadWritePaths=/opt/mcias
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Loading…
Reference in New Issue