2025-12-16 16:37:53 +00:00
|
|
|
package auth
|
2025-11-05 17:15:33 +00:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
2026-01-25 19:36:56 +00:00
|
|
|
"strings"
|
2025-11-05 17:15:33 +00:00
|
|
|
|
2026-03-12 23:49:16 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
2025-11-24 19:49:19 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2025-11-05 17:15:33 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-21 19:37:26 +00:00
|
|
|
type InactiveUser struct{}
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2026-04-21 19:37:26 +00:00
|
|
|
func (e InactiveUser) Error() string { return "That user is not active" }
|
2025-11-05 17:15:33 +00:00
|
|
|
|
|
|
|
|
type InvalidCredentials struct{}
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-05 17:15:33 +00:00
|
|
|
func (e InvalidCredentials) Error() string { return "No username with that password exists" }
|
|
|
|
|
|
|
|
|
|
type InvalidUsername struct{}
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-05 17:15:33 +00:00
|
|
|
func (e InvalidUsername) Error() string { return "That username doesn't exist" }
|
|
|
|
|
|
2026-04-21 19:37:26 +00:00
|
|
|
type NoCredentialsError struct{}
|
|
|
|
|
|
|
|
|
|
func (e NoCredentialsError) Error() string { return "No credentials were present in the request" }
|
|
|
|
|
|
2026-03-12 23:49:16 +00:00
|
|
|
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, platform.User)
|
2025-11-13 15:50:10 +00:00
|
|
|
type EnsureAuth struct {
|
|
|
|
|
handler AuthenticatedHandler
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 17:39:16 +00:00
|
|
|
func AddUserSession(ctx context.Context, user *platform.User) {
|
|
|
|
|
id_str := strconv.Itoa(int(user.ID))
|
|
|
|
|
sessionManager.Put(ctx, "user_id", id_str)
|
|
|
|
|
sessionManager.Put(ctx, "username", user.Username)
|
|
|
|
|
log.Debug().Str("id", id_str).Str("username", user.Username).Msg("added user session")
|
|
|
|
|
}
|
2026-04-02 21:31:31 +00:00
|
|
|
func ImpersonateEnd(ctx context.Context) {
|
|
|
|
|
sessionManager.Put(ctx, "impersonated_user_id", "")
|
|
|
|
|
}
|
2026-04-02 17:39:16 +00:00
|
|
|
func ImpersonateUser(ctx context.Context, target_user_id int) {
|
|
|
|
|
target_user_id_str := strconv.Itoa(int(target_user_id))
|
|
|
|
|
sessionManager.Put(ctx, "impersonated_user_id", target_user_id_str)
|
|
|
|
|
}
|
|
|
|
|
func ImpersonatedUser(ctx context.Context) *int32 {
|
|
|
|
|
i_str := sessionManager.GetString(ctx, "impersonated_user_id")
|
|
|
|
|
if i_str == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
i, err := strconv.Atoi(i_str)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Str("impersonated_user_id", i_str).Msg("failed to parse impersonated_user_id")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
result := int32(i)
|
|
|
|
|
return &result
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|
2026-04-02 21:31:31 +00:00
|
|
|
func ImpersonatorID(ctx context.Context) *int32 {
|
|
|
|
|
user_id_str := sessionManager.GetString(ctx, "user_id")
|
|
|
|
|
user_id, err := strconv.Atoi(user_id_str)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error().Err(err).Str("user_id", user_id_str).Msg("failed to parse user_id")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
result := int32(user_id)
|
|
|
|
|
return &result
|
2025-12-16 16:37:53 +00:00
|
|
|
|
2026-04-02 21:31:31 +00:00
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
user_id_str := sessionManager.GetString(ctx, "user_id")
|
2026-04-02 17:39:16 +00:00
|
|
|
impersonated_user_id_str := sessionManager.GetString(ctx, "impersonated_user_id")
|
|
|
|
|
if impersonated_user_id_str != "" {
|
|
|
|
|
user_id_str = impersonated_user_id_str
|
|
|
|
|
}
|
2025-12-16 16:37:53 +00:00
|
|
|
if user_id_str != "" {
|
|
|
|
|
user_id, err := strconv.Atoi(user_id_str)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("Failed to convert user_id to int: %w", err)
|
|
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
username := sessionManager.GetString(ctx, "username")
|
2025-12-16 16:37:53 +00:00
|
|
|
if user_id > 0 && username != "" {
|
2026-04-21 19:37:26 +00:00
|
|
|
user, err := platform.UserByID(ctx, int32(user_id))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("user by ID: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !user.IsActive {
|
|
|
|
|
return nil, fmt.Errorf("user is inactive")
|
|
|
|
|
}
|
|
|
|
|
return user, nil
|
2025-12-16 16:37:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If we can't get the user from the session try to get from auth headers
|
|
|
|
|
username, password, ok := r.BasicAuth()
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, &NoCredentialsError{}
|
|
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
user, err := validateUser(ctx, username, password)
|
2025-12-16 16:37:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-02 17:39:16 +00:00
|
|
|
AddUserSession(ctx, user)
|
2025-12-16 16:37:53 +00:00
|
|
|
return user, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 15:50:10 +00:00
|
|
|
func NewEnsureAuth(handlerToWrap AuthenticatedHandler) *EnsureAuth {
|
|
|
|
|
return &EnsureAuth{handlerToWrap}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// If this is an API request respond with a more machine-readable error state
|
2026-04-16 16:01:45 +00:00
|
|
|
accept := r.Header.Get("Accept")
|
|
|
|
|
/*
|
|
|
|
|
offers := []string{"application/json", "text/html"}
|
2025-11-13 15:50:10 +00:00
|
|
|
|
2026-04-16 16:01:45 +00:00
|
|
|
content_type := NegotiateContent(accept, offers)
|
|
|
|
|
*/
|
2025-12-16 16:37:53 +00:00
|
|
|
user, err := GetAuthenticatedUser(r)
|
2025-11-24 18:09:06 +00:00
|
|
|
if err != nil || user == nil {
|
2025-11-13 15:50:10 +00:00
|
|
|
var msg []byte
|
2026-04-16 16:01:45 +00:00
|
|
|
// Don't send authentication headers for browsers because it forces the authentication popup
|
|
|
|
|
requested_with := r.Header.Get("X-Requested-With")
|
|
|
|
|
//log.Debug().Str("x-requested-with", requested_with).Send()
|
2026-05-01 20:49:37 +00:00
|
|
|
if !strings.HasPrefix(requested_with, "nidus-web") && accept != "text/event-stream" {
|
2026-04-16 16:01:45 +00:00
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Nidus Sync"`)
|
|
|
|
|
// Separate return codes for different authentication failures
|
|
|
|
|
if _, ok := err.(*NoCredentialsError); ok {
|
|
|
|
|
log.Info().Msg("No credentials present and no session")
|
|
|
|
|
w.Header().Set("WWW-Authenticate-Error", "no-credentials")
|
|
|
|
|
msg = []byte("Please provide credentials.\n")
|
|
|
|
|
} else if _, ok := err.(*platform.NoUserError); ok {
|
|
|
|
|
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
|
|
|
|
|
msg = []byte("Invalid credentials provided.\n")
|
|
|
|
|
} else if _, ok := err.(*InvalidCredentials); ok {
|
|
|
|
|
w.Header().Set("WWW-Authenticate-Error", "invalid-credentials")
|
|
|
|
|
msg = []byte("Invalid credentials provided.\n")
|
|
|
|
|
}
|
2025-11-13 15:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(401)
|
|
|
|
|
w.Write(msg)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
ea.handler(w, r, *user)
|
2025-11-13 15:50:10 +00:00
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
func SigninUser(r *http.Request, username string, password string) (*platform.User, error) {
|
2025-11-05 17:15:33 +00:00
|
|
|
user, err := validateUser(r.Context(), username, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if user == nil {
|
|
|
|
|
return nil, errors.New("No matching user")
|
|
|
|
|
}
|
2026-04-02 17:39:16 +00:00
|
|
|
AddUserSession(r.Context(), user)
|
2025-11-05 17:15:33 +00:00
|
|
|
return user, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 23:49:16 +00:00
|
|
|
func SignoutUser(r *http.Request, user platform.User) {
|
2026-01-15 00:20:19 +00:00
|
|
|
sessionManager.Put(r.Context(), "user_id", "")
|
|
|
|
|
sessionManager.Put(r.Context(), "username", "")
|
2026-04-16 19:50:23 +00:00
|
|
|
sessionManager.Destroy(r.Context())
|
2026-03-13 17:33:39 +00:00
|
|
|
log.Info().Str("username", user.Username).Int("user_id", (user.ID)).Msg("Ended user session")
|
2026-01-15 00:20:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 23:49:16 +00:00
|
|
|
func SignupUser(ctx context.Context, username string, name string, password string) (*platform.User, error) {
|
|
|
|
|
password_hash, err := HashPassword(password)
|
2025-11-05 17:15:33 +00:00
|
|
|
if err != nil {
|
2026-01-06 15:06:16 +00:00
|
|
|
return nil, fmt.Errorf("Cannot signup user, failed to create hashed password: %w", err)
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
u, err := platform.CreateUser(ctx, username, name, password_hash)
|
2025-11-05 17:15:33 +00:00
|
|
|
if err != nil {
|
2026-03-12 23:49:16 +00:00
|
|
|
return nil, fmt.Errorf("create user: %s", err)
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 18:42:30 +00:00
|
|
|
func HashPassword(password string) (string, error) {
|
2025-12-16 16:37:53 +00:00
|
|
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
|
|
|
|
return string(bytes), err
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 19:36:56 +00:00
|
|
|
func redact(s string) string {
|
|
|
|
|
if len(s) <= 4 {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
first_two := s[:2]
|
|
|
|
|
last_two := s[len(s)-2:]
|
|
|
|
|
middle_length := len(s) - 4
|
|
|
|
|
|
|
|
|
|
return first_two + strings.Repeat("*", middle_length) + last_two
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 17:15:33 +00:00
|
|
|
func validatePassword(password, hash string) bool {
|
|
|
|
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
2026-03-13 17:33:39 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Debug().Err(err).Str("password", password).Str("hash", hash).Msg("!validate password")
|
|
|
|
|
}
|
2025-11-05 17:15:33 +00:00
|
|
|
return err == nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 23:49:16 +00:00
|
|
|
func validateUser(ctx context.Context, username string, password string) (*platform.User, error) {
|
2026-01-26 18:42:30 +00:00
|
|
|
passwordHash, err := HashPassword(password)
|
2025-11-05 17:15:33 +00:00
|
|
|
if err != nil {
|
2025-11-13 20:53:20 +00:00
|
|
|
return nil, fmt.Errorf("Failed to hash password: %w", err)
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
user, err := platform.UserByUsername(ctx, username)
|
2025-11-05 17:15:33 +00:00
|
|
|
if err != nil {
|
2025-11-13 20:53:20 +00:00
|
|
|
return nil, fmt.Errorf("Failed to query for user: %w", err)
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
if user == nil {
|
2026-01-25 19:36:56 +00:00
|
|
|
log.Info().Str("username", username).Str("password", redact(password)).Msg("Invalid username")
|
2025-11-05 17:15:33 +00:00
|
|
|
return nil, InvalidUsername{}
|
|
|
|
|
}
|
2026-04-21 19:37:26 +00:00
|
|
|
if !user.IsActive {
|
|
|
|
|
return nil, InactiveUser{}
|
|
|
|
|
}
|
2026-03-12 23:49:16 +00:00
|
|
|
if !validatePassword(password, user.PasswordHash) {
|
|
|
|
|
log.Info().Str("username", username).Str("password", redact(password)).Str("hash", passwordHash).Msg("Invalid password for user")
|
|
|
|
|
return nil, InvalidCredentials{}
|
|
|
|
|
}
|
|
|
|
|
return user, nil
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|