ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/missionmatch / internal/modules/auth/service.go

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

Source identity

FieldRecorded value
Source typeGitHub source
Original recordJanady13/missionmatch / internal/modules/auth/service.go
DateNot specified in source
RepositoryJanady13/missionmatch
Revision39df94f92b2a33f852481fe1954dabed841524bb
Branchw0-aedt-compliance
Path or archive memberinternal/modules/auth/service.go
Original SHA-25667131366f5ceee183807b2955be713744087eacb6fa59b91cf8fde738c984497
Git blob SHA22c21bad6c7836e2b7568c5d26c0d0c34adc641c
Reading-copy SHA-25675fa31d5eccaf4c821ff727b974771e0d7e7d22e774b15be0999e5ff47ea98ed

Original source ↗

Recovered source transcript

Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.

package auth import ( "context" "errors" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/missionworkforce/missionmatch/internal/platform/audit" "github.com/missionworkforce/missionmatch/internal/platform/database/dbgen" "github.com/missionworkforce/missionmatch/internal/platform/security" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // Sentinel errors. Invalid credential and invalid session errors are deliberately generic so neither // the response nor its timing reveals whether an account exists. var ( ErrNoDatabase = errors.New("auth: no database configured") ErrInvalidCredentials = errors.New("auth: invalid credentials") ErrAccountLocked = errors.New("auth: account temporarily locked") ErrInvalidSession = errors.New("auth: invalid or expired session") ErrInvalidState = errors.New("auth: session is not in the required state") ErrNotFound = errors.New("auth: not found") ErrForbidden = errors.New("auth: not permitted") ErrBadRequest = errors.New("auth: bad request") ) const ( maxFailedLogins = 10 // failed password attempts per email and ip before a soft lock maxMFAFailures = 5 // failed MFA attempts before the pending session is revoked failWindowSeconds = 900 // 15 minute brute force window recoveryCodeCount = 10 ) // reason categories for login_attempts and audit, never the raw email or code. const ( reasonNoUser = "no_user" reasonBadPassword = "[credential removed]" reasonLocked = "locked" reasonMFAFailed = "mfa_failed" ) // Service implements the authentication and accounts flows. It calls the SECURITY DEFINER functions // in migration 0009 over the app pool; those functions are the only path to the secret bearing // tables, which the app role cannot read directly. type Service struct { deps Deps dummyHash string dummyPepper int } // NewService builds the service and precomputes a dummy password hash so the unknown user login path // burns the same PBKDF2 cost as a real verify (no timing oracle). func NewService(d Deps) (*Service, error) { if d.Hasher == nil || d.Tagger == nil { return nil, errors.New("auth: hasher and tagger are required") } dummy, version, err := d.Hasher.Hash("missionmatch equal cost dummy secret") if err != nil { return nil, fmt.Errorf("auth: precompute dummy hash: %w", err) } return &Service{deps: d, dummyHash: dummy, dummyPepper: version}, nil } func (s *Service) db() (dbgen.DBTX, error) { if s.deps.DB == nil || s.deps.DB.Pool == nil { return nil, ErrNoDatabase } return s.deps.DB.Pool, nil } // --------------------------------------------------------------------------- // Login: password verify then an auth_pending session. // --------------------------------------------------------------------------- type loginRow struct { userID uuid.UUID orgID uuid.UUID role string userStatus string passwordHash string pepperVer int mfaEnrolled bool mfaStatus *string secretCipher []byte secretNonce []byte keyVersion *int lastUsedStep *int64 } // Login verifies a password and issues an auth_pending session. The handler sets the returned token // as the session cookie and routes the SPA to enroll or verify based on MFAEnrolled. func (s *Service) Login(ctx context.Context, in LoginInput, ip, ua string) (LoginResult, error) { db, err := s.db() if err != nil { return LoginResult{}, err } locked, err := s.tooManyFailures(ctx, db, in.Email, ip) if err != nil { return LoginResult{}, err } if locked { s.recordAttempt(ctx, db, in.Email, nil, ip, false, reasonLocked) if aerr := s.deps.Audit.System(ctx, audit.Event{Action: "auth.account.locked", IPAddress: ip, UserAgent: ua, EntityType: "email", NewValues: reasonJSON(reasonLocked)}); aerr != nil { return LoginResult{}, aerr } return LoginResult{}, ErrAccountLocked } row, found, err := s.lookupLogin(ctx, db, in.Email) if err != nil { return LoginResult{}, err } if !found { // Equal cost dummy verify and identical attempt accounting so existence is not revealed. _, _, _ = s.deps.Hasher.Verify(in.Password, s.dummyHash, s.dummyPepper) s.recordAttempt(ctx, db, in.Email, nil, ip, false, reasonNoUser) if aerr := s.failureAudit(ctx, nil, ip, ua, reasonNoUser); aerr != nil { return LoginResult{}, aerr } return LoginResult{}, ErrInvalidCredentials } ok, needsRehash, err := s.deps.Hasher.Verify(in.Password, row.passwordHash, row.pepperVer) if err != nil { return LoginResult{}, err } if !ok { s.recordAttempt(ctx, db, in.Email, &row.orgID, ip, false, reasonBadPassword) actor := row.userID if aerr := s.failureAudit(ctx, &actor, ip, ua, reasonBadPassword); aerr != nil { return LoginResult{}, aerr } return LoginResult{}, ErrInvalidCredentials } if needsRehash { if rehash, ver, herr := s.deps.Hasher.Hash(in.Password); herr == nil { _, _ = db.Exec(ctx, `SELECT auth_set_credential($1,$2,$3,$4,$5)`, row.userID, row.orgID, in.Email, rehash, ver) } } token, tokenHash, err := security.NewSessionToken() if err != nil { return LoginResult{}, err } if _, err := db.Exec(ctx, `SELECT auth_create_session($1,$2,$3,'auth_pending',false,$4,$4,$5,$6)`, tokenHash, row.userID, row.orgID, s.deps.PendingSeconds, ip, ua); err != nil { return LoginResult{}, err } s.recordAttempt(ctx, db, in.Email, &row.orgID, ip, true, "") actor := row.userID if aerr := s.deps.Audit.System(ctx, audit.Event{Action: "auth.login.success", ActorUserID: &actor, IPAddress: ip, UserAgent: ua}); aerr != nil { return LoginResult{}, aerr } return LoginResult{Token: token, MFAEnrolled: row.mfaEnrolled}, nil } // --------------------------------------------------------------------------- // MFA: enroll, activate, verify, recover. All run on the pre MFA auth_pending session. // --------------------------------------------------------------------------- // EnrollMFA generates a TOTP secret for a pending session and returns the provisioning material. func (s *Service) EnrollMFA(ctx context.Context, token string) (EnrollResult, error) { db, err := s.db() if err != nil { return EnrollResult{}, err } v, err := s.validatePending(ctx, db, token) if err != nil { return EnrollResult{}, err } // Refuse to re enroll an already active authenticator from a pre MFA session. Without this an // attacker holding the password could plant their own secret over the victim's. A genuine reset // goes through the authenticated admin ResetMFA path. The database WHERE status = 'pending' guard // in auth_mfa_enroll is the authoritative defense; this is the early, friendly rejection. if active, err := s.mfaIsActive(ctx, db, v.userID); err != nil { return EnrollResult{}, err } else if active { return EnrollResult{}, ErrInvalidState } email, err := s.credentialEmail(ctx, db, v.userID) if err != nil { return EnrollResult{}, err } secret, err := security.GenerateSecret() if err != nil { return EnrollResult{}, err } kek, ok := s.deps.TOTPKEKs[s.deps.TOTPKEKActive] if !ok { return EnrollResult{}, errors.New("auth: active TOTP key not configured") } ct, nonce, err := security.SealSecret(kek, secret) if err != nil { return EnrollResult{}, err } var enrolled bool if err := db.QueryRow(ctx, `SELECT auth_mfa_enroll($1,$2,$3,$4,$5)`, v.userID, v.orgID, ct, nonce, s.deps.TOTPKEKActive).Scan(&enrolled); err != nil { return EnrollResult{}, err } if !enrolled { // The row was already active, so the pending overwrite wrote nothing. Fail closed. return EnrollResult{}, ErrInvalidState } uri := security.ProvisioningURI(secret, email, s.deps.TOTPIssuer) return EnrollResult{OTPAuthURI: uri, Secret: security.EncodeSecret(secret)}, nil } // mfaIsActive reports whether the user already has an active TOTP secret. func (s *Service) mfaIsActive(ctx context.Context, db dbgen.DBTX, userID string) (bool, error) { var ct, nonce []byte var keyVer int var status string var lastStep int64 err := db.QueryRow(ctx, `SELECT secret_ciphertext, secret_nonce, key_version, status, last_used_step FROM auth_mfa_get($1)`, mustUUID(userID)). Scan(&ct, &nonce, &keyVer, &status, &lastStep) if errors.Is(err, pgx.ErrNoRows) { return false, nil } if err != nil { return false, err } return status == "active", nil } // ActivateMFA verifies the first code, activates the secret, upgrades the session to active, and // returns the one time recovery codes. It is the ONLY transition that sets mfa_enrolled and yields an // active, MFA satisfied session. func (s *Service) ActivateMFA(ctx context.Context, token string, in CodeInput, ip, ua string) (ActivateResult, error) { db, err := s.db() if err != nil { return ActivateResult{}, err } v, err := s.validatePending(ctx, db, token) if err != nil { return ActivateResult{}, err } used, err := s.verifyTOTPCode(ctx, db, v.userID, in.Code, "pending") if err != nil { return ActivateResult{}, err } codes, hashes, versions, tags, err := s.buildRecoveryCodes() if err != nil { return ActivateResult{}, err } newToken, newHash, err := security.NewSessionToken() if err != nil { return ActivateResult{}, err } err = s.inTx(ctx, db, func(tx pgx.Tx) error { var advanced int if e := tx.QueryRow(ctx, `SELECT auth_mfa_advance_step($1,$2)`, v.userID, used).Scan(&advanced); e != nil { return e } if advanced == 0 { return ErrInvalidCredentials // the activation code was already consumed concurrently } var activated bool if e := tx.QueryRow(ctx, `SELECT auth_mfa_activate($1)`, v.userID).Scan(&activated); e != nil { return e } if !activated { return ErrInvalidState } if _, e := tx.Exec(ctx, `SELECT auth_recovery_store($1,$2,$3,$4,$5)`, v.userID, v.orgID, hashes, versions, tags); e != nil { return e } return rotate(ctx, tx, s.tokenHash(token), newHash, true, s.deps.IdleSeconds, s.deps.AbsoluteSeconds, ip, ua) }) if err != nil { return ActivateResult{}, err } actor := mustUUID(v.userID) _ = s.deps.Audit.System(ctx, audit.Event{Action: "auth.mfa.activated", ActorUserID: &actor, IPAddress: ip, UserAgent: ua}) return ActivateResult{Token: newToken, RecoveryCodes: codes}, nil } // VerifyTOTP verifies a code for an already enrolled user and upgrades the pending session to active. func (s *Service) VerifyTOTP(ctx context.Context, token string, in CodeInput, ip, ua string) (TokenResult, error) { db, err := s.db() if err != nil { return TokenResult{}, err } v, err := s.validatePending(ctx, db, token) if err != nil { return TokenResult{}, err } used, verr := s.verifyTOTPCode(ctx, db, v.userID, in.Code, "active") if verr != nil { if aerr := s.afterMFAFailure(ctx, db, v, token, ip, ua); aerr != nil { return TokenResult{}, aerr } return TokenResult{}, ErrInvalidCredentials } newToken, err := s.advanceAndRotate(ctx, db, v, token, used, ip, ua, "auth.mfa.verify.success") if err != nil { return TokenResult{}, err } return TokenResult{Token: newToken}, nil } // RecoverMFA consumes a single use recovery code to upgrade the pending session to active. func (s *Service) RecoverMFA(ctx context.Context, token string, in CodeInput, ip, ua string) (TokenResult, error) { db, err := s.db() if err != nil { return TokenResult{}, err } v, err := s.validatePending(ctx, db, token) if err != nil { return TokenResult{}, err } tag := s.deps.Tagger.Tag(in.Code) var id uuid.UUID var codeHash string var ver int err = db.QueryRow(ctx, `SELECT id, code_hash, pepper_version FROM auth_recovery_lookup($1,$2)`, v.userID, tag).Scan(&id, &codeHash, &ver) if errors.Is(err, pgx.ErrNoRows) { if aerr := s.afterMFAFailure(ctx, db, v, token, ip, ua); aerr != nil { return TokenResult{}, aerr } return TokenResult{}, ErrInvalidCredentials } if err != nil { return TokenResult{}, err } ok, _, err := s.deps.Hasher.Verify(in.Code, codeHash, ver) if err != nil { return TokenResult{}, err } if !ok { if aerr := s.afterMFAFailure(ctx, db, v, token, ip, ua); aerr != nil { return TokenResult{}, aerr } return TokenResult{}, ErrInvalidCredentials } newToken, newHash, err := security.NewSessionToken() if err != nil { return TokenResult{}, err } err = s.inTx(ctx, db, func(tx pgx.Tx) error { var claimed int if e := tx.QueryRow(ctx, `SELECT auth_recovery_claim($1)`, id).Scan(&claimed); e != nil { return e } if claimed == 0 { return ErrInvalidCredentials // already spent by a concurrent attempt } return rotate(ctx, tx, s.tokenHash(token), newHash, true, s.deps.IdleSeconds, s.deps.AbsoluteSeconds, ip, ua) }) if err != nil { return TokenResult{}, err } actor := mustUUID(v.userID) _ = s.deps.Audit.System(ctx, audit.Event{Action: "auth.mfa.recovery_code.used", ActorUserID: &actor, IPAddress: ip, UserAgent: ua}) return TokenResult{Token: newToken}, nil } // --------------------------------------------------------------------------- // Session lifecycle. // --------------------------------------------------------------------------- // Logout revokes the presented session. func (s *Service) Logout(ctx context.Context, token string, ip, ua string) error { db, err := s.db() if err != nil { return err } if _, err := db.Exec(ctx, `SELECT auth_revoke_session($1)`, s.tokenHash(token)); err != nil { return err } _ = s.deps.Audit.System(ctx, audit.Event{Action: "auth.logout", IPAddress: ip, UserAgent: ua}) return nil } // LogoutAll revokes every session of the authenticated user except the current one. func (s *Service) LogoutAll(ctx context.Context, token string) error { db, err := s.db() if err != nil { return err } p, ok := tenant.UserFromContext(ctx) if !ok { return ErrForbidden } if _, err := db.Exec(ctx, `SELECT auth_revoke_user_sessions($1,$2)`, mustUUID(p.UserID), s.tokenHash(token)); err != nil { return err } return nil } // ChangePassword verifies the old password, rehashes the new one, revokes all other sessions, and // rotates the current session to a fresh token. func (s *Service) ChangePassword(ctx context.Context, token string, in ChangePasswordInput, ip, ua string) (TokenResult, error) { db, err := s.db() if err != nil { return TokenResult{}, err } p, ok := tenant.UserFromContext(ctx) if !ok { return TokenResult{}, ErrForbidden } uid := mustUUID(p.UserID) var orgID uuid.UUID var email, hash string var ver int err = db.QueryRow(ctx, `SELECT organization_id, email_normalized, password_hash, pepper_version FROM auth_get_credential($1)`, uid). Scan(&orgID, &email, &hash, &ver) if errors.Is(err, pgx.ErrNoRows) { return TokenResult{}, ErrNotFound } if err != nil { return TokenResult{}, err } okOld, _, err := s.deps.Hasher.Verify(in.OldPassword, hash, ver) if err != nil { return TokenResult{}, err } if !okOld { return TokenResult{}, ErrInvalidCredentials } newHash, newVer, err := s.deps.Hasher.Hash(in.NewPassword) if err != nil { return TokenResult{}, err // includes ErrPasswordTooLong } newToken, newSessHash, err := security.NewSessionToken() if err != nil { return TokenResult{}, err } err = s.inTx(ctx, db, func(tx pgx.Tx) error { if _, e := tx.Exec(ctx, `SELECT auth_set_credential($1,$2,$3,$4,$5)`, uid, orgID, email, newHash, newVer); e != nil { return e } if _, e := tx.Exec(ctx, `SELECT auth_revoke_user_sessions($1,$2)`, uid, s.tokenHash(token)); e != nil { return e } return rotate(ctx, tx, s.tokenHash(token), newSessHash, true, s.deps.IdleSeconds, s.deps.AbsoluteSeconds, ip, ua) }) if err != nil { return TokenResult{}, err } _ = s.deps.Audit.System(ctx, audit.Event{Action: "auth.password.changed", ActorUserID: &uid, IPAddress: ip, UserAgent: ua}) return TokenResult{Token: newToken}, nil } // --------------------------------------------------------------------------- // Internal helpers. // --------------------------------------------------------------------------- type pendingSession struct { sessionID string userID string orgID string role string } // validatePending validates a session and requires it to be auth_pending (password verified, MFA not // yet satisfied). It is the gate for the MFA routes. func (s *Service) validatePending(ctx context.Context, db dbgen.DBTX, token string) (pendingSession, error) { v, err := s.validate(ctx, db, token) if err != nil { return pendingSession{}, err } if v.state != "auth_pending" { return pendingSession{}, ErrInvalidState } return pendingSession{sessionID: v.sessionID, userID: v.userID, orgID: v.orgID, role: v.role}, nil } type validatedSession struct { sessionID string userID string orgID string role string state string mfaSatisfied bool userStatus string } // validate runs the atomic validate, slide, and revocation check. No row means invalid. func (s *Service) validate(ctx context.Context, db dbgen.DBTX, token string) (validatedSession, error) { var sid, uid, oid uuid.UUID var v validatedSession err := db.QueryRow(ctx, `SELECT session_id, user_id, organization_id, role, state, mfa_satisfied, user_status FROM auth_validate_session($1,$2)`, s.tokenHash(token), s.deps.IdleSeconds).Scan(&sid, &uid, &oid, &v.role, &v.state, &v.mfaSatisfied, &v.userStatus) if errors.Is(err, pgx.ErrNoRows) { return validatedSession{}, ErrInvalidSession } if err != nil { return validatedSession{}, err } v.sessionID, v.userID, v.orgID = sid.String(), uid.String(), oid.String() return v, nil } // Validate is the resolver facing check: it returns the principal only for an active, MFA satisfied // session, so a pre MFA session can never reach a tenant route. func (s *Service) Validate(ctx context.Context, token string) (tenant.Principal, string, error) { db, err := s.db() if err != nil { return tenant.Principal{}, "", err } v, err := s.validate(ctx, db, token) if err != nil { return tenant.Principal{}, "", err } if v.state != "active" || !v.mfaSatisfied || v.orgID == "" { return tenant.Principal{}, "", ErrInvalidSession } return tenant.Principal{UserID: v.userID, Role: v.role}, v.orgID, nil } func (s *Service) verifyTOTPCode(ctx context.Context, db dbgen.DBTX, userID, code, wantStatus string) (int64, error) { var ct, nonce []byte var keyVer int var status string var lastStep int64 err := db.QueryRow(ctx, `SELECT secret_ciphertext, secret_nonce, key_version, status, last_used_step FROM auth_mfa_get($1)`, mustUUID(userID)). Scan(&ct, &nonce, &keyVer, &status, &lastStep) if errors.Is(err, pgx.ErrNoRows) { return 0, ErrInvalidState } if err != nil { return 0, err } if status != wantStatus { return 0, ErrInvalidState } kek, ok := s.deps.TOTPKEKs[keyVer] if !ok { return 0, fmt.Errorf("auth: TOTP key version %d not configured", keyVer) } secret, err := security.OpenSecret(kek, ct, nonce) if err != nil { return 0, err } ok, used := s.deps.TOTP.Validate(secret, code, nowFn(), lastStep) if !ok { return 0, ErrInvalidCredentials } return used, nil } func (s *Service) advanceAndRotate(ctx context.Context, db dbgen.DBTX, v pendingSession, oldToken string, used int64, ip, ua, action string) (string, error) { newToken, newHash, err := security.NewSessionToken() if err != nil { return "", err } err = s.inTx(ctx, db, func(tx pgx.Tx) error { // Advancing the monotonic step is the authoritative single use gate. If a concurrent verify // already consumed this step the update affects zero rows, so we abort and mint no session. var advanced int if e := tx.QueryRow(ctx, `SELECT auth_mfa_advance_step($1,$2)`, mustUUID(v.userID), used).Scan(&advanced); e != nil { return e } if advanced == 0 { return ErrInvalidCredentials } return rotate(ctx, tx, s.tokenHash(oldToken), newHash, true, s.deps.IdleSeconds, s.deps.AbsoluteSeconds, ip, ua) }) if err != nil { return "", err } actor := mustUUID(v.userID) _ = s.deps.Audit.System(ctx, audit.Event{Action: action, ActorUserID: &actor, IPAddress: ip, UserAgent: ua}) return newToken, nil } // afterMFAFailure records a failed MFA attempt and revokes the pending session once the per account // and ip budget is exhausted, forcing a fresh password login (the limiter is not reset by re login). // Its security audit writes fail closed, matching the login failure path: if the trail cannot be // written the request surfaces that error rather than proceeding with the attempt unrecorded. func (s *Service) afterMFAFailure(ctx context.Context, db dbgen.DBTX, v pendingSession, token, ip, ua string) error { email, err := s.credentialEmail(ctx, db, v.userID) if err != nil { email = v.userID // fall back to a stable non secret key } s.recordAttempt(ctx, db, email, orgPtr(v.orgID), ip, false, reasonMFAFailed) actor := mustUUID(v.userID) if aerr := s.deps.Audit.System(ctx, audit.Event{Action: "auth.mfa.verify.failure", ActorUserID: &actor, IPAddress: ip, UserAgent: ua}); aerr != nil { return aerr } count, cerr := s.failureCount(ctx, db, email, ip, reasonMFAFailed) if cerr == nil && count >= maxMFAFailures { if _, derr := db.Exec(ctx, `SELECT auth_revoke_session($1)`, s.tokenHash(token)); derr != nil { return derr } if aerr := s.deps.Audit.System(ctx, audit.Event{Action: "auth.mfa.pending_session.revoked", ActorUserID: &actor, IPAddress: ip, UserAgent: ua}); aerr != nil { return aerr } } return nil } func (s *Service) buildRecoveryCodes() (codes []string, hashes []string, versions []int, tags [][]byte, err error) { codes, err = security.GenerateRecoveryCodes(recoveryCodeCount) if err != nil { return nil, nil, nil, nil, err } for _, c := range codes { h, ver, herr := s.deps.Hasher.Hash(c) if herr != nil { return nil, nil, nil, nil, herr } hashes = append(hashes, h) versions = append(versions, ver) tags = append(tags, s.deps.Tagger.Tag(c)) } return codes, hashes, versions, tags, nil } func (s *Service) lookupLogin(ctx context.Context, db dbgen.DBTX, email string) (loginRow, bool, error) { var r loginRow err := db.QueryRow(ctx, `SELECT user_id, organization_id, role, user_status, password_hash, pepper_version, mfa_enrolled, mfa_status, mfa_secret_ciphertext, mfa_secret_nonce, mfa_key_version, last_used_step FROM auth_lookup_login($1)`, email). Scan(&r.userID, &r.orgID, &r.role, &r.userStatus, &r.passwordHash, &r.pepperVer, &r.mfaEnrolled, &r.mfaStatus, &r.secretCipher, &r.secretNonce, &r.keyVersion, &r.lastUsedStep) if errors.Is(err, pgx.ErrNoRows) { return loginRow{}, false, nil } if err != nil { return loginRow{}, false, err } if r.userStatus != "active" && r.userStatus != "invited" { return loginRow{}, false, nil // disabled accounts cannot log in; treated as not found } return r, true, nil } func (s *Service) credentialEmail(ctx context.Context, db dbgen.DBTX, userID string) (string, error) { var org uuid.UUID var email, hash string var ver int err := db.QueryRow(ctx, `SELECT organization_id, email_normalized, password_hash, pepper_version FROM auth_get_credential($1)`, mustUUID(userID)). Scan(&org, &email, &hash, &ver) if err != nil { return "", err } return email, nil } func (s *Service) recordAttempt(ctx context.Context, db dbgen.DBTX, email string, org *uuid.UUID, ip string, ok bool, reason string) { var reasonPtr *string if reason != "" { reasonPtr = &reason } _, _ = db.Exec(ctx, `INSERT INTO login_attempts (email_normalized, organization_id, ip_address, succeeded, reason) VALUES (lower($1),$2,$3,$4,$5)`, email, org, nullStr(ip), ok, reasonPtr) } func (s *Service) tooManyFailures(ctx context.Context, db dbgen.DBTX, email, ip string) (bool, error) { n, err := s.failureCount(ctx, db, email, ip, reasonBadPassword) if err != nil { return false, err } return n >= maxFailedLogins, nil } func (s *Service) failureCount(ctx context.Context, db dbgen.DBTX, email, ip, reason string) (int, error) { var n int err := db.QueryRow(ctx, `SELECT count(*) FROM login_attempts WHERE email_normalized = lower($1) AND succeeded = false AND reason = $2 AND (ip_address = $3 OR $3 IS NULL) AND created_at > now() - make_interval(secs => $4)`, email, reason, nullStr(ip), failWindowSeconds).Scan(&n) return n, err } func (s *Service) failureAudit(ctx context.Context, actor *uuid.UUID, ip, ua, reason string) error { return s.deps.Audit.System(ctx, audit.Event{ Action: "auth.login.failure", ActorUserID: actor, IPAddress: ip, UserAgent: ua, EntityType: "login", NewValues: reasonJSON(reason), }) } func (s *Service) tokenHash(token string) []byte { return security.HashSessionToken(token) } func (s *Service) inTx(ctx context.Context, db dbgen.DBTX, fn func(tx pgx.Tx) error) (err error) { pool, ok := db.(interface { Begin(context.Context) (pgx.Tx, error) }) if !ok { return errors.New("auth: pool does not support transactions") } tx, err := pool.Begin(ctx) if err != nil { return err } defer func() { _ = tx.Rollback(ctx) }() if err = fn(tx); err != nil { return err } return tx.Commit(ctx) } // rotate revokes the old token and mints a new one in one definer call. func rotate(ctx context.Context, tx pgx.Tx, oldHash, newHash []byte, mfaSatisfied bool, idle, absolute int, ip, ua string) error { var sid uuid.UUID var idleExp, absExp interface{} err := tx.QueryRow(ctx, `SELECT session_id, idle_expires_at, absolute_expires_at FROM auth_rotate_session($1,$2,'active',$3,$4,$5,$6,$7)`, oldHash, newHash, mfaSatisfied, idle, absolute, nullStr(ip), nullStr(ua)).Scan(&sid, &idleExp, &absExp) if errors.Is(err, pgx.ErrNoRows) { return ErrInvalidSession } return err }

Search equations, builds, manuscript text, and recovered sources.