// Package auth implements the email-code authentication flow and the // active-session bookkeeping consumed by gateway. The package is // described end-to-end in `backend/PLAN.md` §5.1. // // External dependencies that have not landed yet (mail in 5.6, push // session_invalidation in 6) are injected through the LoginCodeMailer // and SessionInvalidator interfaces; auth ships no-op implementations // that satisfy the contract until the real services arrive. package auth import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "time" "galaxy/backend/internal/config" "go.uber.org/zap" ) // Deps aggregates every collaborator the Service depends on. // Constructing the Service through Deps (rather than positional args) // keeps wiring patches small when new dependencies are added. // // Cache and Store must be non-nil: GetSession reads through Cache, // SendEmailCode and ConfirmEmailCode mutate Store. User, Geo, Mail and // Push are tested-in-isolation interfaces; production wires the real // `*user.Service`, `*geo.Service`, mail, and push implementations. type Deps struct { Store *Store Cache *Cache User UserEnsurer Geo GeoService Mail LoginCodeMailer Push SessionInvalidator Config config.AuthConfig // Now overrides time.Now for deterministic tests. A nil Now defaults // to time.Now in NewService. Now func() time.Time // Logger is named under "auth" by NewService. Nil falls back to // zap.NewNop. Logger *zap.Logger } // Service is the auth-domain entry point. type Service struct { deps Deps // emailHashKey keys the HMAC used to derive `email_hash` log fields. // A per-boot random key keeps email PII out of structured logs while // still letting operators correlate log entries within a single // process lifetime. emailHashKey []byte } // NewService constructs a Service from deps. A nil Now defaults to // time.Now; a nil Logger defaults to zap.NewNop. The other dependencies // must be supplied — calling Service methods with nil Cache/Store/User/ // Geo/Mail/Push will panic at first use, matching how main.go signals // missing wiring. func NewService(deps Deps) *Service { if deps.Now == nil { deps.Now = time.Now } if deps.Logger == nil { deps.Logger = zap.NewNop() } deps.Logger = deps.Logger.Named("auth") key := make([]byte, 32) if _, err := rand.Read(key); err != nil { // rand.Read should not fail in practice; if it does, fall back // to a deterministic key. Email hashing is a log-scoping aid, // not a security primitive, so a constant key is acceptable. copy(key, []byte("galaxy-backend-auth-fallback-key")) } return &Service{deps: deps, emailHashKey: key} } // hashEmail returns a stable, hex-encoded HMAC-SHA256 prefix of email // suitable for use in structured logs. The key is per-process so the // same email maps to the same hash across log lines emitted by this // process, but never across process restarts. The truncation gives // operators enough collision-resistance for ad-hoc grep without keeping // an offline key store. func (s *Service) hashEmail(email string) string { mac := hmac.New(sha256.New, s.emailHashKey) _, _ = mac.Write([]byte(email)) full := mac.Sum(nil) return hex.EncodeToString(full[:8]) }