security: Add proper authentication, RBAC, and tenant isolation

- Add password hashing with bcrypt
- Add AuthService with proper login
- Add password strength validation
- Add RBAC middleware (AdminOnly, ManagerOrAdmin)
- Add tenant isolation middleware
- Update CRM handler with tenant filtering
- Add JWT fallback for development mode
- Add user context helpers
- Build successful
This commit is contained in:
Bernt
2026-08-10 12:52:48 +00:00
parent 8921fd1467
commit 78b57273e2
141 changed files with 29192 additions and 180 deletions
+10
View File
@@ -38,6 +38,16 @@ func (c Claims) Valid() error {
return nil
}
// HasRole kontrollerar om användaren har en specifik roll
func (c Claims) HasRole(role string) bool {
for _, r := range c.Roles {
if r == role {
return true
}
}
return false
}
// ── Context Key ────────────────────────────────────────────────────────────
type contextKey int
+62
View File
@@ -0,0 +1,62 @@
package auth
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
// HashPassword skapar en bcrypt hash av lösenordet
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(bytes), nil
}
// VerifyPassword kontrollerar att lösenordet matchar hashen
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// ValidatePasswordStrength kontrollerar lösenordsstyrka
func ValidatePasswordStrength(password string) error {
if len(password) < 8 {
return fmt.Errorf("password must be at least 8 characters")
}
hasUpper := false
hasLower := false
hasNumber := false
hasSpecial := false
for _, c := range password {
switch {
case c >= 'A' && c <= 'Z':
hasUpper = true
case c >= 'a' && c <= 'z':
hasLower = true
case c >= '0' && c <= '9':
hasNumber = true
case c >= '!' && c <= '/' || c >= ':' && c <= '@' || c >= '[' && c <= '`' || c >= '{' && c <= '~':
hasSpecial = true
}
}
if !hasUpper {
return fmt.Errorf("password must contain at least one uppercase letter")
}
if !hasLower {
return fmt.Errorf("password must contain at least one lowercase letter")
}
if !hasNumber {
return fmt.Errorf("password must contain at least one number")
}
if !hasSpecial {
return fmt.Errorf("password must contain at least one special character")
}
return nil
}
+194
View File
@@ -0,0 +1,194 @@
package auth
import (
"context"
"database/sql"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
)
// AuthService hanterar autentisering och auktorisation
type AuthService struct {
db *sql.DB
jwtSecret string
issuer string
audience string
}
// NewAuthService skapar en ny auth service
func NewAuthService(db *sql.DB, jwtSecret, issuer, audience string) *AuthService {
return &AuthService{
db: db,
jwtSecret: jwtSecret,
issuer: issuer,
audience: audience,
}
}
// User representerar en autentiserad användare
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Role string `json:"role"`
TenantID string `json:"tenant_id"`
Roles []string `json:"roles"`
}
// LoginRequest innehåller login-uppgifter
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
// LoginResponse innehåller token och användardata
type LoginResponse struct {
Token string `json:"token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User User `json:"user"`
}
// Login autentiserar en användare och returnerar JWT token
func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
// Hämta användare från databas
var user User
var passwordHash string
err := s.db.QueryRowContext(ctx, `
SELECT id, email, name, role, tenant_id, password_hash
FROM boc_users
WHERE email = $1 AND status = 'active'
`, req.Email).Scan(&user.ID, &user.Email, &user.Name, &user.Role, &user.TenantID, &passwordHash)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("invalid email or password")
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
// Verifiera lösenord
if !VerifyPassword(req.Password, passwordHash) {
return nil, fmt.Errorf("invalid email or password")
}
// Generera JWT token
token, err := s.GenerateToken(user)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}
// Uppdatera last_login
_, _ = s.db.ExecContext(ctx, `
UPDATE boc_users SET last_login = NOW() WHERE id = $1
`, user.ID)
return &LoginResponse{
Token: token,
TokenType: "Bearer",
ExpiresIn: 3600, // 1 timme
User: user,
}, nil
}
// GenerateToken skapar en JWT token för en användare
func (s *AuthService) GenerateToken(user User) (string, error) {
now := time.Now()
claims := jwt.MapClaims{
"sub": user.ID,
"email": user.Email,
"name": user.Name,
"role": user.Role,
"tenant_id": user.TenantID,
"iss": s.issuer,
"aud": s.audience,
"iat": now.Unix(),
"exp": now.Add(1 * time.Hour).Unix(), // 1 timme
"jti": generateJTI(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.jwtSecret))
}
// ValidateToken validerar en JWT token och returnerar användardata
func (s *AuthService) ValidateToken(tokenString string) (*User, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.jwtSecret), nil
})
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
if !token.Valid {
return nil, fmt.Errorf("token is invalid")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims")
}
user := &User{
ID: getStringClaim(claims, "sub"),
Email: getStringClaim(claims, "email"),
Name: getStringClaim(claims, "name"),
Role: getStringClaim(claims, "role"),
TenantID: getStringClaim(claims, "tenant_id"),
}
return user, nil
}
// HasRole kontrollerar om användaren har en specifik roll
func (s *AuthService) HasRole(user *User, role string) bool {
if user.Role == role {
return true
}
for _, r := range user.Roles {
if r == role {
return true
}
}
return false
}
// RequireRole middleware kontrollerar att användaren har en specifik roll
func (s *AuthService) RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := FromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
user := &User{
ID: claims.Sub,
Email: claims.Email,
Role: "",
Roles: claims.Roles,
}
if !s.HasRole(user, role) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
func generateJTI() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}