Repository: supabase/gotrue
Pull request #827 (2022-11-21 08:30:37)

fix: make add_mfa_indexes re-runnable
## What kind of change does this PR introduce?

Noticed while doing support that `amr_id_pk` does not need currently check if the index already exists. As such, if the migration is re-run it will lead to an error.

## Additional context

See Internal support ticket


Patch:
diff --git a/api/api.go b/api/api.go
index 0330d40b..07f2081a 100644
--- a/api/api.go
+++ b/api/api.go
@@ -134,25 +134,23 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 			r.With(sharedLimiter).Put("/", api.UserUpdate)
 		})
 
-		if api.config.MFA.Enabled {
-			r.With(api.requireAuthentication).Route("/factors", func(r *router) {
-				r.Post("/", api.EnrollFactor)
-				r.Route("/{factor_id}", func(r *router) {
-					r.Use(api.loadFactor)
+		r.With(api.requireAuthentication).Route("/factors", func(r *router) {
+			r.Post("/", api.EnrollFactor)
+			r.Route("/{factor_id}", func(r *router) {
+				r.Use(api.loadFactor)
 
-					r.With(api.limitHandler(
-						tollbooth.NewLimiter(api.config.MFA.RateLimitChallengeAndVerify/60, &limiter.ExpirableOptions{
-							DefaultExpirationTTL: time.Minute,
-						}).SetBurst(30))).Post("/verify", api.VerifyFactor)
-					r.With(api.limitHandler(
-						tollbooth.NewLimiter(api.config.MFA.RateLimitChallengeAndVerify/60, &limiter.ExpirableOptions{
-							DefaultExpirationTTL: time.Minute,
-						}).SetBurst(30))).Post("/challenge", api.ChallengeFactor)
-					r.Delete("/", api.UnenrollFactor)
+				r.With(api.limitHandler(
+					tollbooth.NewLimiter(api.config.MFA.RateLimitChallengeAndVerify/60, &limiter.ExpirableOptions{
+						DefaultExpirationTTL: time.Minute,
+					}).SetBurst(30))).Post("/verify", api.VerifyFactor)
+				r.With(api.limitHandler(
+					tollbooth.NewLimiter(api.config.MFA.RateLimitChallengeAndVerify/60, &limiter.ExpirableOptions{
+						DefaultExpirationTTL: time.Minute,
+					}).SetBurst(30))).Post("/challenge", api.ChallengeFactor)
+				r.Delete("/", api.UnenrollFactor)
 
-				})
 			})
-		}
+		})
 
 		if api.config.SAML.Enabled {
 			r.Route("/sso", func(r *router) {
@@ -189,16 +187,14 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 
 				r.Route("/{user_id}", func(r *router) {
 					r.Use(api.loadUser)
-					if api.config.MFA.Enabled {
-						r.Route("/factors", func(r *router) {
-							r.Get("/", api.adminUserGetFactors)
-							r.Route("/{factor_id}", func(r *router) {
-								r.Use(api.loadFactor)
-								r.Delete("/", api.adminUserDeleteFactor)
-								r.Put("/", api.adminUserUpdateFactor)
-							})
+					r.Route("/factors", func(r *router) {
+						r.Get("/", api.adminUserGetFactors)
+						r.Route("/{factor_id}", func(r *router) {
+							r.Use(api.loadFactor)
+							r.Delete("/", api.adminUserDeleteFactor)
+							r.Put("/", api.adminUserUpdateFactor)
 						})
-					}
+					})
 
 					r.Get("/", api.adminUserGet)
 					r.Put("/", api.adminUserUpdate)
diff --git a/api/auth.go b/api/auth.go
index 3b13fdfe..eeec1f0f 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -21,13 +21,15 @@ func (a *API) requireAuthentication(w http.ResponseWriter, r *http.Request) (con
 		return nil, err
 	}
 
-	ctx, err := a.parseJWTClaims(token, r, w)
+	ctx, err := a.parseJWTClaims(token, r)
 	if err != nil {
+		a.clearCookieTokens(config, w)
 		return ctx, err
 	}
 
 	ctx, err = a.maybeLoadUserOrSession(ctx)
 	if err != nil {
+		a.clearCookieTokens(config, w)
 		return ctx, err
 	}
 	return ctx, err
@@ -62,7 +64,7 @@ func (a *API) extractBearerToken(r *http.Request) (string, error) {
 	return matches[1], nil
 }
 
-func (a *API) parseJWTClaims(bearer string, r *http.Request, w http.ResponseWriter) (context.Context, error) {
+func (a *API) parseJWTClaims(bearer string, r *http.Request) (context.Context, error) {
 	ctx := r.Context()
 	config := a.config
 
@@ -71,7 +73,6 @@ func (a *API) parseJWTClaims(bearer string, r *http.Request, w http.ResponseWrit
 		return []byte(config.JWT.Secret), nil
 	})
 	if err != nil {
-		a.clearCookieTokens(config, w)
 		return nil, unauthorizedError("invalid JWT: unable to parse or verify signature, %v", err)
 	}
 
@@ -112,7 +113,7 @@ func (a *API) maybeLoadUserOrSession(ctx context.Context) (context.Context, erro
 		if err != nil {
 			return ctx, err
 		}
-		session, err = models.FindSessionById(db, sessionId)
+		session, err = models.FindSessionByID(db, sessionId)
 		if err != nil && !models.IsNotFoundError(err) {
 			return ctx, err
 		}
diff --git a/api/external.go b/api/external.go
index 754ec97e..09ae5984 100644
--- a/api/external.go
+++ b/api/external.go
@@ -166,11 +166,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 				return terr
 			}
 		}
-		if config.MFA.Enabled {
-			token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.OAuth, grantParams)
-		} else {
-			token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
-		}
+		token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth, grantParams)
 
 		if terr != nil {
 			return oauthError("server_error", terr.Error())
diff --git a/api/middleware.go b/api/middleware.go
index 51b46218..b132c0f3 100644
--- a/api/middleware.go
+++ b/api/middleware.go
@@ -116,8 +116,9 @@ func (a *API) requireAdminCredentials(w http.ResponseWriter, req *http.Request)
 		return nil, err
 	}
 
-	ctx, err := a.parseJWTClaims(t, req, w)
+	ctx, err := a.parseJWTClaims(t, req)
 	if err != nil {
+		a.clearCookieTokens(a.config, w)
 		return nil, err
 	}
 
diff --git a/api/samlacs.go b/api/samlacs.go
index 59502058..c6f7827c 100644
--- a/api/samlacs.go
+++ b/api/samlacs.go
@@ -228,6 +228,14 @@ func (a *API) SAMLACS(w http.ResponseWriter, r *http.Request) error {
 	// refreshTokenParams.NotBefore = assertion.NotBefore()
 	// refreshTokenParams.NotAfter = assertion.NotAfter()
 
+	notAfter := assertion.NotAfter()
+
+	var grantParams models.GrantParams
+
+	if !notAfter.IsZero() {
+		grantParams.SessionNotAfter = &notAfter
+	}
+
 	var token *AccessTokenResponse
 
 	if err := db.Transaction(func(tx *storage.Connection) error {
@@ -238,11 +246,7 @@ func (a *API) SAMLACS(w http.ResponseWriter, r *http.Request) error {
 			return terr
 		}
 
-		if config.MFA.Enabled {
-			token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.SSOSAML, models.GrantParams{})
-		} else {
-			token, terr = a.issueRefreshToken(ctx, tx, user, models.GrantParams{})
-		}
+		token, terr = a.issueRefreshToken(ctx, tx, user, models.SSOSAML, grantParams)
 
 		if terr != nil {
 			return internalServerError("Unable to issue refresh token from SAML Assertion").WithInternalError(terr)
diff --git a/api/signup.go b/api/signup.go
index f44e1d16..00cc65a1 100644
--- a/api/signup.go
+++ b/api/signup.go
@@ -104,9 +104,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 				return UserExistsError
 			}
 
-			if err := user.UpdateUserMetaData(tx, params.Data); err != nil {
-				return internalServerError("Database error updating user").WithInternalError(err)
-			}
+			// do not update the user because we can't be sure of their claimed identity
 		} else {
 			user, terr = a.signupNewUser(ctx, tx, params)
 			if terr != nil {
@@ -225,12 +223,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 			if terr = triggerEventHooks(ctx, tx, LoginEvent, user, config); terr != nil {
 				return terr
 			}
-
-			if config.MFA.Enabled {
-				token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.PasswordGrant, grantParams)
-			} else {
-				token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
-			}
+			token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant, grantParams)
 
 			if terr != nil {
 				return terr
diff --git a/api/ssoadmin.go b/api/ssoadmin.go
index d8d877b2..7bacf136 100644
--- a/api/ssoadmin.go
+++ b/api/ssoadmin.go
@@ -354,8 +354,8 @@ func (a *API) adminSSOProvidersUpdate(w http.ResponseWriter, r *http.Request) er
 		modified = true
 	}
 
-	var createDomains []string
-	keepDomains := make(map[string]bool)
+	var createDomains []models.SSODomain
+	keepProviders := make(map[string]bool)
 
 	for _, domain := range params.Domains {
 		existingProvider, err := models.FindSSOProviderByDomain(db, domain)
@@ -364,43 +364,57 @@ func (a *API) adminSSOProvidersUpdate(w http.ResponseWriter, r *http.Request) er
 		}
 		if existingProvider != nil {
 			if existingProvider.ID == provider.ID {
-				keepDomains[domain] = true
+				keepProviders[domain] = true
 			} else {
 				return badRequestError("SSO domain '%s' already assigned to another provider (%s)", domain, existingProvider.ID.String())
 			}
 		} else {
-			createDomains = append(createDomains, domain)
+			modified = true
+			createDomains = append(createDomains, models.SSODomain{
+				Domain:        domain,
+				SSOProviderID: provider.ID,
+			})
 		}
 	}
 
-	var updatedDomains []models.SSODomain
+	var deleteDomains []models.SSODomain
 
 	for _, domain := range provider.SSODomains {
-		if keepDomains[domain.Domain] {
-			updatedDomains = append(updatedDomains, domain)
+		if !keepProviders[domain.Domain] {
+			modified = true
+			deleteDomains = append(deleteDomains, domain)
 		}
 	}
 
-	for _, domain := range createDomains {
-		updatedDomains = append(updatedDomains, models.SSODomain{
-			Domain: domain,
-		})
-	}
-
-	modified = modified || len(createDomains) > 0
-
-	provider.SSODomains = updatedDomains
-
-	if !provider.SAMLProvider.AttributeMapping.Equal(&params.AttributeMapping) {
+	updateAttributeMapping := !provider.SAMLProvider.AttributeMapping.Equal(&params.AttributeMapping)
+	if updateAttributeMapping {
 		modified = true
 		provider.SAMLProvider.AttributeMapping = params.AttributeMapping
 	}
 
 	if modified {
 		if err := db.Transaction(func(tx *storage.Connection) error {
-			return tx.Eager().Update(provider)
+			if terr := tx.Eager().Update(provider); terr != nil {
+				return terr
+			}
+
+			if terr := tx.Destroy(deleteDomains); terr != nil {
+				return terr
+			}
+
+			if terr := tx.Eager().Create(createDomains); terr != nil {
+				return terr
+			}
+
+			if updateAttributeMapping {
+				if terr := tx.Eager().Update(&provider.SAMLProvider); terr != nil {
+					return terr
+				}
+			}
+
+			return tx.Eager().Load(provider)
 		}); err != nil {
-			return err
+			return unprocessableEntityError("Updating SSO provider failed, likely due to a conflict. Try again?").WithInternalError(err)
 		}
 	}
 
diff --git a/api/token.go b/api/token.go
index c6fe3737..142c0d18 100644
--- a/api/token.go
+++ b/api/token.go
@@ -238,11 +238,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 		if terr = triggerEventHooks(ctx, tx, LoginEvent, user, config); terr != nil {
 			return terr
 		}
-		if config.MFA.Enabled {
-			token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.PasswordGrant, grantParams)
-		} else {
-			token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
-		}
+		token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant, grantParams)
 
 		if terr != nil {
 			return terr
@@ -280,16 +276,28 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 		return oauthError("invalid_request", "refresh_token required")
 	}
 
-	user, token, err := models.FindUserWithRefreshToken(db, params.RefreshToken)
+	user, token, session, err := models.FindUserWithRefreshToken(db, params.RefreshToken)
 	if err != nil {
 		if models.IsNotFoundError(err) {
-			return oauthError("invalid_grant", "Invalid Refresh Token")
+			return oauthError("invalid_grant", "Invalid Refresh Token: Refresh Token Not Found")
 		}
 		return internalServerError(err.Error())
 	}
 
 	if user.IsBanned() {
-		return oauthError("invalid_grant", "Invalid Refresh Token")
+		return oauthError("invalid_grant", "Invalid Refresh Token: User Banned")
+	}
+
+	if session != nil {
+		var notAfter time.Time
+
+		if session.NotAfter != nil {
+			notAfter = *session.NotAfter
+		}
+
+		if !notAfter.IsZero() && time.Now().UTC().After(notAfter) {
+			return oauthError("invalid_grant", "Invalid Refresh Token: Session Expired")
+		}
 	}
 
 	var newToken *models.RefreshToken
@@ -350,11 +358,8 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 				return internalServerError(terr.Error())
 			}
 		}
-		if config.MFA.Enabled {
-			tokenString, terr = MFA_generateAccessToken(tx, user, newToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
-		} else {
-			tokenString, terr = generateAccessToken(user, newToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
-		}
+		tokenString, terr = generateAccessToken(tx, user, newToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+
 		if terr != nil {
 			return internalServerError("error generating jwt token").WithInternalError(terr)
 		}
@@ -537,12 +542,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R
 				return terr
 			}
 		}
-		if config.MFA.Enabled {
-			token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.OAuth, grantParams)
-		} else {
-			token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
-
-		}
+		token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth, grantParams)
 
 		if terr != nil {
 			return oauthError("server_error", terr.Error())
@@ -562,35 +562,12 @@ 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 *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(),
-			Audience:  user.Aud,
-			ExpiresAt: time.Now().Add(expiresIn).Unix(),
-		},
-		Email:        user.GetEmail(),
-		Phone:        user.GetPhone(),
-		AppMetaData:  user.AppMetaData,
-		UserMetaData: user.UserMetaData,
-		Role:         user.Role,
-		SessionId:    sid,
-	}
-
-	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
-	return token.SignedString([]byte(secret))
-}
-
-func MFA_generateAccessToken(tx *storage.Connection, user *models.User, sessionId *uuid.UUID, expiresIn time.Duration, secret string) (string, error) {
+func generateAccessToken(tx *storage.Connection, user *models.User, sessionId *uuid.UUID, expiresIn time.Duration, secret string) (string, error) {
 	aal, amr := models.AAL1.String(), []models.AMREntry{}
 	sid := ""
 	if sessionId != nil {
 		sid = sessionId.String()
-		session, terr := models.FindSessionById(tx, *sessionId)
+		session, terr := models.FindSessionByID(tx, *sessionId)
 		if terr != nil {
 			return "", terr
 		}
@@ -617,42 +594,7 @@ func MFA_generateAccessToken(tx *storage.Connection, user *models.User, sessionI
 	return token.SignedString([]byte(secret))
 }
 
-func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, grantParams models.GrantParams) (*AccessTokenResponse, error) {
-	config := a.config
-
-	now := time.Now()
-	user.LastSignInAt = &now
-
-	var tokenString string
-	var refreshToken *models.RefreshToken
-
-	err := conn.Transaction(func(tx *storage.Connection) error {
-		var terr error
-		refreshToken, terr = models.GrantAuthenticatedUser(tx, user, grantParams)
-		if terr != nil {
-			return internalServerError("Database error granting user").WithInternalError(terr)
-		}
-
-		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)
-		}
-		return nil
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	return &AccessTokenResponse{
-		Token:        tokenString,
-		TokenType:    "bearer",
-		ExpiresIn:    config.JWT.Exp,
-		RefreshToken: refreshToken.Token,
-		User:         user,
-	}, nil
-}
-
-func (a *API) MFA_issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, authenticationMethod models.AuthenticationMethod, grantParams models.GrantParams) (*AccessTokenResponse, error) {
+func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, authenticationMethod models.AuthenticationMethod, grantParams models.GrantParams) (*AccessTokenResponse, error) {
 	config := a.config
 
 	now := time.Now()
@@ -669,7 +611,7 @@ func (a *API) MFA_issueRefreshToken(ctx context.Context, conn *storage.Connectio
 			return internalServerError("Database error granting user").WithInternalError(terr)
 		}
 
-		session, terr := models.FindSessionById(tx, *refreshToken.SessionId)
+		session, terr := models.FindSessionByID(tx, *refreshToken.SessionId)
 		if terr != nil {
 			return terr
 		}
@@ -679,7 +621,7 @@ func (a *API) MFA_issueRefreshToken(ctx context.Context, conn *storage.Connectio
 		}
 
 		// TODO(Joel): Replace when feature flag is lifted
-		tokenString, terr = MFA_generateAccessToken(tx, user, refreshToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+		tokenString, terr = generateAccessToken(tx, user, refreshToken.SessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
 		if terr != nil {
 			return internalServerError("error generating jwt token").WithInternalError(terr)
 		}
@@ -709,7 +651,7 @@ func (a *API) updateMFASessionAndClaims(r *http.Request, tx *storage.Connection,
 		return nil, internalServerError("Cannot read SessionId claim as UUID").WithInternalError(err)
 	}
 	err = tx.Transaction(func(tx *storage.Connection) error {
-		session, terr := models.FindSessionById(tx, sessionId)
+		session, terr := models.FindSessionByID(tx, sessionId)
 		if terr != nil {
 			return terr
 		}
@@ -717,7 +659,7 @@ func (a *API) updateMFASessionAndClaims(r *http.Request, tx *storage.Connection,
 		if terr != nil {
 			return terr
 		}
-		session, terr = models.FindSessionById(tx, sessionId)
+		session, terr = models.FindSessionByID(tx, sessionId)
 		if terr != nil {
 			return terr
 		}
@@ -738,8 +680,7 @@ func (a *API) updateMFASessionAndClaims(r *http.Request, tx *storage.Connection,
 			return err
 		}
 
-		// TODO(Joel): Replace when feature flag is lifted
-		tokenString, terr = MFA_generateAccessToken(tx, user, &sessionId, time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret)
+		tokenString, terr = generateAccessToken(tx, user, &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/api/verify.go b/api/verify.go
index 8448d83a..0750f670 100644
--- a/api/verify.go
+++ b/api/verify.go
@@ -123,11 +123,8 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error {
 			return terr
 		}
 
-		if config.MFA.Enabled {
-			token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.OTP, grantParams)
-		} else {
-			token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
-		}
+		token, terr = a.issueRefreshToken(ctx, tx, user, models.OTP, grantParams)
+
 		if terr != nil {
 			return terr
 		}
@@ -222,12 +219,7 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error {
 		if terr != nil {
 			return terr
 		}
-		if config.MFA.Enabled {
-			token, terr = a.MFA_issueRefreshToken(ctx, tx, user, models.OTP, grantParams)
-		} else {
-			token, terr = a.issueRefreshToken(ctx, tx, user, grantParams)
-
-		}
+		token, terr = a.issueRefreshToken(ctx, tx, user, models.OTP, grantParams)
 		if terr != nil {
 			return terr
 		}
diff --git a/migrations/20221003041349_add_mfa_schema.up.sql b/migrations/20221003041349_add_mfa_schema.up.sql
index 49851f52..88c6421c 100644
--- a/migrations/20221003041349_add_mfa_schema.up.sql
+++ b/migrations/20221003041349_add_mfa_schema.up.sql
@@ -22,7 +22,7 @@ create table if not exists {{ index .Options "Namespace" }}.mfa_factors(
 );
 comment on table {{ index .Options "Namespace" }}.mfa_factors is 'auth: stores metadata about factors';
 
-create unique index mfa_factors_user_friendly_name_unique on {{ index .Options "Namespace" }}.mfa_factors (friendly_name, user_id) where trim(friendly_name) <> '';
+create unique index if not exists mfa_factors_user_friendly_name_unique on {{ index .Options "Namespace" }}.mfa_factors (friendly_name, user_id) where trim(friendly_name) <> '';
 
 -- auth.mfa_challenges definition
 create table if not exists {{ index .Options "Namespace" }}.mfa_challenges(
diff --git a/migrations/20221011041400_add_mfa_indexes.up.sql b/migrations/20221011041400_add_mfa_indexes.up.sql
index cecdcba5..25d3a863 100644
--- a/migrations/20221011041400_add_mfa_indexes.up.sql
+++ b/migrations/20221011041400_add_mfa_indexes.up.sql
@@ -1,8 +1,17 @@
-create index if not exists refresh_token_session_id on {{ index .Options "Namespace" }}.refresh_tokens using btree(session_id);
-
 alter table {{ index .Options "Namespace" }}.mfa_amr_claims
-  add column if not exists id uuid not null,
-  add constraint amr_id_pk primary key(id);
+  add column if not exists id uuid not null;
+
+do $$
+begin
+  if not exists
+     (select constraint_name
+      from information_schema.check_constraints
+      where constraint_schema = '{{ index .Options "Namespace" }}'
+      and constraint_name = 'amr_id_pk')
+  then
+    alter table {{ index .Options "Namespace" }}.mfa_amr_claims add constraint amr_id_pk primary key(id);
+  end if;
+end $$;
 
 create index if not exists user_id_created_at_idx on {{ index .Options "Namespace" }}.sessions (user_id, created_at);
 create index if not exists factor_id_created_at_idx on {{ index .Options "Namespace" }}.mfa_factors (user_id, created_at);
diff --git a/models/refresh_token.go b/models/refresh_token.go
index b657112f..b86e5067 100644
--- a/models/refresh_token.go
+++ b/models/refresh_token.go
@@ -3,7 +3,6 @@ package models
 import (
 	"database/sql"
 	"net/http"
-	"os"
 	"time"
 
 	"github.com/gobuffalo/pop/v5"
@@ -40,6 +39,8 @@ func (RefreshToken) TableName() string {
 // refresh token to authenticated users.
 type GrantParams struct {
 	FactorID *uuid.UUID
+
+	SessionNotAfter *time.Time
 }
 
 // GrantAuthenticatedUser creates a refresh token for the provided user.
@@ -126,18 +127,25 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok
 	}
 
 	if token.SessionId == nil {
-		// Existing refresh tokens may have a null session_id if they were created before v2.15.3
-		var session *Session
-		var err error
-		// TODO(Joel): Find better workaround
-		if os.Getenv("GOTRUE_MFA_ENABLED") == "true" {
-			session, err = MFA_CreateSession(tx, user, params.FactorID)
-		} else {
-			session, err = CreateSession(tx, user)
-		}
+		session, err := NewSession()
 		if err != nil {
-			return nil, errors.Wrap(err, "Error generated unique session id")
+			return nil, errors.Wrap(err, "error instantiating new session object")
+		}
+
+		session.UserID = user.ID
+
+		if params.FactorID != nil {
+			session.FactorID = params.FactorID
 		}
+
+		if params.SessionNotAfter != nil {
+			session.NotAfter = params.SessionNotAfter
+		}
+
+		if err := tx.Create(session); err != nil {
+			return nil, errors.Wrap(err, "error creating new session")
+		}
+
 		token.SessionId = &session.ID
 	}
 
diff --git a/models/sessions.go b/models/sessions.go
index c1649c1d..aebf853c 100644
--- a/models/sessions.go
+++ b/models/sessions.go
@@ -70,7 +70,7 @@ func (Session) TableName() string {
 	return tableName
 }
 
-func NewSession(user *User, factorID *uuid.UUID) (*Session, error) {
+func NewSession() (*Session, error) {
 	id, err := uuid.NewV4()
 	if err != nil {
 		return nil, errors.Wrap(err, "Error generating unique session id")
@@ -79,38 +79,14 @@ func NewSession(user *User, factorID *uuid.UUID) (*Session, error) {
 	defaultAAL := AAL1.String()
 
 	session := &Session{
-		ID:        id,
-		UserID:    user.ID,
-		FactorID:  factorID,
-		AAL:       &defaultAAL,
-		AMRClaims: []AMRClaim{},
+		ID:  id,
+		AAL: &defaultAAL,
 	}
-	return session, nil
-}
 
-func CreateSession(tx *storage.Connection, user *User) (*Session, error) {
-	session, err := NewSession(user, &uuid.Nil)
-	if err != nil {
-		return nil, err
-	}
-	if err := tx.Create(session); err != nil {
-		return nil, errors.Wrap(err, "error creating session")
-	}
-	return session, nil
-}
-
-func MFA_CreateSession(tx *storage.Connection, user *User, factorID *uuid.UUID) (*Session, error) {
-	session, err := NewSession(user, factorID)
-	if err != nil {
-		return nil, err
-	}
-	if err := tx.Create(session); err != nil {
-		return nil, errors.Wrap(err, "error creating session")
-	}
 	return session, nil
 }
 
-func FindSessionById(tx *storage.Connection, id uuid.UUID) (*Session, error) {
+func FindSessionByID(tx *storage.Connection, id uuid.UUID) (*Session, error) {
 	session := &Session{}
 	if err := tx.Eager().Q().Where("id = ?", id).First(session); err != nil {
 		if errors.Cause(err) == sql.ErrNoRows {
diff --git a/models/user.go b/models/user.go
index a3245cd1..d7856347 100644
--- a/models/user.go
+++ b/models/user.go
@@ -354,21 +354,38 @@ func FindUserByEmailChangeToken(tx *storage.Connection, token string) (*User, er
 }
 
 // FindUserWithRefreshToken finds a user from the provided refresh token.
-func FindUserWithRefreshToken(tx *storage.Connection, token string) (*User, *RefreshToken, error) {
+func FindUserWithRefreshToken(tx *storage.Connection, token string) (*User, *RefreshToken, *Session, error) {
 	refreshToken := &RefreshToken{}
 	if err := tx.Where("token = ?", token).First(refreshToken); err != nil {
 		if errors.Cause(err) == sql.ErrNoRows {
-			return nil, nil, RefreshTokenNotFoundError{}
+			return nil, nil, nil, RefreshTokenNotFoundError{}
 		}
-		return nil, nil, errors.Wrap(err, "error finding refresh token")
+		return nil, nil, nil, errors.Wrap(err, "error finding refresh token")
 	}
 
-	user, err := findUser(tx, "id = ?", refreshToken.UserID)
+	user, err := FindUserByID(tx, refreshToken.UserID)
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, nil, err
 	}
 
-	return user, refreshToken, nil
+	var session *Session
+
+	if refreshToken.SessionId != nil {
+		sessionId := *refreshToken.SessionId
+
+		if sessionId != uuid.Nil {
+			session, err = FindSessionByID(tx, sessionId)
+			if err != nil {
+				if !IsNotFoundError(err) {
+					return nil, nil, nil, errors.Wrap(err, "error finding session from refresh token")
+				}
+
+				// otherwise, there's no session for this refresh token
+			}
+		}
+	}
+
+	return user, refreshToken, session, nil
 }
 
 // FindUsersInAudience finds users with the matching audience.


Test patch:
diff --git a/api/admin_test.go b/api/admin_test.go
index 4cdd38e5..daa3a934 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -520,11 +520,8 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() {
 	}
 }
 
-// TestAdminUserDeleteFactor tests API /admin/users/<user_id>/factor/<factor_id>/
+// TestAdminUserDeleteFactor tests API /admin/users/<user_id>/factors/<factor_id>/
 func (ts *AdminTestSuite) TestAdminUserDeleteFactor() {
-	if !ts.API.config.MFA.Enabled {
-		return
-	}
 	u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
 	require.NoError(ts.T(), err, "Error making new user")
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
@@ -536,7 +533,7 @@ func (ts *AdminTestSuite) TestAdminUserDeleteFactor() {
 
 	// Setup request
 	w := httptest.NewRecorder()
-	req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), nil)
+	req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/factors/%s/", u.ID, f.ID), nil)
 
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
 
@@ -550,9 +547,6 @@ func (ts *AdminTestSuite) TestAdminUserDeleteFactor() {
 
 // TestAdminUserGetFactor tests API /admin/user/<user_id>/factors/
 func (ts *AdminTestSuite) TestAdminUserGetFactors() {
-	if !ts.API.config.MFA.Enabled {
-		return
-	}
 	u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
 	require.NoError(ts.T(), err, "Error making new user")
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
@@ -563,7 +557,7 @@ func (ts *AdminTestSuite) TestAdminUserGetFactors() {
 
 	// Setup request
 	w := httptest.NewRecorder()
-	req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s/factor/", u.ID), nil)
+	req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s/factors/", u.ID), nil)
 
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
 
@@ -571,13 +565,10 @@ func (ts *AdminTestSuite) TestAdminUserGetFactors() {
 	require.Equal(ts.T(), http.StatusOK, w.Code)
 	getFactorsResp := []*models.Factor{}
 	require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&getFactorsResp))
-	require.Equal(ts.T(), getFactorsResp[0].Secret, nil)
+	require.Equal(ts.T(), getFactorsResp[0].Secret, "")
 }
 
 func (ts *AdminTestSuite) TestAdminUserUpdateFactor() {
-	if !ts.API.config.MFA.Enabled {
-		return
-	}
 	u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil)
 	require.NoError(ts.T(), err, "Error making new user")
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user")
@@ -621,7 +612,7 @@ func (ts *AdminTestSuite) TestAdminUserUpdateFactor() {
 			var buffer bytes.Buffer
 			require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.FactorData))
 			w := httptest.NewRecorder()
-			req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), &buffer)
+			req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/admin/users/%s/factors/%s/", u.ID, f.ID), &buffer)
 			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token))
 			ts.API.handler.ServeHTTP(w, req)
 			require.Equal(ts.T(), c.ExpectedCode, w.Code)
diff --git a/api/audit_test.go b/api/audit_test.go
index d0cba113..ca3cc36e 100644
--- a/api/audit_test.go
+++ b/api/audit_test.go
@@ -49,12 +49,7 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string {
 	u.Role = "supabase_admin"
 
 	var token string
-	if ts.Config.MFA.Enabled {
-		token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-
-	}
+	token, err = generateAccessToken(ts.API.db, 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
index dbfeae15..9d616718 100644
--- a/api/auth_test.go
+++ b/api/auth_test.go
@@ -63,8 +63,7 @@ func (ts *AuthTestSuite) TestParseJWTClaims() {
 
 	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)
+	ctx, err := ts.API.parseJWTClaims(userJwt, req)
 	require.NoError(ts.T(), err)
 
 	// check if token is stored in context
@@ -76,14 +75,12 @@ func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
 	u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
 	require.NoError(ts.T(), err)
 
-	var s *models.Session
-	if ts.Config.MFA.Enabled {
-		s, err = models.MFA_CreateSession(ts.API.db, u, &uuid.Nil)
-	} else {
-		s, err = models.CreateSession(ts.API.db, u)
-
-	}
+	s, err := models.NewSession()
 	require.NoError(ts.T(), err)
+	s.UserID = u.ID
+	require.NoError(ts.T(), ts.API.db.Create(s))
+
+	require.NoError(ts.T(), ts.API.db.Load(s))
 
 	cases := []struct {
 		Desc            string
@@ -172,8 +169,7 @@ func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
 			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)
+			ctx, err := ts.API.parseJWTClaims(userJwt, req)
 			require.NoError(ts.T(), err)
 			ctx, err = ts.API.maybeLoadUserOrSession(ctx)
 			if c.ExpectedError != nil {
diff --git a/api/hook_test.go b/api/hook_test.go
index 8b2d448c..8311d6e2 100644
--- a/api/hook_test.go
+++ b/api/hook_test.go
@@ -179,20 +179,4 @@ func TestHookTimeout(t *testing.T) {
 	assert.Equal(t, 3, callCount)
 }
 
-func TestHookNoServer(t *testing.T) {
-	config := &conf.WebhookConfig{
-		URL:        "http://somewhere.something.com",
-		Retries:    1,
-		TimeoutSec: 1,
-	}
-	w := Webhook{
-		WebhookConfig: config,
-	}
-	_, err := w.trigger()
-	require.Error(t, err)
-	herr, ok := err.(*HTTPError)
-	require.True(t, ok)
-	assert.Equal(t, http.StatusBadGateway, herr.Code)
-}
-
 func squash(f func() error) { _ = f }
diff --git a/api/invite_test.go b/api/invite_test.go
index 6f28b5a1..a1a2e2b7 100644
--- a/api/invite_test.go
+++ b/api/invite_test.go
@@ -59,11 +59,8 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string {
 	u.Role = "supabase_admin"
 
 	var token string
-	if ts.API.config.MFA.Enabled {
-		token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	}
+	token, err = generateAccessToken(ts.API.db, 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/logout_test.go b/api/logout_test.go
index a32620f6..47b6a65b 100644
--- a/api/logout_test.go
+++ b/api/logout_test.go
@@ -42,11 +42,7 @@ func (ts *LogoutTestSuite) SetupTest() {
 
 	// generate access token to use for logout
 	var t string
-	if ts.Config.MFA.Enabled {
-		t, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		t, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	}
+	t, err = generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err)
 	ts.token = t
 }
diff --git a/api/mfa_test.go b/api/mfa_test.go
index 40e7e1e6..b27c5b56 100644
--- a/api/mfa_test.go
+++ b/api/mfa_test.go
@@ -40,9 +40,7 @@ func TestMFA(t *testing.T) {
 		Config: config,
 	}
 	defer api.db.Close()
-	if config.MFA.Enabled {
-		suite.Run(t, ts)
-	}
+	suite.Run(t, ts)
 }
 
 func (ts *MFATestSuite) SetupTest() {
@@ -56,8 +54,10 @@ func (ts *MFATestSuite) SetupTest() {
 	require.NoError(ts.T(), err, "Error creating test factor model")
 	require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor")
 	// Create corresponding sessoin
-	s, err := models.NewSession(u, &f.ID)
+	s, err := models.NewSession()
 	require.NoError(ts.T(), err, "Error creating test session")
+	s.UserID = u.ID
+	s.FactorID = &f.ID
 	require.NoError(ts.T(), ts.API.db.Create(s), "Error saving test session")
 
 	// Generate TOTP related settings
@@ -124,7 +124,7 @@ func (ts *MFATestSuite) TestEnrollFactor() {
 			user, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
 			ts.Require().NoError(err)
 
-			token, err := MFA_generateAccessToken(ts.API.db, user, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err := generateAccessToken(ts.API.db, user, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 			require.NoError(ts.T(), err)
 
 			w := httptest.NewRecorder()
@@ -161,7 +161,7 @@ func (ts *MFATestSuite) TestChallengeFactor() {
 	require.NoError(ts.T(), err)
 	f := factors[0]
 
-	token, err := MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(ts.API.db, 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
@@ -216,11 +216,13 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() {
 			require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor")
 
 			// Create session to be invalidated
-			secondarySession, err := models.NewSession(user, &f.ID)
+			secondarySession, err := models.NewSession()
 			require.NoError(ts.T(), err, "Error creating test session")
+			secondarySession.UserID = user.ID
+			secondarySession.FactorID = &f.ID
 			require.NoError(ts.T(), ts.API.db.Create(secondarySession), "Error saving test session")
 
-			token, err := MFA_generateAccessToken(ts.API.db, user, r.SessionId, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err := generateAccessToken(ts.API.db, user, r.SessionId, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 
 			require.NoError(ts.T(), err)
 
@@ -256,7 +258,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() {
 
 			if v.expectedHTTPCode == http.StatusOK {
 				// Ensure alternate session has been deleted
-				_, err = models.FindSessionById(ts.API.db, secondarySession.ID)
+				_, err = models.FindSessionByID(ts.API.db, secondarySession.ID)
 				require.EqualError(ts.T(), err, models.SessionNotFoundError{}.Error())
 			}
 			if !v.validChallenge {
@@ -302,8 +304,10 @@ func (ts *MFATestSuite) TestUnenrollVerifiedFactor() {
 			factors, err := models.FindFactorsByUser(ts.API.db, u)
 			require.NoError(ts.T(), err, "error finding factors")
 			f := factors[0]
-			secondarySession, err = models.NewSession(u, &f.ID)
+			secondarySession, err = models.NewSession()
 			require.NoError(ts.T(), err, "Error creating test session")
+			secondarySession.UserID = u.ID
+			secondarySession.FactorID = &f.ID
 			require.NoError(ts.T(), ts.API.db.Create(secondarySession), "Error saving test session")
 
 			sharedSecret := ts.TestOTPKey.Secret()
@@ -314,7 +318,7 @@ func (ts *MFATestSuite) TestUnenrollVerifiedFactor() {
 
 			var buffer bytes.Buffer
 
-			token, err := MFA_generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err := generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 			require.NoError(ts.T(), err)
 
 			w := httptest.NewRecorder()
@@ -326,7 +330,7 @@ func (ts *MFATestSuite) TestUnenrollVerifiedFactor() {
 			if v.expectedHTTPCode == http.StatusOK {
 				_, err = models.FindFactorByFactorID(ts.API.db, f.ID)
 				require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error())
-				session, _ := models.FindSessionById(ts.API.db, secondarySession.ID)
+				session, _ := models.FindSessionByID(ts.API.db, secondarySession.ID)
 				require.Equal(ts.T(), models.AAL1.String(), session.GetAAL())
 				require.Nil(ts.T(), session.FactorID)
 
@@ -345,8 +349,10 @@ func (ts *MFATestSuite) TestUnenrollUnverifiedFactor() {
 	factors, err := models.FindFactorsByUser(ts.API.db, u)
 	require.NoError(ts.T(), err, "error finding factors")
 	f := factors[0]
-	secondarySession, err = models.NewSession(u, &f.ID)
+	secondarySession, err = models.NewSession()
 	require.NoError(ts.T(), err, "Error creating test session")
+	secondarySession.UserID = u.ID
+	secondarySession.FactorID = &f.ID
 	require.NoError(ts.T(), ts.API.db.Create(secondarySession), "Error saving test session")
 
 	sharedSecret := ts.TestOTPKey.Secret()
@@ -354,7 +360,7 @@ func (ts *MFATestSuite) TestUnenrollUnverifiedFactor() {
 
 	var buffer bytes.Buffer
 
-	token, err := MFA_generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+	token, err := generateAccessToken(ts.API.db, u, &s.ID, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
 	require.NoError(ts.T(), err)
 	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
 		"factor_id": f.ID,
@@ -367,7 +373,7 @@ func (ts *MFATestSuite) TestUnenrollUnverifiedFactor() {
 	require.Equal(ts.T(), http.StatusOK, w.Code)
 	_, err = models.FindFactorByFactorID(ts.API.db, f.ID)
 	require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error())
-	session, _ := models.FindSessionById(ts.API.db, secondarySession.ID)
+	session, _ := models.FindSessionByID(ts.API.db, secondarySession.ID)
 	require.Equal(ts.T(), models.AAL1.String(), session.GetAAL())
 	require.Nil(ts.T(), session.FactorID)
 
@@ -392,7 +398,7 @@ func (ts *MFATestSuite) TestSessionsMaintainAALOnRefresh() {
 	data := &AccessTokenResponse{}
 	require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
 
-	ctx, err := ts.API.parseJWTClaims(data.Token, req, w)
+	ctx, err := ts.API.parseJWTClaims(data.Token, req)
 	require.NoError(ts.T(), err)
 	ctx, err = ts.API.maybeLoadUserOrSession(ctx)
 	require.NoError(ts.T(), err)
@@ -418,7 +424,7 @@ func (ts *MFATestSuite) TestMFAFollowedByPasswordSignIn() {
 
 	data := &AccessTokenResponse{}
 	require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data))
-	ctx, err := ts.API.parseJWTClaims(data.Token, req, w)
+	ctx, err := ts.API.parseJWTClaims(data.Token, req)
 	require.NoError(ts.T(), err)
 	ctx, err = ts.API.maybeLoadUserOrSession(ctx)
 	require.NoError(ts.T(), err)
diff --git a/api/phone_test.go b/api/phone_test.go
index 2a30d1ba..7057746e 100644
--- a/api/phone_test.go
+++ b/api/phone_test.go
@@ -127,12 +127,7 @@ func (ts *PhoneTestSuite) TestMissingSmsProviderConfig() {
 	require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")
 
 	var token string
-	if ts.Config.MFA.Enabled {
-		token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-
-	}
+	token, err = generateAccessToken(ts.API.db, 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 0fefca40..acecc256 100644
--- a/api/token_test.go
+++ b/api/token_test.go
@@ -22,6 +22,7 @@ type TokenTestSuite struct {
 	Config *conf.GlobalConfiguration
 
 	RefreshToken *models.RefreshToken
+	User         *models.User
 }
 
 func TestToken(t *testing.T) {
@@ -50,6 +51,7 @@ func (ts *TokenTestSuite) SetupTest() {
 	u.BannedUntil = nil
 	require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user")
 
+	ts.User = u
 	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{})
 	require.NoError(ts.T(), err, "Error creating refresh token")
 }
@@ -251,3 +253,49 @@ func (ts *TokenTestSuite) createBannedUser() *models.User {
 
 	return u
 }
+
+func (ts *TokenTestSuite) TestTokenRefreshWithExpiredSession() {
+	var err error
+
+	now := time.Now().UTC().Add(-1 * time.Second)
+
+	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, ts.User, models.GrantParams{
+		SessionNotAfter: &now,
+	})
+	require.NoError(ts.T(), err, "Error creating refresh token")
+
+	var buffer bytes.Buffer
+	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+		"refresh_token": ts.RefreshToken.Token,
+	}))
+
+	req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer)
+	req.Header.Set("Content-Type", "application/json")
+
+	w := httptest.NewRecorder()
+	ts.API.handler.ServeHTTP(w, req)
+	assert.Equal(ts.T(), http.StatusBadRequest, w.Code)
+}
+
+func (ts *TokenTestSuite) TestTokenRefreshWithUnexpiredSession() {
+	var err error
+
+	now := time.Now().UTC().Add(1 * time.Second)
+
+	ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, ts.User, models.GrantParams{
+		SessionNotAfter: &now,
+	})
+	require.NoError(ts.T(), err, "Error creating refresh token")
+
+	var buffer bytes.Buffer
+	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+		"refresh_token": ts.RefreshToken.Token,
+	}))
+
+	req := httptest.NewRequest(http.MethodPost, "http://localhost/token?grant_type=refresh_token", &buffer)
+	req.Header.Set("Content-Type", "application/json")
+
+	w := httptest.NewRecorder()
+	ts.API.handler.ServeHTTP(w, req)
+	assert.Equal(ts.T(), http.StatusOK, w.Code)
+}
diff --git a/api/user_test.go b/api/user_test.go
index 7f993b85..f1a9c429 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -48,11 +48,8 @@ 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")
 	var token string
-	if ts.API.config.MFA.Enabled {
-		token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	}
+	token, err = generateAccessToken(ts.API.db, 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)
@@ -117,12 +114,8 @@ func (ts *UserTestSuite) TestUserUpdateEmail() {
 			require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user")
 
 			var token string
-			if ts.API.config.MFA.Enabled {
-				token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-			} else {
-				token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
+			token, err = generateAccessToken(ts.API.db, 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
@@ -182,12 +175,7 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() {
 	for _, c := range cases {
 		ts.Run(c.desc, func() {
 			var token string
-			if ts.API.config.MFA.Enabled {
-				token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-			} else {
-				token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-
-			}
+			token, err = generateAccessToken(ts.API.db, 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
@@ -262,12 +250,7 @@ func (ts *UserTestSuite) TestUserUpdatePassword() {
 			req.Header.Set("Content-Type", "application/json")
 
 			var token string
-			if ts.API.config.MFA.Enabled {
-				token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-			} else {
-				token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-
-			}
+			token, err = generateAccessToken(ts.API.db, 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))
 
@@ -297,12 +280,7 @@ func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() {
 	require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user")
 
 	var token string
-	if ts.API.config.MFA.Enabled {
-		token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-
-	}
+	token, err = generateAccessToken(ts.API.db, 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 346e8ce2..3a3e77e1 100644
--- a/api/verify_test.go
+++ b/api/verify_test.go
@@ -104,12 +104,7 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() {
 
 	// Generate access token for request
 	var token string
-	if ts.API.config.MFA.Enabled {
-		token, err = MFA_generateAccessToken(ts.API.db, u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-	} else {
-		token, err = generateAccessToken(u, nil, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret)
-
-	}
+	token, err = generateAccessToken(ts.API.db, 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 34dd4917..74bcd04b 100644
--- a/hack/test.env
+++ b/hack/test.env
@@ -95,7 +95,6 @@ GOTRUE_SECURITY_CAPTCHA_ENABLED="false"
 GOTRUE_SECURITY_CAPTCHA_PROVIDER="hcaptcha"
 GOTRUE_SECURITY_CAPTCHA_SECRET="0x0000000000000000000000000000000000000000"
 GOTRUE_SECURITY_CAPTCHA_TIMEOUT="10s"
-GOTRUE_MFA_ENABLED="true"
 GOTRUE_SAML_ENABLED="true"
 GOTRUE_SAML_PRIVATE_KEY="MIIEowIBAAKCAQEAszrVveMQcSsa0Y+zN1ZFb19cRS0jn4UgIHTprW2tVBmO2PABzjY3XFCfx6vPirMAPWBYpsKmXrvm1tr0A6DZYmA8YmJd937VUQ67fa6DMyppBYTjNgGEkEhmKuszvF3MARsIKCGtZqUrmS7UG4404wYxVppnr2EYm3RGtHlkYsXu20MBqSDXP47bQP+PkJqC3BuNGk3xt5UHl2FSFpTHelkI6lBynw16B+lUT1F96SERNDaMqi/TRsZdGe5mB/29ngC/QBMpEbRBLNRir5iUevKS7Pn4aph9Qjaxx/97siktK210FJT23KjHpgcUfjoQ6BgPBTLtEeQdRyDuc/CgfwIDAQABAoIBAGYDWOEpupQPSsZ4mjMnAYJwrp4ZISuMpEqVAORbhspVeb70bLKonT4IDcmiexCg7cQBcLQKGpPVM4CbQ0RFazXZPMVq470ZDeWDEyhoCfk3bGtdxc1Zc9CDxNMs6FeQs6r1beEZug6weG5J/yRn/qYxQife3qEuDMl+lzfl2EN3HYVOSnBmdt50dxRuX26iW3nqqbMRqYn9OHuJ1LvRRfYeyVKqgC5vgt/6Tf7DAJwGe0dD7q08byHV8DBZ0pnMVU0bYpf1GTgMibgjnLjK//EVWafFHtN+RXcjzGmyJrk3+7ZyPUpzpDjO21kpzUQLrpEkkBRnmg6bwHnSrBr8avECgYEA3pq1PTCAOuLQoIm1CWR9/dhkbJQiKTJevlWV8slXQLR50P0WvI2RdFuSxlWmA4xZej8s4e7iD3MYye6SBsQHygOVGc4efvvEZV8/XTlDdyj7iLVGhnEmu2r7AFKzy8cOvXx0QcLg+zNd7vxZv/8D3Qj9Jje2LjLHKM5n/dZ3RzUCgYEAzh5Lo2anc4WN8faLGt7rPkGQF+7/18ImQE11joHWa3LzAEy7FbeOGpE/vhOv5umq5M/KlWFIRahMEQv4RusieHWI19ZLIP+JwQFxWxS+cPp3xOiGcquSAZnlyVSxZ//dlVgaZq2o2MfrxECcovRlaknl2csyf+HjFFwKlNxHm2MCgYAr//R3BdEy0oZeVRndo2lr9YvUEmu2LOihQpWDCd0fQw0ZDA2kc28eysL2RROte95r1XTvq6IvX5a0w11FzRWlDpQ4J4/LlcQ6LVt+98SoFwew+/PWuyLmxLycUbyMOOpm9eSc4wJJZNvaUzMCSkvfMtmm5jgyZYMMQ9A2Ul/9SQKBgB9mfh9mhBwVPIqgBJETZMMXOdxrjI5SBYHGSyJqpT+5Q0vIZLfqPrvNZOiQFzwWXPJ+tV4Mc/YorW3rZOdo6tdvEGnRO6DLTTEaByrY/io3/gcBZXoSqSuVRmxleqFdWWRnB56c1hwwWLqNHU+1671FhL6pNghFYVK4suP6qu4BAoGBAMk+VipXcIlD67mfGrET/xDqiWWBZtgTzTMjTpODhDY1GZck1eb4CQMP5j5V3gFJ4cSgWDJvnWg8rcz0unz/q4aeMGl1rah5WNDWj1QKWMS6vJhMHM/rqN1WHWR0ZnV83svYgtg0zDnQKlLujqW4JmGXLMU7ur6a+e6lpa1fvLsP"
 
diff --git a/models/factor_test.go b/models/factor_test.go
index 8c6eabfb..9677cf06 100644
--- a/models/factor_test.go
+++ b/models/factor_test.go
@@ -26,9 +26,7 @@ func TestFactor(t *testing.T) {
 		db: conn,
 	}
 	defer ts.db.Close()
-	if globalConfig.MFA.Enabled {
-		suite.Run(t, ts)
-	}
+	suite.Run(t, ts)
 }
 
 func (ts *FactorTestSuite) SetupTest() {
diff --git a/models/refresh_token_test.go b/models/refresh_token_test.go
index 6da738ba..4f3e6b16 100644
--- a/models/refresh_token_test.go
+++ b/models/refresh_token_test.go
@@ -52,7 +52,7 @@ func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() {
 	s, err := GrantRefreshTokenSwap(&http.Request{}, ts.db, u, r)
 	require.NoError(ts.T(), err)
 
-	_, nr, err := FindUserWithRefreshToken(ts.db, r.Token)
+	_, nr, _, err := FindUserWithRefreshToken(ts.db, r.Token)
 	require.NoError(ts.T(), err)
 
 	require.Equal(ts.T(), r.ID, nr.ID)
@@ -68,7 +68,7 @@ func (ts *RefreshTokenTestSuite) TestLogout() {
 	require.NoError(ts.T(), err)
 
 	require.NoError(ts.T(), Logout(ts.db, u.ID))
-	u, r, err = FindUserWithRefreshToken(ts.db, r.Token)
+	u, r, _, err = FindUserWithRefreshToken(ts.db, r.Token)
 	require.Errorf(ts.T(), err, "expected error when there are no refresh tokens to authenticate. user: %v token: %v", u, r)
 
 	require.True(ts.T(), IsNotFoundError(err), "expected NotFoundError")
diff --git a/models/sessions_test.go b/models/sessions_test.go
index c1d41405..7e4eb647 100644
--- a/models/sessions_test.go
+++ b/models/sessions_test.go
@@ -1,7 +1,6 @@
 package models
 
 import (
-	"github.com/gofrs/uuid"
 	"github.com/netlify/gotrue/conf"
 	"github.com/netlify/gotrue/storage"
 	"github.com/netlify/gotrue/storage/test"
@@ -37,18 +36,17 @@ func TestSession(t *testing.T) {
 		Config: globalConfig,
 	}
 	defer ts.db.Close()
-	if globalConfig.MFA.Enabled {
-		suite.Run(t, ts)
-	}
+	suite.Run(t, ts)
 }
 
 func (ts *SessionsTestSuite) TestCalculateAALAndAMR() {
 	totalDistinctClaims := 2
 	u, err := FindUserByEmailAndAudience(ts.db, "test@example.com", ts.Config.JWT.Aud)
 	require.NoError(ts.T(), err)
-	// TODO(Joel): Replace with CreateSession once MFA feature flag is lifted
-	session, err := MFA_CreateSession(ts.db, u, &uuid.Nil)
+	session, err := NewSession()
 	require.NoError(ts.T(), err)
+	session.UserID = u.ID
+	require.NoError(ts.T(), ts.db.Create(session))
 
 	err = AddClaimToSession(ts.db, session, PasswordGrant)
 	require.NoError(ts.T(), err)
@@ -56,7 +54,7 @@ func (ts *SessionsTestSuite) TestCalculateAALAndAMR() {
 	firstClaimAddedTime := time.Now()
 	err = AddClaimToSession(ts.db, session, TOTPSignIn)
 	require.NoError(ts.T(), err)
-	session, err = FindSessionById(ts.db, session.ID)
+	session, err = FindSessionByID(ts.db, session.ID)
 	require.NoError(ts.T(), err)
 
 	aal, amr := session.CalculateAALAndAMR()
@@ -66,7 +64,7 @@ func (ts *SessionsTestSuite) TestCalculateAALAndAMR() {
 	err = AddClaimToSession(ts.db, session, TOTPSignIn)
 	require.NoError(ts.T(), err)
 
-	session, err = FindSessionById(ts.db, session.ID)
+	session, err = FindSessionByID(ts.db, session.ID)
 	require.NoError(ts.T(), err)
 
 	aal, amr = session.CalculateAALAndAMR()
diff --git a/models/user_test.go b/models/user_test.go
index e6de8c6d..db664faf 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -151,10 +151,12 @@ func (ts *UserTestSuite) TestFindUserWithRefreshToken() {
 	r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{})
 	require.NoError(ts.T(), err)
 
-	n, nr, err := FindUserWithRefreshToken(ts.db, r.Token)
+	n, nr, s, err := FindUserWithRefreshToken(ts.db, r.Token)
 	require.NoError(ts.T(), err)
 	require.Equal(ts.T(), r.ID, nr.ID)
 	require.Equal(ts.T(), u.ID, n.ID)
+	require.NotNil(ts.T(), s)
+	require.Equal(ts.T(), *r.SessionId, s.ID)
 }
 
 func (ts *UserTestSuite) TestIsDuplicatedEmail() {