Repository: supabase/gotrue
Pull request #657 (2022-08-31 13:29:17)

refactor: add pointer type checks in `IsNotFoundError`
In the codebase today none of these errors are passed as pointers, but I can see how easily a mistake can be made where a pointer is passed.


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2d08ff59..ff3464a3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,12 +1,15 @@
 name: Build and Release on Push to Master
 
 on:
+  workflow_dispatch:
+
   workflow_run:
     workflows: [Test]
     types: [completed]
     branches:
       - master
       - mfa
+      - zerosessionidfix
 
 jobs:
   release:
diff --git a/.releaserc b/.releaserc
index 9a1cc36c..9e263fff 100644
--- a/.releaserc
+++ b/.releaserc
@@ -4,6 +4,10 @@
     {
         "name": "mfa",
         "prerelease": true
+    },
+    {
+        "name": "zerosessionidfix",
+        "prerelease": true
     }
   ],
   "plugins": [
diff --git a/Makefile b/Makefile
index 48c885f0..0e12d6ec 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all build deps dev-deps image migrate test vet sec format
+.PHONY: all build deps dev-deps image migrate test vet sec format unused
 CHECK_FILES?=./...
 FLAGS?=-ldflags "-X github.com/netlify/gotrue/cmd.Version=`git describe --tags`"
 DEV_DOCKER_COMPOSE:=docker-compose-dev.yml
@@ -6,13 +6,13 @@ DEV_DOCKER_COMPOSE:=docker-compose-dev.yml
 help: ## Show this help.
 	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
 
-all: vet sec build ## Run the tests and build the binary.
+all: vet sec unused build ## Run the tests and build the binary.
 
 build: deps ## Build the binary.
 	go build $(FLAGS)
 	GOOS=linux GOARCH=arm64 go build $(FLAGS) -o gotrue-arm64
 
-dev-deps: deps ## Install developer dependencies
+dev-deps: ## Install developer dependencies
 	@go install github.com/gobuffalo/pop/soda@latest
 	@go install github.com/securego/gosec/v2/cmd/gosec@latest
 	@go install honnef.co/go/tools/cmd/staticcheck@latest
@@ -33,11 +33,11 @@ test: build ## Run tests.
 vet: # Vet the code
 	go vet $(CHECK_FILES)
 
-sec: # Check for security vulnerabilities
+sec: dev-deps # Check for security vulnerabilities
 	gosec -quiet $(CHECK_FILES)
 	gosec -quiet -tests -exclude=G104 $(CHECK_FILES)
 
-unused: # Look for unused code
+unused: dev-deps # Look for unused code
 	@echo "Unused code:"
 	staticcheck -checks U1000 $(CHECK_FILES)
 	
diff --git a/api/auth.go b/api/auth.go
index 6c42066c..797bc6fb 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -15,7 +15,7 @@ import (
 
 // requireAuthentication checks incoming requests for tokens presented using the Authorization header
 func (a *API) requireAuthentication(w http.ResponseWriter, r *http.Request) (context.Context, error) {
-	token, err := a.extractBearerToken(w, r)
+	token, err := a.extractBearerToken(r)
 	config := a.config
 	if err != nil {
 		a.clearCookieTokens(config, w)
@@ -53,7 +53,7 @@ func (a *API) requireAdmin(ctx context.Context, w http.ResponseWriter, r *http.R
 	return nil, unauthorizedError("User not allowed")
 }
 
-func (a *API) extractBearerToken(w http.ResponseWriter, r *http.Request) (string, error) {
+func (a *API) extractBearerToken(r *http.Request) (string, error) {
 	authHeader := r.Header.Get("Authorization")
 	matches := bearerRegexp.FindStringSubmatch(authHeader)
 	if len(matches) != 2 {
@@ -103,13 +103,13 @@ func (a *API) maybeLoadUserOrSession(ctx context.Context) (context.Context, erro
 	}
 
 	var session *models.Session
-	if claims.SessionId != "" {
+	if claims.SessionId != "" && claims.SessionId != uuid.Nil.String() {
 		sessionId, err := uuid.FromString(claims.SessionId)
 		if err != nil {
 			return ctx, err
 		}
 		session, err = models.FindSessionById(a.db, sessionId)
-		if err != nil {
+		if err != nil && !models.IsNotFoundError(err) {
 			return ctx, err
 		}
 		ctx = withSession(ctx, session)
diff --git a/api/context.go b/api/context.go
index 394e701e..f4d1737f 100644
--- a/api/context.go
+++ b/api/context.go
@@ -73,6 +73,9 @@ func withUser(ctx context.Context, u *models.User) context.Context {
 
 // getUser reads the user from the context.
 func getUser(ctx context.Context) *models.User {
+	if ctx == nil {
+		return nil
+	}
 	obj := ctx.Value(userKey)
 	if obj == nil {
 		return nil
@@ -87,6 +90,9 @@ func withSession(ctx context.Context, s *models.Session) context.Context {
 
 // getSession reads the session from the context.
 func getSession(ctx context.Context) *models.Session {
+	if ctx == nil {
+		return nil
+	}
 	obj := ctx.Value(sessionKey)
 	if obj == nil {
 		return nil
diff --git a/api/external.go b/api/external.go
index ed84659d..e005fb4e 100644
--- a/api/external.go
+++ b/api/external.go
@@ -109,7 +109,8 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 
 	providerType := getExternalProviderType(ctx)
 	var userData *provider.UserProvidedData
-	var providerToken string
+	var providerAccessToken string
+	var providerRefreshToken string = ""
 	if providerType == "twitter" {
 		// future OAuth1.0 providers will use this method
 		oAuthResponseData, err := a.oAuth1Callback(ctx, r, providerType)
@@ -117,14 +118,15 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 			return err
 		}
 		userData = oAuthResponseData.userData
-		providerToken = oAuthResponseData.token
+		providerAccessToken = oAuthResponseData.token
 	} else {
 		oAuthResponseData, err := a.oAuthCallback(ctx, r, providerType)
 		if err != nil {
 			return err
 		}
 		userData = oAuthResponseData.userData
-		providerToken = oAuthResponseData.token
+		providerAccessToken = oAuthResponseData.token
+		providerRefreshToken = oAuthResponseData.refreshToken
 	}
 
 	var user *models.User
@@ -286,7 +288,12 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 	rurl := a.getExternalRedirectURL(r)
 	if token != nil {
 		q := url.Values{}
-		q.Set("provider_token", providerToken)
+		q.Set("provider_token", providerAccessToken)
+		// Because not all providers give out a refresh token
+		// See corresponding OAuth2 spec: <https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1>
+		if providerRefreshToken != "" {
+			q.Set("provider_refresh_token", providerRefreshToken)
+		}
 		q.Set("access_token", token.Token)
 		q.Set("token_type", token.TokenType)
 		q.Set("expires_in", strconv.Itoa(token.ExpiresIn))
diff --git a/api/external_oauth.go b/api/external_oauth.go
index dc645fff..d00d9836 100644
--- a/api/external_oauth.go
+++ b/api/external_oauth.go
@@ -14,8 +14,9 @@ import (
 
 // OAuthProviderData contains the userData and token returned by the oauth provider
 type OAuthProviderData struct {
-	userData *provider.UserProvidedData
-	token    string
+	userData     *provider.UserProvidedData
+	token        string
+	refreshToken string
 }
 
 // loadOAuthState parses the `state` query parameter as a JWS payload,
@@ -96,8 +97,9 @@ func (a *API) oAuthCallback(ctx context.Context, r *http.Request, providerType s
 	}
 
 	return &OAuthProviderData{
-		userData: userData,
-		token:    token.AccessToken,
+		userData:     userData,
+		token:        token.AccessToken,
+		refreshToken: token.RefreshToken,
 	}, nil
 }
 
@@ -134,8 +136,9 @@ func (a *API) oAuth1Callback(ctx context.Context, r *http.Request, providerType
 	}
 
 	return &OAuthProviderData{
-		userData: userData,
-		token:    accessToken.Token,
+		userData:     userData,
+		token:        accessToken.Token,
+		refreshToken: "",
 	}, nil
 
 }
diff --git a/api/magic_link.go b/api/magic_link.go
index 01d28ac3..8e743f00 100644
--- a/api/magic_link.go
+++ b/api/magic_link.go
@@ -15,8 +15,8 @@ import (
 
 // MagicLinkParams holds the parameters for a magic link request
 type MagicLinkParams struct {
-	Email    string                 `json:"email"`
-	Metadata map[string]interface{} `json:"metadata"`
+	Email string                 `json:"email"`
+	Data  map[string]interface{} `json:"metadata"`
 }
 
 // MagicLink sends a recovery email
@@ -35,8 +35,8 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error {
 		return badRequestError("Could not read verification params: %v", err)
 	}
 
-	if params.Metadata == nil {
-		params.Metadata = make(map[string]interface{})
+	if params.Data == nil {
+		params.Data = make(map[string]interface{})
 	}
 
 	if params.Email == "" {
@@ -59,7 +59,7 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error {
 			signUpParams := &SignupParams{
 				Email:    params.Email,
 				Password: password,
-				Data:     params.Metadata,
+				Data:     params.Data,
 			}
 			newBodyContent, err := json.Marshal(signUpParams)
 			if err != nil {
@@ -76,7 +76,7 @@ func (a *API) MagicLink(w http.ResponseWriter, r *http.Request) error {
 				}
 				newBodyContent := &SignupParams{
 					Email: params.Email,
-					Data:  params.Metadata,
+					Data:  params.Data,
 				}
 				metadata, err := json.Marshal(newBodyContent)
 				if err != nil {
diff --git a/api/middleware.go b/api/middleware.go
index 69c76701..ff604d96 100644
--- a/api/middleware.go
+++ b/api/middleware.go
@@ -97,7 +97,7 @@ func (a *API) limitEmailSentHandler() middlewareHandler {
 }
 
 func (a *API) requireAdminCredentials(w http.ResponseWriter, req *http.Request) (context.Context, error) {
-	t, err := a.extractBearerToken(w, req)
+	t, err := a.extractBearerToken(req)
 	if err != nil || t == "" {
 		return nil, err
 	}
diff --git a/api/otp.go b/api/otp.go
index da626f5d..6aa4c84e 100644
--- a/api/otp.go
+++ b/api/otp.go
@@ -17,13 +17,13 @@ type OtpParams struct {
 	Email      string                 `json:"email"`
 	Phone      string                 `json:"phone"`
 	CreateUser bool                   `json:"create_user"`
-	Metadata   map[string]interface{} `json:"metadata"`
+	Data       map[string]interface{} `json:"data"`
 }
 
 // SmsParams contains the request body params for sms otp
 type SmsParams struct {
-	Phone    string                 `json:"phone"`
-	Metadata map[string]interface{} `json:"metadata"`
+	Phone string                 `json:"phone"`
+	Data  map[string]interface{} `json:"data"`
 }
 
 // Otp returns the MagicLink or SmsOtp handler based on the request body params
@@ -31,8 +31,8 @@ func (a *API) Otp(w http.ResponseWriter, r *http.Request) error {
 	params := &OtpParams{
 		CreateUser: true,
 	}
-	if params.Metadata == nil {
-		params.Metadata = make(map[string]interface{})
+	if params.Data == nil {
+		params.Data = make(map[string]interface{})
 	}
 
 	body, err := getBodyBytes(r)
@@ -82,8 +82,8 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error {
 	if err := json.Unmarshal(body, params); err != nil {
 		return badRequestError("Could not read sms otp params: %v", err)
 	}
-	if params.Metadata == nil {
-		params.Metadata = make(map[string]interface{})
+	if params.Data == nil {
+		params.Data = make(map[string]interface{})
 	}
 
 	params.Phone, err = a.validatePhone(params.Phone)
@@ -105,7 +105,7 @@ func (a *API) SmsOtp(w http.ResponseWriter, r *http.Request) error {
 			signUpParams := &SignupParams{
 				Phone:    params.Phone,
 				Password: password,
-				Data:     params.Metadata,
+				Data:     params.Data,
 			}
 			newBodyContent, err := json.Marshal(signUpParams)
 			if err != nil {
diff --git a/api/token.go b/api/token.go
index b92aedb1..1d267012 100644
--- a/api/token.go
+++ b/api/token.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gofrs/uuid"
 	"github.com/golang-jwt/jwt"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/metering"
@@ -26,7 +27,7 @@ type GoTrueClaims struct {
 	AppMetaData  map[string]interface{} `json:"app_metadata"`
 	UserMetaData map[string]interface{} `json:"user_metadata"`
 	Role         string                 `json:"role"`
-	SessionId    string                 `json:"session_id"`
+	SessionId    string                 `json:"session_id,omitempty"`
 }
 
 // AccessTokenResponse represents an OAuth2 success response
@@ -325,7 +326,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 			}
 		}
 
-		tokenString, terr = generateAccessToken(user, newToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+		tokenString, terr = generateAccessToken(user, newToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
 		if terr != nil {
 			return internalServerError("error generating jwt token").WithInternalError(terr)
 		}
@@ -526,7 +527,11 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R
 	return sendJSON(w, http.StatusOK, token)
 }
 
-func generateAccessToken(user *models.User, sessionId string, expiresIn time.Duration, secret string) (string, error) {
+func generateAccessToken(user *models.User, sessionId *uuid.UUID, expiresIn time.Duration, secret string) (string, error) {
+	sid := ""
+	if sessionId != nil {
+		sid = sessionId.String()
+	}
 	claims := &GoTrueClaims{
 		StandardClaims: jwt.StandardClaims{
 			Subject:   user.ID.String(),
@@ -538,7 +543,7 @@ func generateAccessToken(user *models.User, sessionId string, expiresIn time.Dur
 		AppMetaData:  user.AppMetaData,
 		UserMetaData: user.UserMetaData,
 		Role:         user.Role,
-		SessionId:    sessionId,
+		SessionId:    sid,
 	}
 
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -556,12 +561,12 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u
 
 	err := conn.Transaction(func(tx *storage.Connection) error {
 		var terr error
-		refreshToken, terr = models.GrantAuthenticatedUser(tx, user)
+		refreshToken, terr = models.GrantAuthenticatedUser(tx, user, models.GrantParams{})
 		if terr != nil {
 			return internalServerError("Database error granting user").WithInternalError(terr)
 		}
 
-		tokenString, terr = generateAccessToken(user, refreshToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+		tokenString, terr = generateAccessToken(user, refreshToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
 		if terr != nil {
 			return internalServerError("error generating jwt token").WithInternalError(terr)
 		}
diff --git a/cmd/migrate_cmd.go b/cmd/migrate_cmd.go
index 3b355c9f..469be24e 100644
--- a/cmd/migrate_cmd.go
+++ b/cmd/migrate_cmd.go
@@ -32,7 +32,7 @@ func migrate(cmd *cobra.Command, args []string) {
 		globalConfig.DB.Driver = u.Scheme
 	}
 
-	log := logrus.New()
+	log := logrus.StandardLogger()
 
 	pop.Debug = false
 	if globalConfig.Logging.Level != "" {
diff --git a/conf/configuration.go b/conf/configuration.go
index 38e2ae7a..de25c141 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,22 +255,25 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) {
 		return nil, err
 	}
 
-	config.ApplyDefaults()
+	if err := config.ApplyDefaults(); err != nil {
+		return nil, err
+	}
 
-	if _, err := ConfigureLogging(&config.Logging); err != nil {
+	if err := ConfigureLogging(&config.Logging); err != nil {
+		return nil, err
+	}
+
+	if err := config.Validate(); err != nil {
 		return nil, err
 	}
 
 	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/logging.go b/conf/logging.go
index 831b3af9..e28bd0f5 100644
--- a/conf/logging.go
+++ b/conf/logging.go
@@ -2,11 +2,18 @@ package conf
 
 import (
 	"os"
-	"time"
 
+	"github.com/gobuffalo/pop/v5"
+	"github.com/gobuffalo/pop/v5/logging"
 	"github.com/sirupsen/logrus"
 )
 
+const (
+	LOG_SQL_ALL       = "all"
+	LOG_SQL_NONE      = "none"
+	LOG_SQL_STATEMENT = "statement"
+)
+
 type LoggingConfig struct {
 	Level            string                 `mapstructure:"log_level" json:"log_level"`
 	File             string                 `mapstructure:"log_file" json:"log_file"`
@@ -14,47 +21,78 @@ type LoggingConfig struct {
 	QuoteEmptyFields bool                   `mapstructure:"quote_empty_fields" split_words:"true" json:"quote_empty_fields"`
 	TSFormat         string                 `mapstructure:"ts_format" json:"ts_format"`
 	Fields           map[string]interface{} `mapstructure:"fields" json:"fields"`
+	SQL              string                 `mapstructure:"sql" json:"sql"`
 }
 
-func ConfigureLogging(config *LoggingConfig) (*logrus.Entry, error) {
-	logger := logrus.New()
-
-	tsFormat := time.RFC3339Nano
-	if config.TSFormat != "" {
-		tsFormat = config.TSFormat
-	}
-	// always use the full timestamp
-	logger.SetFormatter(&logrus.TextFormatter{
-		FullTimestamp:    true,
-		DisableTimestamp: false,
-		TimestampFormat:  tsFormat,
-		DisableColors:    config.DisableColors,
-		QuoteEmptyFields: config.QuoteEmptyFields,
-	})
+func ConfigureLogging(config *LoggingConfig) error {
+	logrus.SetFormatter(&logrus.JSONFormatter{})
 
 	// use a file if you want
 	if config.File != "" {
 		f, errOpen := os.OpenFile(config.File, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660) //#nosec G302 -- Log files should be rw-rw-r--
 		if errOpen != nil {
-			return nil, errOpen
+			return errOpen
 		}
-		logger.SetOutput(f)
-		logger.Infof("Set output file to %s", config.File)
+		logrus.SetOutput(f)
+		logrus.Infof("Set output file to %s", config.File)
 	}
 
 	if config.Level != "" {
 		level, err := logrus.ParseLevel(config.Level)
 		if err != nil {
-			return nil, err
+			return err
 		}
-		logger.SetLevel(level)
-		logger.Debug("Set log level to: " + logger.GetLevel().String())
+		logrus.SetLevel(level)
+		logrus.Debug("Set log level to: " + logrus.GetLevel().String())
 	}
 
 	f := logrus.Fields{}
 	for k, v := range config.Fields {
 		f[k] = v
 	}
+	logrus.WithFields(f)
+
+	setPopLogger(config.SQL)
+
+	return nil
+}
+
+func setPopLogger(sql string) {
+	popLog := logrus.WithField("component", "pop")
+	sqlLog := logrus.WithField("component", "sql")
 
-	return logger.WithFields(f), nil
+	shouldLogSQL := sql == LOG_SQL_STATEMENT || sql == LOG_SQL_ALL
+	shouldLogSQLArgs := sql == LOG_SQL_ALL
+
+	pop.SetLogger(func(lvl logging.Level, s string, args ...interface{}) {
+		// Special case SQL logging since we have 2 extra flags to check
+		if lvl == logging.SQL {
+			if !shouldLogSQL {
+				return
+			}
+
+			if shouldLogSQLArgs && len(args) > 0 {
+				sqlLog.WithField("args", args).Info(s)
+			} else {
+				sqlLog.Info(s)
+			}
+			return
+		}
+
+		l := popLog
+		if len(args) > 0 {
+			l = l.WithField("args", args)
+		}
+
+		switch lvl {
+		case logging.Debug:
+			l.Debug(s)
+		case logging.Info:
+			l.Info(s)
+		case logging.Warn:
+			l.Warn(s)
+		case logging.Error:
+			l.Error(s)
+		}
+	})
 }
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/logger/log.go b/logger/log.go
index 8f5c14a9..028cb541 100644
--- a/logger/log.go
+++ b/logger/log.go
@@ -6,13 +6,11 @@ import (
 	"time"
 
 	chimiddleware "github.com/go-chi/chi/middleware"
+	"github.com/netlify/gotrue/utilities"
 	"github.com/sirupsen/logrus"
 )
 
 func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler {
-	logger.Formatter = &logrus.JSONFormatter{
-		DisableTimestamp: true,
-	}
 	return chimiddleware.RequestLogger(&structuredLogger{logger})
 }
 
@@ -26,7 +24,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/mailer/mailer.go b/mailer/mailer.go
index 266944f3..b9d634b4 100644
--- a/mailer/mailer.go
+++ b/mailer/mailer.go
@@ -40,7 +40,7 @@ func NewMailer(instanceConfig *conf.GlobalConfiguration) Mailer {
 			Pass:    instanceConfig.SMTP.Pass,
 			From:    from,
 			BaseURL: instanceConfig.SiteURL,
-			Logger:  logrus.New(),
+			Logger:  logrus.StandardLogger(),
 		}
 	}
 
diff --git a/models/connection.go b/models/connection.go
index 446c58ff..8ffa95e7 100644
--- a/models/connection.go
+++ b/models/connection.go
@@ -43,7 +43,7 @@ func TruncateAll(conn *storage.Connection) error {
 		}
 
 		for _, tableName := range tables {
-			if err := tx.RawQuery("TRUNCATE " + tableName + " CASCADE").Exec(); err != nil {
+			if err := tx.RawQuery("DELETE FROM " + tableName + " CASCADE").Exec(); err != nil {
 				return err
 			}
 		}
diff --git a/models/errors.go b/models/errors.go
index d2b6fe64..773cc852 100644
--- a/models/errors.go
+++ b/models/errors.go
@@ -3,19 +3,19 @@ package models
 // IsNotFoundError returns whether an error represents a "not found" error.
 func IsNotFoundError(err error) bool {
 	switch err.(type) {
-	case UserNotFoundError:
+	case UserNotFoundError, *UserNotFoundError:
 		return true
-	case SessionNotFoundError:
+	case SessionNotFoundError, *SessionNotFoundError:
 		return true
-	case ConfirmationTokenNotFoundError:
+	case ConfirmationTokenNotFoundError, *ConfirmationTokenNotFoundError:
 		return true
-	case RefreshTokenNotFoundError:
+	case RefreshTokenNotFoundError, *RefreshTokenNotFoundError:
 		return true
-	case InstanceNotFoundError:
+	case InstanceNotFoundError, *InstanceNotFoundError:
 		return true
-	case TotpSecretNotFoundError:
+	case TotpSecretNotFoundError, *TotpSecretNotFoundError:
 		return true
-	case IdentityNotFoundError:
+	case IdentityNotFoundError, *IdentityNotFoundError:
 		return true
 	}
 	return false
diff --git a/models/refresh_token.go b/models/refresh_token.go
index 5f1bc463..847244ff 100644
--- a/models/refresh_token.go
+++ b/models/refresh_token.go
@@ -5,7 +5,6 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/gobuffalo/nulls"
 	"github.com/gobuffalo/pop/v5"
 	"github.com/gofrs/uuid"
 	"github.com/netlify/gotrue/crypto"
@@ -22,7 +21,7 @@ type RefreshToken struct {
 	UserID uuid.UUID `db:"user_id"`
 
 	Parent    storage.NullString `db:"parent"`
-	SessionId nulls.UUID         `db:"session_id"`
+	SessionId *uuid.UUID         `db:"session_id"`
 
 	Revoked   bool      `db:"revoked"`
 	CreatedAt time.Time `db:"created_at"`
@@ -36,9 +35,14 @@ func (RefreshToken) TableName() string {
 	return tableName
 }
 
+// GrantParams is used to pass session-specific parameters when issuing a new
+// refresh token to authenticated users.
+type GrantParams struct {
+}
+
 // GrantAuthenticatedUser creates a refresh token for the provided user.
-func GrantAuthenticatedUser(tx *storage.Connection, user *User) (*RefreshToken, error) {
-	return createRefreshToken(tx, user, nil)
+func GrantAuthenticatedUser(tx *storage.Connection, user *User, params GrantParams) (*RefreshToken, error) {
+	return createRefreshToken(tx, user, nil, &params)
 }
 
 // GrantRefreshTokenSwap swaps a refresh token for a new one, revoking the provided token.
@@ -54,7 +58,7 @@ func GrantRefreshTokenSwap(r *http.Request, tx *storage.Connection, user *User,
 		if terr = tx.UpdateOnly(token, "revoked"); terr != nil {
 			return terr
 		}
-		newToken, terr = createRefreshToken(rtx, user, token)
+		newToken, terr = createRefreshToken(rtx, user, token, nil)
 		return terr
 	})
 	return newToken, err
@@ -64,7 +68,7 @@ func GrantRefreshTokenSwap(r *http.Request, tx *storage.Connection, user *User,
 func RevokeTokenFamily(tx *storage.Connection, token *RefreshToken) error {
 	var err error
 	tablename := (&pop.Model{Value: RefreshToken{}}).TableName()
-	if token.SessionId.Valid && len(token.SessionId.UUID) > 0 {
+	if token.SessionId != nil {
 		err = tx.RawQuery(`update `+tablename+` set revoked = true where session_id = ?;`, token.SessionId).Exec()
 	} else {
 		err = tx.RawQuery(`
@@ -94,7 +98,7 @@ func GetValidChildToken(tx *storage.Connection, token *RefreshToken) (*RefreshTo
 	return refreshToken, nil
 }
 
-func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshToken) (*RefreshToken, error) {
+func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshToken, params *GrantParams) (*RefreshToken, error) {
 	token := &RefreshToken{
 		UserID: user.ID,
 		Token:  crypto.SecureToken(),
@@ -103,12 +107,15 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok
 	if oldToken != nil {
 		token.Parent = storage.NullString(oldToken.Token)
 		token.SessionId = oldToken.SessionId
-	} else {
+	}
+
+	if token.SessionId == nil {
+		// Existing refresh tokens may have a null session_id if they were created before v2.15.3
 		session, err := CreateSession(tx, user)
 		if err != nil {
 			return nil, errors.Wrap(err, "Error generated unique session id")
 		}
-		token.SessionId = nulls.NewUUID(session.ID)
+		token.SessionId = &session.ID
 	}
 
 	if err := tx.Create(token); err != 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/storage/dial.go b/storage/dial.go
index 2a78d3d1..47c3af60 100644
--- a/storage/dial.go
+++ b/storage/dial.go
@@ -41,10 +41,6 @@ func Dial(config *conf.GlobalConfiguration) (*Connection, error) {
 	if err := db.Open(); err != nil {
 		return nil, errors.Wrap(err, "checking database connection")
 	}
-	if logrus.StandardLogger().Level == logrus.DebugLevel {
-		pop.Debug = true
-	}
-
 	return &Connection{db}, nil
 }
 
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/.github/workflows/test.yml b/.github/workflows/test.yml
index 6331afc0..4e35d79b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,6 +7,7 @@ on:
       - master
       - mfa
       - saml
+      - zerosessionidfix
     tags: ['*']
 
 jobs:
diff --git a/api/audit_test.go b/api/audit_test.go
index ad868de3..0c65c565 100644
--- a/api/audit_test.go
+++ b/api/audit_test.go
@@ -48,7 +48,7 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string {
 
 	u.Role = "supabase_admin"
 
-	token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err, "Error generating access token")
 
 	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
diff --git a/api/auth_test.go b/api/auth_test.go
new file mode 100644
index 00000000..24a9995f
--- /dev/null
+++ b/api/auth_test.go
@@ -0,0 +1,168 @@
+package api
+
+import (
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/gofrs/uuid"
+	jwt "github.com/golang-jwt/jwt"
+	"github.com/netlify/gotrue/conf"
+	"github.com/netlify/gotrue/models"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+)
+
+type AuthTestSuite struct {
+	suite.Suite
+	API    *API
+	Config *conf.GlobalConfiguration
+}
+
+func TestAuth(t *testing.T) {
+	api, config, err := setupAPIForTest()
+	require.NoError(t, err)
+
+	ts := &AuthTestSuite{
+		API:    api,
+		Config: config,
+	}
+	defer api.db.Close()
+	suite.Run(t, ts)
+}
+
+func (ts *AuthTestSuite) SetupTest() {
+	models.TruncateAll(ts.API.db)
+
+	// Create user
+	u, err := models.NewUser("", "test@example.com", "password", ts.Config.JWT.Aud, nil)
+	require.NoError(ts.T(), err, "Error creating test user model")
+	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
+}
+
+func (ts *AuthTestSuite) TestExtractBearerToken() {
+	userClaims := &GoTrueClaims{
+		Role: "authenticated",
+	}
+	userJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims).SignedString([]byte(ts.Config.JWT.Secret))
+	require.NoError(ts.T(), err)
+	req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
+	req.Header.Set("Authorization", "Bearer "+userJwt)
+
+	token, err := ts.API.extractBearerToken(req)
+	require.NoError(ts.T(), err)
+	require.Equal(ts.T(), userJwt, token)
+}
+
+func (ts *AuthTestSuite) TestParseJWTClaims() {
+	userClaims := &GoTrueClaims{
+		Role: "authenticated",
+	}
+	userJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims).SignedString([]byte(ts.Config.JWT.Secret))
+	require.NoError(ts.T(), err)
+
+	req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
+	req.Header.Set("Authorization", "Bearer "+userJwt)
+	w := httptest.NewRecorder()
+	ctx, err := ts.API.parseJWTClaims(userJwt, req, w)
+	require.NoError(ts.T(), err)
+
+	// check if token is stored in context
+	token := getToken(ctx)
+	require.Equal(ts.T(), userJwt, token.Raw)
+}
+
+func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
+	u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
+	require.NoError(ts.T(), err)
+
+	s, err := models.CreateSession(ts.API.db, u)
+	require.NoError(ts.T(), err)
+
+	cases := []struct {
+		Desc            string
+		UserJwtClaims   *GoTrueClaims
+		ExpectedError   error
+		ExpectedUser    *models.User
+		ExpectedSession *models.Session
+	}{
+		{
+			Desc: "Missing Subject Claim",
+			UserJwtClaims: &GoTrueClaims{
+				StandardClaims: jwt.StandardClaims{
+					Subject: "",
+				},
+				Role: "authenticated",
+			},
+			ExpectedError: errors.New("invalid claim: subject missing"),
+			ExpectedUser:  nil,
+		},
+		{
+			Desc: "Valid Subject Claim",
+			UserJwtClaims: &GoTrueClaims{
+				StandardClaims: jwt.StandardClaims{
+					Subject: u.ID.String(),
+				},
+				Role: "authenticated",
+			},
+			ExpectedError: nil,
+			ExpectedUser:  u,
+		},
+		{
+			Desc: "Empty Session ID Claim",
+			UserJwtClaims: &GoTrueClaims{
+				StandardClaims: jwt.StandardClaims{
+					Subject: u.ID.String(),
+				},
+				Role:      "authenticated",
+				SessionId: "",
+			},
+			ExpectedError: nil,
+			ExpectedUser:  u,
+		},
+		{
+			Desc: "Invalid Session ID Claim",
+			UserJwtClaims: &GoTrueClaims{
+				StandardClaims: jwt.StandardClaims{
+					Subject: u.ID.String(),
+				},
+				Role:      "authenticated",
+				SessionId: uuid.Nil.String(),
+			},
+			ExpectedError: nil,
+			ExpectedUser:  u,
+		},
+		{
+			Desc: "Valid Session ID Claim",
+			UserJwtClaims: &GoTrueClaims{
+				StandardClaims: jwt.StandardClaims{
+					Subject: u.ID.String(),
+				},
+				Role:      "authenticated",
+				SessionId: s.ID.String(),
+			},
+			ExpectedError:   nil,
+			ExpectedUser:    u,
+			ExpectedSession: s,
+		},
+	}
+
+	for _, c := range cases {
+		ts.Run(c.Desc, func() {
+			userJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, c.UserJwtClaims).SignedString([]byte(ts.Config.JWT.Secret))
+			require.NoError(ts.T(), err)
+
+			req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
+			req.Header.Set("Authorization", "Bearer "+userJwt)
+
+			w := httptest.NewRecorder()
+			ctx, err := ts.API.parseJWTClaims(userJwt, req, w)
+			require.NoError(ts.T(), err)
+			ctx, err = ts.API.maybeLoadUserOrSession(ctx)
+			require.Equal(ts.T(), c.ExpectedError, err)
+			require.Equal(ts.T(), c.ExpectedUser, getUser(ctx))
+			require.Equal(ts.T(), c.ExpectedSession, getSession(ctx))
+		})
+	}
+}
diff --git a/api/invite_test.go b/api/invite_test.go
index 52a8e092..70cccea3 100644
--- a/api/invite_test.go
+++ b/api/invite_test.go
@@ -58,7 +58,7 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string {
 
 	u.Role = "supabase_admin"
 
-	token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err, "Error generating access token")
 
 	p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
diff --git a/api/otp_test.go b/api/otp_test.go
index db240282..8154459c 100644
--- a/api/otp_test.go
+++ b/api/otp_test.go
@@ -51,7 +51,7 @@ func (ts *OtpTestSuite) TestOtp() {
 			OtpParams{
 				Email:      "test@example.com",
 				CreateUser: true,
-				Metadata: map[string]interface{}{
+				Data: map[string]interface{}{
 					"somedata": "metadata",
 				},
 			},
diff --git a/api/phone_test.go b/api/phone_test.go
index 5f5f9cc6..31434f97 100644
--- a/api/phone_test.go
+++ b/api/phone_test.go
@@ -126,7 +126,7 @@ func (ts *PhoneTestSuite) TestMissingSmsProviderConfig() {
 	u.PhoneConfirmedAt = &now
 	require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")
 
-	token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err)
 
 	cases := []struct {
diff --git a/api/token_test.go b/api/token_test.go
index 3ff75803..c8d36377 100644
--- a/api/token_test.go
+++ b/api/token_test.go
@@ -50,7 +50,7 @@ func (ts *TokenTestSuite) SetupTest() {
 	u.BannedUntil = nil
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
 
-	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u)
+	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{})
 	require.NoError(ts.T(), err, "Error creating refresh token")
 }
 
@@ -153,7 +153,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() {
 	t := time.Now()
 	u.EmailConfirmedAt = &t
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user")
-	first, err := models.GrantAuthenticatedUser(ts.API.db, u)
+	first, err := models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{})
 	require.NoError(ts.T(), err)
 	second, err := models.GrantRefreshTokenSwap(&http.Request{}, ts.API.db, u, first)
 	require.NoError(ts.T(), err)
@@ -245,7 +245,7 @@ func (ts *TokenTestSuite) createBannedUser() *models.User {
 	u.BannedUntil = &t
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test banned user")
 
-	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u)
+	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{})
 	require.NoError(ts.T(), err, "Error creating refresh token")
 
 	return u
diff --git a/api/user_test.go b/api/user_test.go
index 83163bd1..8a0d5fc9 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -47,7 +47,7 @@ func (ts *UserTestSuite) SetupTest() {
 func (ts *UserTestSuite) TestUserGet() {
 	u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
 	require.NoError(ts.T(), err, "Error finding user")
-	token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err, "Error generating access token")
 
 	req := httptest.NewRequest(http.MethodGet, "http://localhost/user", nil)
@@ -111,7 +111,7 @@ func (ts *UserTestSuite) TestUserUpdateEmail() {
 			require.NoError(ts.T(), u.SetPhone(ts.API.db, c.userData["phone"]), "Error setting user phone")
 			require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user")
 
-			token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 			require.NoError(ts.T(), err, "Error generating access token")
 
 			var buffer bytes.Buffer
@@ -170,7 +170,7 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() {
 
 	for _, c := range cases {
 		ts.Run(c.desc, func() {
-			token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 			require.NoError(ts.T(), err, "Error generating access token")
 
 			var buffer bytes.Buffer
@@ -244,7 +244,7 @@ func (ts *UserTestSuite) TestUserUpdatePassword() {
 			req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer)
 			req.Header.Set("Content-Type", "application/json")
 
-			token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 			require.NoError(ts.T(), err)
 			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 
@@ -272,7 +272,7 @@ func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() {
 	u.EmailConfirmedAt = &now
 	require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")
 
-	token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err)
 
 	// request for reauthentication nonce
diff --git a/api/verify_test.go b/api/verify_test.go
index 3b17a75c..8482a1ef 100644
--- a/api/verify_test.go
+++ b/api/verify_test.go
@@ -103,7 +103,7 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() {
 	req.Header.Set("Content-Type", "application/json")
 
 	// Generate access token for request
-	token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err)
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 
diff --git a/hack/test.env b/hack/test.env
index f76624d6..f076a5a8 100644
--- a/hack/test.env
+++ b/hack/test.env
@@ -9,7 +9,8 @@ GOTRUE_DB_AUTOMIGRATE=true
 DATABASE_URL="postgres://supabase_auth_admin:root@localhost:5432/postgres"
 GOTRUE_API_HOST=localhost
 PORT=9999
-GOTRUE_LOG_LEVEL=debug
+GOTRUE_LOG_SQL=none
+GOTRUE_LOG_LEVEL=warn
 GOTRUE_SITE_URL=https://example.netlify.com
 GOTRUE_URI_ALLOW_LIST="http://localhost:3000"
 GOTRUE_OPERATOR_TOKEN=foobar
diff --git a/models/refresh_token_test.go b/models/refresh_token_test.go
index cbf32af2..6da738ba 100644
--- a/models/refresh_token_test.go
+++ b/models/refresh_token_test.go
@@ -37,7 +37,7 @@ func TestRefreshToken(t *testing.T) {
 
 func (ts *RefreshTokenTestSuite) TestGrantAuthenticatedUser() {
 	u := ts.createUser()
-	r, err := GrantAuthenticatedUser(ts.db, u)
+	r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{})
 	require.NoError(ts.T(), err)
 
 	require.NotEmpty(ts.T(), r.Token)
@@ -46,7 +46,7 @@ func (ts *RefreshTokenTestSuite) TestGrantAuthenticatedUser() {
 
 func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() {
 	u := ts.createUser()
-	r, err := GrantAuthenticatedUser(ts.db, u)
+	r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{})
 	require.NoError(ts.T(), err)
 
 	s, err := GrantRefreshTokenSwap(&http.Request{}, ts.db, u, r)
@@ -64,7 +64,7 @@ func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() {
 
 func (ts *RefreshTokenTestSuite) TestLogout() {
 	u := ts.createUser()
-	r, err := GrantAuthenticatedUser(ts.db, u)
+	r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{})
 	require.NoError(ts.T(), err)
 
 	require.NoError(ts.T(), Logout(ts.db, u.ID))
diff --git a/models/user_test.go b/models/user_test.go
index 8ff308a4..eb75a599 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -148,7 +148,7 @@ func (ts *UserTestSuite) TestFindUserByRecoveryToken() {
 
 func (ts *UserTestSuite) TestFindUserWithRefreshToken() {
 	u := ts.createUser()
-	r, err := GrantAuthenticatedUser(ts.db, u)
+	r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{})
 	require.NoError(ts.T(), err)
 
 	n, nr, err := FindUserWithRefreshToken(ts.db, r.Token)
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)
+	}
+}