package onboarding import ( "context" "net/http" "time" "github.com/jackc/pgx/v5" ) const trustedLoginLifetime = 30 * 24 * time.Hour func (s *Service) loginRealm() string { if s.operatorLogin { return "operator" } return "customer" } func (s *Service) trustedLoginCookieName() string { name := "platform-" + s.loginRealm() + "-trusted-device" if !s.options.AllowLocalHTTP { name = "__Host-" + name } return name } func (s *Service) trustedLoginCookie(r *http.Request) string { if c, err := r.Cookie(s.trustedLoginCookieName()); err == nil && validB64(c.Value, 32) { return c.Value } return "" } // Only a successfully verified, single-use SMS can establish device trust. // Bind it to the exact credential so an administrative password reset also // invalidates previously trusted devices without needing a separate revocation. func (s *Service) rememberLoginDevice(ctx context.Context, tx pgx.Tx, phoneHash string) (string, error) { var encoded string if err := tx.QueryRow(ctx, "SELECT COALESCE(password_hash,'') FROM "+s.phoneCredentialTable()+" WHERE phone_digest=$1 FOR SHARE", phoneHash).Scan(&encoded); err != nil { return "", err } token := randomToken(32) _, err := tx.Exec(ctx, `INSERT INTO trusted_login_device(token_hash,realm,phone_digest,credential_digest,verified_at,expires_at) VALUES($1,$2,$3,$4,$5,$6)`, digest([]byte(token)), s.loginRealm(), phoneHash, digest([]byte(encoded)), s.now(), s.now().Add(trustedLoginLifetime)) return token, err } func (s *Service) requireTrustedLoginDevice(ctx context.Context, tx pgx.Tx, phoneHash, encoded, token string) error { trusted := false if validB64(token, 32) { err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM trusted_login_device WHERE token_hash=$1 AND realm=$2 AND phone_digest=$3 AND credential_digest=$4 AND expires_at>$5 AND revoked_at IS NULL)`, digest([]byte(token)), s.loginRealm(), phoneHash, digest([]byte(encoded)), s.now()).Scan(&trusted) if err != nil { return err } } if !trusted { return fail(403, "NEW_DEVICE_SMS_REQUIRED", "首次在此设备登录,请使用手机验证码完成验证") } return nil } func (s *Service) updateTrustedDevicesAfterPassword(ctx context.Context, tx pgx.Tx, phoneHash, oldHash, newHash, currentToken string) error { // Retain only an existing, unexpired SMS verification on this browser. A // password change must never turn an unverified browser into a trusted one. _, err := tx.Exec(ctx, `UPDATE trusted_login_device SET credential_digest=CASE WHEN token_hash=$3 AND credential_digest=$4 AND expires_at>$6 THEN $5 ELSE credential_digest END,revoked_at=CASE WHEN token_hash=$3 AND credential_digest=$4 AND expires_at>$6 THEN NULL ELSE $6 END WHERE realm=$1 AND phone_digest=$2 AND revoked_at IS NULL`, s.loginRealm(), phoneHash, digest([]byte(currentToken)), digest([]byte(oldHash)), digest([]byte(newHash)), s.now()) return err }