Repository: supabase/gotrue
Pull request #649 (2022-08-29 14:05:16)

feat: use proper ip address
Uses the [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) header to determine the proper origin of the HTTP request. Comes out of #582.


Patch:
diff --git a/conf/configuration.go b/conf/configuration.go
index 38e2ae7a..d87b41b1 100644
--- a/conf/configuration.go
+++ b/conf/configuration.go
@@ -2,6 +2,7 @@ package conf
 
 import (
 	"errors"
+	"net/url"
 	"os"
 	"time"
 
@@ -36,6 +37,10 @@ type DBConfiguration struct {
 	MigrationsPath string `json:"migrations_path" split_words:"true" default:"./migrations"`
 }
 
+func (c *DBConfiguration) Validate() error {
+	return nil
+}
+
 // JWTConfiguration holds all the JWT related configuration.
 type JWTConfiguration struct {
 	Secret           string   `json:"secret" required:"true"`
@@ -46,15 +51,30 @@ type JWTConfiguration struct {
 	DefaultGroupName string   `json:"default_group_name" split_words:"true"`
 }
 
+type APIConfiguration struct {
+	Host            string
+	Port            int `envconfig:"PORT" default:"8081"`
+	Endpoint        string
+	RequestIDHeader string `envconfig:"REQUEST_ID_HEADER"`
+	ExternalURL     string `json:"external_url" envconfig:"API_EXTERNAL_URL"`
+}
+
+func (a *APIConfiguration) Validate() error {
+	if a.ExternalURL != "" {
+		// sometimes, in tests, ExternalURL is empty and we regard that
+		// as a valid value
+		_, err := url.ParseRequestURI(a.ExternalURL)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // GlobalConfiguration holds all the configuration that applies to all instances.
 type GlobalConfiguration struct {
-	API struct {
-		Host            string
-		Port            int `envconfig:"PORT" default:"8081"`
-		Endpoint        string
-		RequestIDHeader string `envconfig:"REQUEST_ID_HEADER"`
-		ExternalURL     string `json:"external_url" envconfig:"API_EXTERNAL_URL"`
-	}
+	API                   APIConfiguration
 	DB                    DBConfiguration
 	External              ProviderConfiguration
 	Logging               LoggingConfig `envconfig:"LOG"`
@@ -127,6 +147,10 @@ type SMTPConfiguration struct {
 	SenderName   string        `json:"sender_name" split_words:"true"`
 }
 
+func (c *SMTPConfiguration) Validate() error {
+	return nil
+}
+
 type MailerConfiguration struct {
 	Autoconfirm              bool                      `json:"autoconfirm"`
 	Subjects                 EmailContentConfiguration `json:"subjects"`
@@ -231,7 +255,13 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) {
 		return nil, err
 	}
 
-	config.ApplyDefaults()
+	if err := config.ApplyDefaults(); err != nil {
+		return nil, err
+	}
+
+	if err := config.Validate(); err != nil {
+		return nil, err
+	}
 
 	if _, err := ConfigureLogging(&config.Logging); err != nil {
 		return nil, err
@@ -239,14 +269,11 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) {
 
 	ConfigureTracing(&config.Tracing)
 
-	if config.SMTP.MaxFrequency == 0 {
-		config.SMTP.MaxFrequency = 1 * time.Minute
-	}
 	return config, nil
 }
 
 // ApplyDefaults sets defaults for a GlobalConfiguration
-func (config *GlobalConfiguration) ApplyDefaults() {
+func (config *GlobalConfiguration) ApplyDefaults() error {
 	if config.JWT.AdminGroupName == "" {
 		config.JWT.AdminGroupName = "admin"
 	}
@@ -320,6 +347,7 @@ func (config *GlobalConfiguration) ApplyDefaults() {
 	if config.URIAllowList == nil {
 		config.URIAllowList = []string{}
 	}
+
 	if config.URIAllowList != nil {
 		config.URIAllowListMap = make(map[string]glob.Glob)
 		for _, uri := range config.URIAllowList {
@@ -327,9 +355,32 @@ func (config *GlobalConfiguration) ApplyDefaults() {
 			config.URIAllowListMap[uri] = g
 		}
 	}
+
 	if config.PasswordMinLength < defaultMinPasswordLength {
 		config.PasswordMinLength = defaultMinPasswordLength
 	}
+
+	return nil
+}
+
+// Validate validates all of configuration.
+func (c *GlobalConfiguration) Validate() error {
+	validatables := []interface {
+		Validate() error
+	}{
+		&c.API,
+		&c.DB,
+		&c.Tracing,
+		&c.SMTP,
+	}
+
+	for _, validatable := range validatables {
+		if err := validatable.Validate(); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
 func (o *OAuthProviderConfiguration) Validate() error {
diff --git a/conf/tracing.go b/conf/tracing.go
index 8f51ffe9..ad01540d 100644
--- a/conf/tracing.go
+++ b/conf/tracing.go
@@ -16,6 +16,10 @@ type TracingConfig struct {
 	Tags        map[string]string
 }
 
+func (tc *TracingConfig) Validate() error {
+	return nil
+}
+
 func (tc *TracingConfig) tracingAddr() string {
 	return fmt.Sprintf("%s:%s", tc.Host, tc.Port)
 }
diff --git a/crypto/crypto.go b/crypto/crypto.go
index a6af6d8c..6912a75b 100644
--- a/crypto/crypto.go
+++ b/crypto/crypto.go
@@ -33,22 +33,3 @@ func GenerateOtp(digits int) (string, error) {
 	otp := fmt.Sprintf(expr, val.String())
 	return otp, nil
 }
-
-// GenerateOtpFromCharset generates a random n-length otp from a charset
-func GenerateOtpFromCharset(length int, charset string) (string, error) {
-	b := make([]byte, length)
-	for i := range b {
-		val, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
-		if err != nil {
-			return "", errors.WithMessage(err, "Error generating otp from charset")
-		}
-		b[i] = charset[val.Int64()]
-	}
-	return string(b), nil
-}
-
-// GenerateEmailOtp generates a random n-length alphanumeric otp
-func GenerateEmailOtp(length int) (string, error) {
-	const charset = "abcdefghijklmnopqrstuvwxyz"
-	return GenerateOtpFromCharset(length, charset)
-}
diff --git a/logger/log.go b/logger/log.go
index 8f5c14a9..705acfb4 100644
--- a/logger/log.go
+++ b/logger/log.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	chimiddleware "github.com/go-chi/chi/middleware"
+	"github.com/netlify/gotrue/utilities"
 	"github.com/sirupsen/logrus"
 )
 
@@ -26,7 +27,7 @@ func (l *structuredLogger) NewLogEntry(r *http.Request) chimiddleware.LogEntry {
 		"component":   "api",
 		"method":      r.Method,
 		"path":        r.URL.Path,
-		"remote_addr": r.RemoteAddr,
+		"remote_addr": utilities.GetIPAddress(r),
 		"referer":     r.Referer(),
 		"timestamp":   time.Now().UTC().Format(time.RFC3339),
 	}
diff --git a/models/connection.go b/models/connection.go
index ddb50b87..446c58ff 100644
--- a/models/connection.go
+++ b/models/connection.go
@@ -30,17 +30,24 @@ type SortField struct {
 	Dir  SortDirection
 }
 
+// TruncateAll deletes all data from the database, as managed by GoTrue. Not
+// intended for use outside of tests.
 func TruncateAll(conn *storage.Connection) error {
 	return conn.Transaction(func(tx *storage.Connection) error {
-		if err := tx.RawQuery("delete from " + (&pop.Model{Value: User{}}).TableName()).Exec(); err != nil {
-			return err
+		tables := []string{
+			(&pop.Model{Value: User{}}).TableName(),
+			(&pop.Model{Value: Identity{}}).TableName(),
+			(&pop.Model{Value: RefreshToken{}}).TableName(),
+			(&pop.Model{Value: AuditLogEntry{}}).TableName(),
+			(&pop.Model{Value: Session{}}).TableName(),
 		}
-		if err := tx.RawQuery("delete from " + (&pop.Model{Value: RefreshToken{}}).TableName()).Exec(); err != nil {
-			return err
-		}
-		if err := tx.RawQuery("delete from " + (&pop.Model{Value: AuditLogEntry{}}).TableName()).Exec(); err != nil {
-			return err
+
+		for _, tableName := range tables {
+			if err := tx.RawQuery("TRUNCATE " + tableName + " CASCADE").Exec(); err != nil {
+				return err
+			}
 		}
+
 		return nil
 	})
 }
diff --git a/security/hcaptcha.go b/security/hcaptcha.go
index c584f77e..a0014ab1 100644
--- a/security/hcaptcha.go
+++ b/security/hcaptcha.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/netlify/gotrue/utilities"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
@@ -77,7 +78,7 @@ func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error
 	if err != nil || strings.TrimSpace(res.Security.Token) == "" {
 		return UserRequestFailed, errors.Wrap(err, "couldn't decode captcha info")
 	}
-	clientIP := strings.Split(r.RemoteAddr, ":")[0]
+	clientIP := utilities.GetIPAddress(r)
 	return verifyCaptchaCode(res.Security.Token, secretKey, clientIP)
 }
 
diff --git a/utilities/request.go b/utilities/request.go
new file mode 100644
index 00000000..3e244b7b
--- /dev/null
+++ b/utilities/request.go
@@ -0,0 +1,35 @@
+package utilities
+
+import (
+	"net"
+	"net/http"
+	"strings"
+)
+
+// GetIPAddress returns the real IP address of the HTTP request. It parses the
+// X-Forwarded-For header.
+func GetIPAddress(r *http.Request) string {
+	if r.Header != nil {
+		xForwardedFor := r.Header.Get("X-Forwarded-For")
+		if xForwardedFor != "" {
+			ips := strings.Split(xForwardedFor, ",")
+			for i := range ips {
+				ips[i] = strings.TrimSpace(ips[i])
+			}
+
+			for _, ip := range ips {
+				if ip != "" {
+					return ip
+				}
+			}
+		}
+	}
+
+	ipPort := r.RemoteAddr
+	ip, _, err := net.SplitHostPort(ipPort)
+	if err != nil {
+		return ipPort
+	}
+
+	return ip
+}


Test patch:
diff --git a/utilities/request_test.go b/utilities/request_test.go
new file mode 100644
index 00000000..da76042e
--- /dev/null
+++ b/utilities/request_test.go
@@ -0,0 +1,87 @@
+package utilities
+
+import (
+	"net/http"
+	tst "testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetIPAddress(t *tst.T) {
+	examples := []func(r *http.Request) string{
+		func(r *http.Request) string {
+			r.Header = nil
+			r.RemoteAddr = "127.0.0.1:8080"
+
+			return "127.0.0.1"
+		},
+
+		func(r *http.Request) string {
+			r.Header = nil
+			r.RemoteAddr = "incorrect"
+
+			return "incorrect"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "127.0.0.1:8080"
+
+			return "127.0.0.1"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "[::1]:8080"
+
+			return "::1"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "127.0.0.1:8080"
+			r.Header.Add("X-Forwarded-For", "127.0.0.2")
+
+			return "127.0.0.2"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "127.0.0.1:8080"
+			r.Header.Add("X-Forwarded-For", "127.0.0.2")
+
+			return "127.0.0.2"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "127.0.0.1:8080"
+			r.Header.Add("X-Forwarded-For", "127.0.0.2,")
+
+			return "127.0.0.2"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "127.0.0.1:8080"
+			r.Header.Add("X-Forwarded-For", "127.0.0.2,127.0.0.3")
+
+			return "127.0.0.2"
+		},
+
+		func(r *http.Request) string {
+			r.Header = make(http.Header)
+			r.RemoteAddr = "127.0.0.1:8080"
+			r.Header.Add("X-Forwarded-For", "::1,127.0.0.2")
+
+			return "::1"
+		},
+	}
+
+	for _, example := range examples {
+		req := &http.Request{}
+		expected := example(req)
+
+		require.Equal(t, GetIPAddress(req), expected)
+	}
+}