From 82081b960997d85ebd6492b344b5d1d62d6888f4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sun, 25 Jan 2026 19:36:56 +0000 Subject: [PATCH] Add API signin URL That was we can have much more specific failure modes for API clients --- api/routes.go | 1 + api/signin.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ auth/auth.go | 17 ++++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 api/signin.go diff --git a/api/routes.go b/api/routes.go index 80726f40..8db2879b 100644 --- a/api/routes.go +++ b/api/routes.go @@ -22,6 +22,7 @@ func AddRoutes(r chi.Router) { // Unauthenticated endpoints r.Get("/district", apiGetDistrict) r.Get("/district/{slug}/logo", apiGetDistrictLogo) + r.Post("/signin", postSignin) r.Post("/twilio/message", twilioMessagePost) r.Post("/twilio/status", twilioStatusPost) r.Post("/twilio/text", twilioTextPost) diff --git a/api/signin.go b/api/signin.go new file mode 100644 index 00000000..d182c897 --- /dev/null +++ b/api/signin.go @@ -0,0 +1,45 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/auth" + "github.com/go-chi/render" + "github.com/rs/zerolog/log" +) + +func postSignin(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + render.Render(w, r, errRender(fmt.Errorf("Failed to parse POST form: %w", err))) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + if password == "" || username == "" { + w.Header().Set("WWW-Authenticate-Error", "no-credentials") + http.Error(w, "invalid-credentials", http.StatusUnauthorized) + return + } + log.Info().Str("username", username).Msg("API Signin") + _, err := auth.SigninUser(r, username, password) + if err != nil { + if errors.Is(err, auth.InvalidCredentials{}) { + w.Header().Set("WWW-Authenticate-Error", "invalid-credentials") + http.Error(w, "invalid-credentials", http.StatusUnauthorized) + return + } + if errors.Is(err, auth.InvalidUsername{}) { + w.Header().Set("WWW-Authenticate-Error", "invalid-credentials") + http.Error(w, "invalid-credentials", http.StatusUnauthorized) + return + } + http.Error(w, "signin-server-error", http.StatusInternalServerError) + return + } + + http.Error(w, "", http.StatusAccepted) +} diff --git a/auth/auth.go b/auth/auth.go index 7548ecca..d5245414 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" @@ -188,6 +189,18 @@ func hashPassword(password string) (string, error) { return string(bytes), err } +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 +} + func validatePassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil @@ -198,17 +211,18 @@ func validateUser(ctx context.Context, username string, password string) (*model if err != nil { return nil, fmt.Errorf("Failed to hash password: %w", err) } - log.Info().Str("username", username).Str("password", password).Str("hash", passwordHash).Msg("Validating user") result, err := sql.UserByUsername(username).All(ctx, db.PGInstance.BobDB) if err != nil { return nil, fmt.Errorf("Failed to query for user: %w", err) } switch len(result) { case 0: + log.Info().Str("username", username).Str("password", redact(password)).Msg("Invalid username") return nil, InvalidUsername{} case 1: row := result[0] if !validatePassword(password, row.PasswordHash) { + log.Info().Str("username", username).Str("password", redact(password)).Str("hash", passwordHash).Msg("Invalid password for user") return nil, InvalidCredentials{} } user := models.User{ @@ -223,6 +237,7 @@ func validateUser(ctx context.Context, username string, password string) (*model OrganizationID: row.OrganizationID, Username: row.Username, } + log.Info().Str("username", username).Msg("Validated user") return &user, nil default: return nil, errors.New("More than one matching row, this should be impossible.")