fix(auth): wiring Auth/Sessions, нормализация email, GetUserByID для /me, 409 на дубль, timing-guard логина
This commit is contained in:
@@ -2,17 +2,43 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/vasyakrg/dns-autoresolver/internal/auth"
|
||||
"github.com/vasyakrg/dns-autoresolver/internal/store"
|
||||
)
|
||||
|
||||
const sessionCookieName = "session"
|
||||
|
||||
// dummyPasswordHash is a valid-format argon2 hash with no real matching
|
||||
// password. handleLogin runs VerifyPassword against it whenever the email
|
||||
// lookup fails, so a login attempt for an unregistered email takes the same
|
||||
// wall-clock time as one for a registered email with a wrong password —
|
||||
// otherwise the timing difference would let an attacker enumerate which
|
||||
// emails are registered.
|
||||
var dummyPasswordHash string
|
||||
|
||||
func init() {
|
||||
h, err := auth.HashPassword("dns-autoresolver-timing-guard-dummy")
|
||||
if err != nil {
|
||||
panic("api: failed to initialize dummy password hash: " + err.Error())
|
||||
}
|
||||
dummyPasswordHash = h
|
||||
}
|
||||
|
||||
// normalizeEmail trims surrounding whitespace and lowercases the email so
|
||||
// storage and lookup are always consistent regardless of how the client
|
||||
// cased or padded the input.
|
||||
func normalizeEmail(email string) string {
|
||||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
// ctxKeyUserID is a private context key carrying the authenticated user's ID.
|
||||
// Task 4's RequireAuth middleware sets it after validating the session
|
||||
// cookie; handleMe reads it back.
|
||||
@@ -45,7 +71,8 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if !decodeBody(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.Email == "" || req.Password == "" {
|
||||
email := normalizeEmail(req.Email)
|
||||
if email == "" || req.Password == "" {
|
||||
writeErr(w, http.StatusBadRequest, "email and password are required")
|
||||
return
|
||||
}
|
||||
@@ -57,8 +84,12 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
u, p, err := a.Auth.RegisterUser(r.Context(), req.Email, hash)
|
||||
u, p, err := a.Auth.RegisterUser(r.Context(), email, hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrEmailTaken) {
|
||||
writeErr(w, http.StatusConflict, "email already registered")
|
||||
return
|
||||
}
|
||||
log.Printf("api: register user failed: %v", err)
|
||||
writeErr(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
@@ -87,9 +118,14 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if !decodeBody(w, r, &req) {
|
||||
return
|
||||
}
|
||||
email := normalizeEmail(req.Email)
|
||||
|
||||
u, err := a.Auth.GetUserByEmail(r.Context(), req.Email)
|
||||
u, err := a.Auth.GetUserByEmail(r.Context(), email)
|
||||
if err != nil {
|
||||
// No such user: still spend the argon2 verification cost against a
|
||||
// fixed dummy hash (see dummyPasswordHash) so this path isn't
|
||||
// distinguishable by timing from a wrong-password rejection below.
|
||||
_, _ = auth.VerifyPassword(dummyPasswordHash, req.Password)
|
||||
invalidCredentials(w)
|
||||
return
|
||||
}
|
||||
@@ -131,8 +167,7 @@ func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
// handleMe returns the authenticated caller's identity + default project.
|
||||
// The user ID comes from the request context, set by Task 4's RequireAuth
|
||||
// middleware after validating the session cookie (tests set it directly via
|
||||
// context.WithValue in the interim). AuthStore has no GetUserByID — the
|
||||
// email field is intentionally left empty here; see task-3-report.md.
|
||||
// context.WithValue in the interim).
|
||||
func (a *API) handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := userIDFromContext(r.Context())
|
||||
if !ok {
|
||||
@@ -140,6 +175,13 @@ func (a *API) handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
u, err := a.Auth.GetUserByID(r.Context(), userID)
|
||||
if err != nil {
|
||||
log.Printf("api: get user by id failed: %v", err)
|
||||
writeErr(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
p, err := a.Auth.GetUserProject(r.Context(), userID)
|
||||
if err != nil {
|
||||
log.Printf("api: get user project failed: %v", err)
|
||||
@@ -147,8 +189,5 @@ func (a *API) handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, authResponse{
|
||||
User: userResponse{ID: userID.String()},
|
||||
Project: projectResponse{ID: p.ID.String(), Name: p.Name},
|
||||
})
|
||||
writeJSON(w, http.StatusOK, toAuthResponse(u, p))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user