Repository: supabase/gotrue
Pull request #1277 (2023-10-18 14:23:06)

feat: add friendly name to enroll factor response
## What kind of change does this PR introduce?

See title
 - [x] Need to update docs
 

PR to update GoTrue: https://github.com/supabase/gotrue/pull/1277/files
Docs Update: https://github.com/supabase/gotrue/pull/1277


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 679644e2..2f44adab 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -39,7 +39,7 @@ jobs:
 
       - uses: actions/setup-go@v3
         with:
-          go-version: "^1.20.0" # The Go version to download (if necessary) and use.
+          go-version: "^1.21.0" # The Go version to download (if necessary) and use.
 
       - run: make deps
       - run: make all
diff --git a/Dockerfile b/Dockerfile
index 413228ee..88ae5abf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.20-alpine as build
+FROM golang:1.21-alpine as build
 ENV GO111MODULE=on
 ENV CGO_ENABLED=0
 ENV GOOS=linux
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 8df1f740..85b5dbb7 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,4 +1,4 @@
-FROM golang:1.20-alpine
+FROM golang:1.21-alpine
 ENV GO111MODULE=on
 ENV CGO_ENABLED=0
 ENV GOOS=linux
diff --git a/README.md b/README.md
index bd58d591..413e7f91 100644
--- a/README.md
+++ b/README.md
@@ -311,8 +311,7 @@ To enable tracing configure these variables:
 
 `GOTRUE_TRACING_ENABLED` - `boolean`
 
-`GOTRUE_TRACING_EXPORTER` - `string` only `opentracing` (deprecated) and
-`opentelemetry` supported
+`GOTRUE_TRACING_EXPORTER` - `string` only `opentelemetry` supported
 
 Make sure you also configure the [OpenTelemetry
 Exporter](https://opentelemetry.io/docs/reference/specification/protocol/exporter/)
diff --git a/go.mod b/go.mod
index e435faa3..e6febe2d 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 	github.com/jmoiron/sqlx v1.3.5
 	github.com/joho/godotenv v1.4.0
 	github.com/kelseyhightower/envconfig v1.4.0
-	github.com/microcosm-cc/bluemonday v1.0.24 // indirect
+	github.com/microcosm-cc/bluemonday v1.0.26 // indirect
 	github.com/mitchellh/mapstructure v1.1.2
 	github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450
 	github.com/pkg/errors v0.9.1
@@ -33,7 +33,7 @@ require (
 	github.com/spf13/cobra v1.6.1
 	github.com/stretchr/testify v1.8.4
 	golang.org/x/crypto v0.14.0
-	golang.org/x/oauth2 v0.6.0
+	golang.org/x/oauth2 v0.7.0
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 )
 
@@ -45,7 +45,6 @@ require (
 require (
 	github.com/XSAM/otelsql v0.16.0
 	github.com/bombsimon/logrusr/v3 v3.0.0
-	github.com/opentracing/opentracing-go v1.1.0
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.0
 	go.opentelemetry.io/contrib/instrumentation/runtime v0.35.0
 	go.opentelemetry.io/otel v1.10.0
@@ -60,7 +59,6 @@ require (
 	go.opentelemetry.io/otel/sdk v1.10.0
 	go.opentelemetry.io/otel/sdk/metric v0.31.0
 	go.opentelemetry.io/otel/trace v1.10.0
-	gopkg.in/DataDog/dd-trace-go.v1 v1.12.1
 	gopkg.in/h2non/gock.v1 v1.1.2
 )
 
@@ -95,7 +93,7 @@ require (
 	github.com/gobuffalo/plush/v4 v4.1.18 // indirect
 	github.com/gobuffalo/tags/v3 v3.1.4 // indirect
 	github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/gorilla/context v1.1.1 // indirect
 	github.com/gorilla/css v1.0.0 // indirect
@@ -116,7 +114,6 @@ require (
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
-	github.com/philhofer/fwd v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_golang v1.12.2 // indirect
 	github.com/prometheus/client_model v0.2.0 // indirect
@@ -129,7 +126,6 @@ require (
 	github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/stretchr/objx v0.5.0 // indirect
-	github.com/tinylib/msgp v1.1.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 // indirect
 	go.opentelemetry.io/proto/otlp v0.19.0 // indirect
 	golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
@@ -139,12 +135,12 @@ require (
 	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
-	google.golang.org/grpc v1.53.0 // indirect
-	google.golang.org/protobuf v1.28.1 // indirect
+	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+	google.golang.org/grpc v1.56.3 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
-go 1.19
+go 1.21
diff --git a/go.sum b/go.sum
index ccd40c00..6335a26e 100644
--- a/go.sum
+++ b/go.sum
@@ -60,6 +60,7 @@ github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62/go.mod h1:r5ZalvR
 github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
 github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -183,8 +184,9 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
 github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
 github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
 github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
+github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -210,8 +212,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -225,6 +228,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -337,6 +341,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -371,8 +376,8 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
-github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
-github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
+github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
+github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -386,13 +391,9 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
-github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/patrickmn/go-cache v0.0.0-20170418232947-7ac151875ffb/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
-github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
-github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -484,8 +485,6 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/supabase/mailme v0.0.0-20230628061017-01f68480c747 h1:FIUdLV4o5JLsJno4Poum157kAxDKINeJo6liBfauLrI=
 github.com/supabase/mailme v0.0.0-20230628061017-01f68480c747/go.mod h1:kWsnmPfUBZTavlXYkfJrE9unzmmRAIi/kqsxXfEWEY8=
-github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
-github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -535,6 +534,7 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
+go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -638,8 +638,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
-golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
+golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -837,8 +837,8 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
-google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -855,8 +855,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
 google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -870,10 +870,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/DataDog/dd-trace-go.v1 v1.12.1 h1:zkyLw+Uq6BvGwy5hFeLVI1ePgOkqJswFPL1uOx6SSA4=
-gopkg.in/DataDog/dd-trace-go.v1 v1.12.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
@@ -901,6 +899,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/api/api.go b/internal/api/api.go
index a55df273..78541ce2 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -14,6 +14,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"github.com/supabase/gotrue/internal/conf"
 	"github.com/supabase/gotrue/internal/mailer"
+	"github.com/supabase/gotrue/internal/models"
 	"github.com/supabase/gotrue/internal/observability"
 	"github.com/supabase/gotrue/internal/storage"
 )
@@ -31,6 +32,17 @@ type API struct {
 	db      *storage.Connection
 	config  *conf.GlobalConfiguration
 	version string
+
+	// overrideTime can be used to override the clock used by handlers. Should only be used in tests!
+	overrideTime func() time.Time
+}
+
+func (a *API) Now() time.Time {
+	if a.overrideTime != nil {
+		return a.overrideTime()
+	}
+
+	return time.Now()
 }
 
 // NewAPI instantiates a new REST API
@@ -67,13 +79,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 	// request tracing should be added only when tracing or metrics is
 	// enabled
 	if globalConfig.Tracing.Enabled {
-		switch globalConfig.Tracing.Exporter {
-		case conf.OpenTracing:
-			r.UseBypass(opentracer)
-
-		default:
-			r.UseBypass(observability.RequestTracing())
-		}
+		r.UseBypass(observability.RequestTracing())
 	} else if globalConfig.Metrics.Enabled {
 		r.UseBypass(observability.RequestTracing())
 	}
@@ -82,7 +88,14 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
 	r.Use(recoverer)
 
 	if globalConfig.DB.CleanupEnabled {
-		r.UseBypass(api.databaseCleanup)
+		cleanup := &models.Cleanup{
+			SessionTimebox:           globalConfig.Sessions.Timebox,
+			SessionInactivityTimeout: globalConfig.Sessions.InactivityTimeout,
+		}
+
+		cleanup.Setup()
+
+		r.UseBypass(api.databaseCleanup(cleanup))
 	}
 
 	r.Get("/health", api.HealthCheck)
diff --git a/internal/api/external.go b/internal/api/external.go
index ca817ffb..8b1a89ac 100644
--- a/internal/api/external.go
+++ b/internal/api/external.go
@@ -156,6 +156,8 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 	var grantParams models.GrantParams
 	var err error
 
+	grantParams.FillGrantParams(r)
+
 	if providerType == "twitter" {
 		// future OAuth1.0 providers will use this method
 		oAuthResponseData, err := a.oAuth1Callback(ctx, r, providerType)
@@ -244,7 +246,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
 		}
 	} else {
 		// Left as hash fragment to comply with spec. Additionally, may override existing error query param if set to PKCE.
-		rurl, err = a.prepErrorRedirectURL(unauthorizedError("Unverified email with %v", providerType), w, r, rurl, models.ImplicitFlow)
+		rurl, err = a.prepErrorRedirectURL(unauthorizedError("Unverified email with %v. A confirmation email has been sent to your %v email.", providerType, providerType), w, r, rurl, models.ImplicitFlow)
 		if err != nil {
 			return err
 		}
@@ -295,7 +297,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
 			}
 		}
 
-		if _, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
+		if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
 			return nil, terr
 		}
 
@@ -331,7 +333,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
 			return nil, terr
 		}
 
-		if _, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
+		if identity, terr = a.createNewIdentity(tx, user, providerType, identityData); terr != nil {
 			return nil, terr
 		}
 
@@ -366,13 +368,11 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
 		return nil, unauthorizedError("User is unauthorized")
 	}
 
-	// an account with a previously unconfirmed email + password
-	// combination or phone may exist. so now that there is an
-	// OAuth identity bound to this user, and since they have not
-	// confirmed their email or phone, they are unaware that a
-	// potentially malicious door exists into their account; thus
-	// the password and phone needs to be removed.
-	if terr = user.RemoveUnconfirmedIdentities(tx); terr != nil {
+	// An account with a previously unconfirmed email + password
+	// combination, phone or oauth identity may exist. These identities
+	// need to be removed when a new oauth identity is being added
+	// to prevent pre-account takeover attacks from happening.
+	if terr = user.RemoveUnconfirmedIdentities(tx, identity); terr != nil {
 		return nil, internalServerError("Error updating user").WithInternalError(terr)
 	}
 
@@ -446,15 +446,16 @@ func (a *API) processInvite(r *http.Request, ctx context.Context, tx *storage.Co
 	if userData.Metadata != nil {
 		identityData = structs.Map(userData.Metadata)
 	}
-	if _, err := a.createNewIdentity(tx, user, providerType, identityData); err != nil {
+	identity, err := a.createNewIdentity(tx, user, providerType, identityData)
+	if err != nil {
 		return nil, err
 	}
-	if err = user.UpdateAppMetaData(tx, map[string]interface{}{
+	if err := user.UpdateAppMetaData(tx, map[string]interface{}{
 		"provider": providerType,
 	}); err != nil {
 		return nil, err
 	}
-	if err = user.UpdateAppMetaDataProviders(tx); err != nil {
+	if err := user.UpdateAppMetaDataProviders(tx); err != nil {
 		return nil, err
 	}
 	if err := user.UpdateUserMetaData(tx, identityData); err != nil {
@@ -476,7 +477,7 @@ func (a *API) processInvite(r *http.Request, ctx context.Context, tx *storage.Co
 	// confirmed their email or phone, they are unaware that a
 	// potentially malicious door exists into their account; thus
 	// the password and phone needs to be removed.
-	if err = user.RemoveUnconfirmedIdentities(tx); err != nil {
+	if err := user.RemoveUnconfirmedIdentities(tx, identity); err != nil {
 		return nil, internalServerError("Error updating user").WithInternalError(err)
 	}
 
diff --git a/internal/api/mfa.go b/internal/api/mfa.go
index ab717862..57130438 100644
--- a/internal/api/mfa.go
+++ b/internal/api/mfa.go
@@ -33,9 +33,10 @@ type TOTPObject struct {
 }
 
 type EnrollFactorResponse struct {
-	ID   uuid.UUID  `json:"id"`
-	Type string     `json:"type"`
-	TOTP TOTPObject `json:"totp,omitempty"`
+	ID           uuid.UUID  `json:"id"`
+	Type         string     `json:"type"`
+	FriendlyName string     `json:"friendly_name"`
+	TOTP         TOTPObject `json:"totp,omitempty"`
 }
 
 type VerifyFactorParams struct {
@@ -149,8 +150,9 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error {
 	}
 
 	return sendJSON(w, http.StatusOK, &EnrollFactorResponse{
-		ID:   factor.ID,
-		Type: models.TOTP,
+		ID:           factor.ID,
+		Type:         models.TOTP,
+		FriendlyName: factor.FriendlyName,
 		TOTP: TOTPObject{
 			// See: https://css-tricks.com/probably-dont-base64-svg/
 			QRCode: buf.String(),
diff --git a/internal/api/middleware.go b/internal/api/middleware.go
index 22309395..0ccb51a5 100644
--- a/internal/api/middleware.go
+++ b/internal/api/middleware.go
@@ -233,26 +233,28 @@ func (a *API) requireSAMLEnabled(w http.ResponseWriter, req *http.Request) (cont
 	return ctx, nil
 }
 
-func (a *API) databaseCleanup(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		next.ServeHTTP(w, r)
+func (a *API) databaseCleanup(cleanup *models.Cleanup) func(http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			next.ServeHTTP(w, r)
 
-		switch r.Method {
-		case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
-			// continue
+			switch r.Method {
+			case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
+				// continue
 
-		default:
-			return
-		}
+			default:
+				return
+			}
 
-		db := a.db.WithContext(r.Context())
-		log := observability.GetLogEntry(r)
+			db := a.db.WithContext(r.Context())
+			log := observability.GetLogEntry(r)
 
-		affectedRows, err := models.Cleanup(db)
-		if err != nil {
-			log.WithError(err).WithField("affected_rows", affectedRows).Warn("database cleanup failed")
-		} else if affectedRows > 0 {
-			log.WithField("affected_rows", affectedRows).Debug("cleaned up expired or stale rows")
-		}
-	})
+			affectedRows, err := cleanup.Clean(db)
+			if err != nil {
+				log.WithError(err).WithField("affected_rows", affectedRows).Warn("database cleanup failed")
+			} else if affectedRows > 0 {
+				log.WithField("affected_rows", affectedRows).Debug("cleaned up expired or stale rows")
+			}
+		})
+	}
 }
diff --git a/internal/api/opentracer.go b/internal/api/opentracer.go
deleted file mode 100644
index 65bdcf65..00000000
--- a/internal/api/opentracer.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package api
-
-import (
-	"fmt"
-	"net/http"
-	"strconv"
-
-	"github.com/opentracing/opentracing-go"
-	"github.com/opentracing/opentracing-go/ext"
-	ddtrace_ext "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
-	ddopentracer "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
-)
-
-type tracingResponseWriter struct {
-	http.ResponseWriter
-	statusCode int
-}
-
-func newTracingResponseWriter(w http.ResponseWriter) *tracingResponseWriter {
-	return &tracingResponseWriter{w, http.StatusOK}
-}
-
-func (trw *tracingResponseWriter) WriteHeader(code int) {
-	trw.statusCode = code
-	trw.ResponseWriter.WriteHeader(code)
-}
-
-func opentracer(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		clientContext, _ := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
-		span, traceCtx := opentracing.StartSpanFromContext(r.Context(), "http.handler",
-			ext.RPCServerOption(clientContext),
-			ddopentracer.SpanType(ddtrace_ext.AppTypeWeb),
-		)
-		defer span.Finish()
-
-		ext.HTTPMethod.Set(span, r.Method)
-		ext.HTTPUrl.Set(span, r.URL.Path)
-		resourceName := r.URL.Path
-		resourceName = r.Method + " " + resourceName
-		span.SetTag("resource.name", resourceName)
-
-		if reqID := getRequestID(r.Context()); reqID != "" {
-			span.SetTag("http.request_id", reqID)
-		}
-
-		trw := newTracingResponseWriter(w)
-		next.ServeHTTP(trw, r.WithContext(traceCtx))
-
-		status := trw.statusCode
-
-		// Setting the status as an int doesn't propogate for use in datadog dashboards,
-		// so we convert to a string.
-		span.SetTag(string(ext.HTTPStatusCode), strconv.Itoa(status))
-
-		if status >= 500 && status < 600 {
-			ext.Error.Set(span, true)
-			span.SetTag("error.type", fmt.Sprintf("%d: %s", status, http.StatusText(status)))
-			span.LogKV(
-				"event", "error",
-				"message", fmt.Sprintf("%d: %s", status, http.StatusText(status)),
-			)
-		}
-	})
-}
diff --git a/internal/api/provider/fly.go b/internal/api/provider/fly.go
index b7f58cae..5c0f9aa0 100644
--- a/internal/api/provider/fly.go
+++ b/internal/api/provider/fly.go
@@ -19,14 +19,18 @@ type flyProvider struct {
 }
 
 type flyUser struct {
-	ResourceOwnerID string            `json:"resource_owner_id"`
-	UserID          string            `json:"user_id"`
-	UserName        string            `json:"user_name"`
-	Email           string            `json:"email"`
-	Scope           []string          `json:"scope"`
-	Application     map[string]string `json:"application"`
-	ExpiresIn       int               `json:"expires_in"`
-	CreatedAt       int               `json:"created_at"`
+	ResourceOwnerID string `json:"resource_owner_id"`
+	UserID          string `json:"user_id"`
+	UserName        string `json:"user_name"`
+	Email           string `json:"email"`
+	Organizations   []struct {
+		ID   string `json:"id"`
+		Role string `json:"role"`
+	} `json:"organizations"`
+	Scope       []string          `json:"scope"`
+	Application map[string]string `json:"application"`
+	ExpiresIn   int               `json:"expires_in"`
+	CreatedAt   int               `json:"created_at"`
 }
 
 // NewFlyProvider creates a Fly oauth provider.
@@ -86,6 +90,7 @@ func (p flyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserP
 			ProviderId:    u.UserID,
 			CustomClaims: map[string]interface{}{
 				"resource_owner_id": u.ResourceOwnerID,
+				"organizations":     u.Organizations,
 				"application":       u.Application,
 				"scope":             u.Scope,
 				"created_at":        u.CreatedAt,
diff --git a/internal/api/provider/provider.go b/internal/api/provider/provider.go
index b72f3725..296f24b5 100644
--- a/internal/api/provider/provider.go
+++ b/internal/api/provider/provider.go
@@ -51,9 +51,9 @@ type Claims struct {
 	Locale            string `json:"locale,omitempty" structs:"locale,omitempty"`
 	UpdatedAt         string `json:"updated_at,omitempty" structs:"updated_at,omitempty"`
 	Email             string `json:"email,omitempty" structs:"email,omitempty"`
-	EmailVerified     bool   `json:"email_verified,omitempty" structs:"email_verified,omitempty"`
+	EmailVerified     bool   `json:"email_verified,omitempty" structs:"email_verified"`
 	Phone             string `json:"phone,omitempty" structs:"phone,omitempty"`
-	PhoneVerified     bool   `json:"phone_verified,omitempty" structs:"phone_verified,omitempty"`
+	PhoneVerified     bool   `json:"phone_verified,omitempty" structs:"phone_verified"`
 
 	// Custom profile claims that are provider specific
 	CustomClaims map[string]interface{} `json:"custom_claims,omitempty" structs:"custom_claims,omitempty"`
diff --git a/internal/api/provider/spotify.go b/internal/api/provider/spotify.go
index f278c3ff..58911cec 100644
--- a/internal/api/provider/spotify.go
+++ b/internal/api/provider/spotify.go
@@ -86,12 +86,14 @@ func (g spotifyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 
 	return &UserProvidedData{
 		Metadata: &Claims{
-			Issuer:        g.APIPath,
-			Subject:       u.ID,
-			Name:          u.DisplayName,
-			Picture:       avatarURL,
-			Email:         u.Email,
-			EmailVerified: true, // Spotify dosen't provide data on if email is verified.
+			Issuer:  g.APIPath,
+			Subject: u.ID,
+			Name:    u.DisplayName,
+			Picture: avatarURL,
+			Email:   u.Email,
+			// Spotify dosen't provide data on whether the user's email is verified.
+			// https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile
+			EmailVerified: false,
 
 			// To be deprecated
 			AvatarURL:  avatarURL,
@@ -100,7 +102,7 @@ func (g spotifyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U
 		},
 		Emails: []Email{{
 			Email:    u.Email,
-			Verified: true, // Spotify dosen't provide data on if email is verified.
+			Verified: false,
 			Primary:  true,
 		}},
 	}, nil
diff --git a/internal/api/samlacs.go b/internal/api/samlacs.go
index f4433390..fa561798 100644
--- a/internal/api/samlacs.go
+++ b/internal/api/samlacs.go
@@ -262,6 +262,8 @@ func (a *API) SAMLACS(w http.ResponseWriter, r *http.Request) error {
 
 	var grantParams models.GrantParams
 
+	grantParams.FillGrantParams(r)
+
 	if !notAfter.IsZero() {
 		grantParams.SessionNotAfter = &notAfter
 	}
diff --git a/internal/api/signup.go b/internal/api/signup.go
index efe30139..62097302 100644
--- a/internal/api/signup.go
+++ b/internal/api/signup.go
@@ -108,6 +108,9 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error {
 
 	var user *models.User
 	var grantParams models.GrantParams
+
+	grantParams.FillGrantParams(r)
+
 	params.Aud = a.requestAud(ctx, r)
 
 	switch params.Provider {
diff --git a/internal/api/token.go b/internal/api/token.go
index afbf6057..30f82c37 100644
--- a/internal/api/token.go
+++ b/internal/api/token.go
@@ -113,6 +113,9 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri
 	var user *models.User
 	var grantParams models.GrantParams
 	var provider string
+
+	grantParams.FillGrantParams(r)
+
 	if params.Email != "" {
 		provider = "email"
 		if !config.External.Email.Enabled {
@@ -180,6 +183,12 @@ func (a *API) PKCE(ctx context.Context, w http.ResponseWriter, r *http.Request)
 	db := a.db.WithContext(ctx)
 	var grantParams models.GrantParams
 
+	// There is a slight problem with this as it will pick-up the
+	// User-Agent and IP addresses from the server if used on the server
+	// side. Currently there's no mechanism to distinguish, but the server
+	// can be told to at least propagate the User-Agent header.
+	grantParams.FillGrantParams(r)
+
 	params := &PKCEGrantParams{}
 	body, err := getBodyBytes(r)
 	if err != nil {
@@ -245,7 +254,6 @@ func (a *API) PKCE(ctx context.Context, w http.ResponseWriter, r *http.Request)
 	}
 
 	return sendJSON(w, http.StatusOK, token)
-
 }
 
 func generateAccessToken(tx *storage.Connection, user *models.User, sessionId *uuid.UUID, config *conf.JWTConfiguration) (string, int64, error) {
diff --git a/internal/api/token_oidc.go b/internal/api/token_oidc.go
index 3c8553f4..dd00ea99 100644
--- a/internal/api/token_oidc.go
+++ b/internal/api/token_oidc.go
@@ -193,6 +193,8 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R
 	var token *AccessTokenResponse
 	var grantParams models.GrantParams
 
+	grantParams.FillGrantParams(r)
+
 	if err := db.Transaction(func(tx *storage.Connection) error {
 		var user *models.User
 		var terr error
diff --git a/internal/api/token_refresh.go b/internal/api/token_refresh.go
index 09f3199e..de1d0bb8 100644
--- a/internal/api/token_refresh.go
+++ b/internal/api/token_refresh.go
@@ -10,6 +10,7 @@ import (
 	"github.com/supabase/gotrue/internal/metering"
 	"github.com/supabase/gotrue/internal/models"
 	"github.com/supabase/gotrue/internal/storage"
+	"github.com/supabase/gotrue/internal/utilities"
 )
 
 const retryLoopDuration = 5.0
@@ -44,13 +45,13 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 	// Instead of waiting at the database level, they're waiting at the API
 	// level instead and retry to refresh the locked row every 10-30
 	// milliseconds.
-	retryStart := time.Now()
+	retryStart := a.Now()
 	retry := true
 
 	for retry && time.Since(retryStart).Seconds() < retryLoopDuration {
 		retry = false
 
-		user, _, session, err := models.FindUserWithRefreshToken(db, params.RefreshToken, false)
+		user, token, session, err := models.FindUserWithRefreshToken(db, params.RefreshToken, false)
 		if err != nil {
 			if models.IsNotFoundError(err) {
 				return oauthError("invalid_grant", "Invalid Refresh Token: Refresh Token Not Found")
@@ -69,9 +70,25 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 				notAfter = *session.NotAfter
 			}
 
-			if !notAfter.IsZero() && time.Now().UTC().After(notAfter) {
+			if config.Sessions.Timebox != nil {
+				sessionEndsAt := session.CreatedAt.Add((*config.Sessions.Timebox).Abs())
+
+				if notAfter.IsZero() || notAfter.After(sessionEndsAt) {
+					notAfter = sessionEndsAt
+				}
+			}
+
+			if !notAfter.IsZero() && a.Now().After(notAfter) {
 				return oauthError("invalid_grant", "Invalid Refresh Token: Session Expired")
 			}
+
+			if config.Sessions.InactivityTimeout != nil {
+				timesOutAt := session.LastRefreshedAt(&token.UpdatedAt).Add(*config.Sessions.InactivityTimeout)
+
+				if timesOutAt.Before(a.Now()) {
+					return oauthError("invalid_grant", "Invalid Refresh Token: Session Expired (Inactivity)")
+				}
+			}
 		}
 
 		// Basic checks above passed, now we need to serialize access
@@ -86,7 +103,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 		var newTokenResponse *AccessTokenResponse
 
 		err = db.Transaction(func(tx *storage.Connection) error {
-			user, token, _, terr := models.FindUserWithRefreshToken(tx, params.RefreshToken, true /* forUpdate */)
+			user, token, session, terr := models.FindUserWithRefreshToken(tx, params.RefreshToken, true /* forUpdate */)
 			if terr != nil {
 				if models.IsNotFoundError(terr) {
 					// because forUpdate was set, and the
@@ -106,25 +123,43 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 			// refresh token row and session are locked at this
 			// point, cannot be concurrently refreshed
 
-			if token.Revoked {
-				a.clearCookieTokens(config, w)
-				// For a revoked refresh token to be reused, it
-				// has to fall within the reuse interval.
-
-				reuseUntil := token.UpdatedAt.Add(
-					time.Second * time.Duration(config.Security.RefreshTokenReuseInterval))
+			var issuedToken *models.RefreshToken
 
-				if time.Now().After(reuseUntil) {
-					// not OK to reuse this token
+			if token.Revoked {
+				activeRefreshToken, terr := session.FindCurrentlyActiveRefreshToken(tx)
+				if terr != nil {
+					return internalServerError(terr.Error())
+				}
 
-					if config.Security.RefreshTokenRotationEnabled {
-						// Revoke all tokens in token family
-						if err := models.RevokeTokenFamily(tx, token); err != nil {
-							return internalServerError(err.Error())
+				if activeRefreshToken != nil && activeRefreshToken.Parent.String() == token.Token {
+					// Token was revoked, but it's the
+					// parent of the currently active one.
+					// This indicates that the client was
+					// not able to store the result when it
+					// refreshed token. This case is
+					// allowed, provided we return back the
+					// active refresh token instead of
+					// creating a new one.
+					issuedToken = activeRefreshToken
+				} else {
+					// For a revoked refresh token to be reused, it
+					// has to fall within the reuse interval.
+					reuseUntil := token.UpdatedAt.Add(
+						time.Second * time.Duration(config.Security.RefreshTokenReuseInterval))
+
+					if a.Now().After(reuseUntil) {
+						a.clearCookieTokens(config, w)
+						// not OK to reuse this token
+
+						if config.Security.RefreshTokenRotationEnabled {
+							// Revoke all tokens in token family
+							if err := models.RevokeTokenFamily(tx, token); err != nil {
+								return internalServerError(err.Error())
+							}
 						}
-					}
 
-					return oauthError("invalid_grant", "Invalid Refresh Token: Already Used").WithInternalMessage("Possible abuse attempt: %v", token.ID)
+						return oauthError("invalid_grant", "Invalid Refresh Token: Already Used").WithInternalMessage("Possible abuse attempt: %v", token.ID)
+					}
 				}
 			}
 
@@ -132,26 +167,47 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h
 				return terr
 			}
 
-			// a new refresh token is generated and explicitly not reusing
-			// a previous one as it could have already been revoked while
-			// this handler was running
-			newToken, terr := models.GrantRefreshTokenSwap(r, tx, user, token)
-			if terr != nil {
-				return terr
-			}
+			if issuedToken == nil {
+				newToken, terr := models.GrantRefreshTokenSwap(r, tx, user, token)
+				if terr != nil {
+					return terr
+				}
 
-			tokenString, expiresAt, terr = generateAccessToken(tx, user, newToken.SessionId, &config.JWT)
+				issuedToken = newToken
+			}
 
+			tokenString, expiresAt, terr = generateAccessToken(tx, user, issuedToken.SessionId, &config.JWT)
 			if terr != nil {
 				return internalServerError("error generating jwt token").WithInternalError(terr)
 			}
 
+			refreshedAt := a.Now()
+			session.RefreshedAt = &refreshedAt
+
+			userAgent := r.Header.Get("User-Agent")
+			if userAgent != "" {
+				session.UserAgent = &userAgent
+			} else {
+				session.UserAgent = nil
+			}
+
+			ipAddress := utilities.GetIPAddress(r)
+			if ipAddress != "" {
+				session.IP = &ipAddress
+			} else {
+				session.IP = nil
+			}
+
+			if terr := session.UpdateOnlyRefreshInfo(tx); terr != nil {
+				return internalServerError("failed to update session information").WithInternalError(terr)
+			}
+
 			newTokenResponse = &AccessTokenResponse{
 				Token:        tokenString,
 				TokenType:    "bearer",
 				ExpiresIn:    config.JWT.Exp,
 				ExpiresAt:    expiresAt,
-				RefreshToken: newToken.Token,
+				RefreshToken: issuedToken.Token,
 				User:         user,
 			}
 			if terr = a.setCookieTokens(config, newTokenResponse, false, w); terr != nil {
diff --git a/internal/api/verify.go b/internal/api/verify.go
index 33af2c8f..dc5a5e0d 100644
--- a/internal/api/verify.go
+++ b/internal/api/verify.go
@@ -19,11 +19,6 @@ import (
 	"github.com/supabase/gotrue/internal/utilities"
 )
 
-var (
-	// indicates that a user should be redirected due to an error
-	errRedirectWithQuery = errors.New("redirect user")
-)
-
 const (
 	signupVerification      = "signup"
 	recoveryVerification    = "recovery"
@@ -138,6 +133,9 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request, params *VerifyPa
 		token       *AccessTokenResponse
 		authCode    string
 	)
+
+	grantParams.FillGrantParams(r)
+
 	flowType := models.ImplicitFlow
 	var authenticationMethod models.AuthenticationMethod
 	if strings.HasPrefix(params.Token, PKCEPrefix) {
@@ -233,6 +231,8 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request, params *VerifyP
 	)
 	var isSingleConfirmationResponse = false
 
+	grantParams.FillGrantParams(r)
+
 	err := db.Transaction(func(tx *storage.Connection) error {
 		var terr error
 		aud := a.requestAud(ctx, r)
@@ -515,13 +515,13 @@ func (a *API) verifyTokenHash(ctx context.Context, conn *storage.Connection, par
 
 	if err != nil {
 		if models.IsNotFoundError(err) {
-			return nil, expiredTokenError("Email link is invalid or has expired").WithInternalError(errRedirectWithQuery)
+			return nil, expiredTokenError("Email link is invalid or has expired").WithInternalError(err)
 		}
 		return nil, internalServerError("Database error finding user from email link").WithInternalError(err)
 	}
 
 	if user.IsBanned() {
-		return nil, unauthorizedError("Error confirming user").WithInternalError(errRedirectWithQuery)
+		return nil, unauthorizedError("Error confirming user").WithInternalMessage("user is banned")
 	}
 
 	var isExpired bool
@@ -543,7 +543,7 @@ func (a *API) verifyTokenHash(ctx context.Context, conn *storage.Connection, par
 	}
 
 	if isExpired {
-		return nil, expiredTokenError("Email link is invalid or has expired").WithInternalError(errRedirectWithQuery)
+		return nil, expiredTokenError("Email link is invalid or has expired").WithInternalMessage("email link has expired")
 	}
 
 	return user, nil
@@ -563,6 +563,8 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection,
 	case smsVerification:
 		user, err = models.FindUserByPhoneAndAudience(conn, params.Phone, aud)
 	case emailChangeVerification:
+		// Since the email change could be trigger via the implicit or PKCE flow,
+		// the query used has to also check if the token saved in the db contains the pkce_ prefix
 		user, err = models.FindUserForEmailChange(conn, params.Email, tokenHash, aud, config.Mailer.SecureEmailChangeEnabled)
 	default:
 		user, err = models.FindUserByEmailAndAudience(conn, params.Email, aud)
@@ -570,13 +572,13 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection,
 
 	if err != nil {
 		if models.IsNotFoundError(err) {
-			return nil, notFoundError(err.Error()).WithInternalError(errRedirectWithQuery)
+			return nil, notFoundError(err.Error()).WithInternalError(err)
 		}
 		return nil, internalServerError("Database error finding user").WithInternalError(err)
 	}
 
 	if user.IsBanned() {
-		return nil, unauthorizedError("Error confirming user").WithInternalError(errRedirectWithQuery)
+		return nil, unauthorizedError("Error confirming user").WithInternalMessage("user is banned")
 	}
 
 	var isValid bool
@@ -624,8 +626,8 @@ func (a *API) verifyUserAndToken(ctx context.Context, conn *storage.Connection,
 		isValid = isOtpValid(tokenHash, expectedToken, sentAt, config.Sms.OtpExp)
 	}
 
-	if !isValid || err != nil {
-		return nil, expiredTokenError("Token has expired or is invalid").WithInternalError(errRedirectWithQuery)
+	if !isValid {
+		return nil, expiredTokenError("Token has expired or is invalid").WithInternalMessage("token has expired or is invalid")
 	}
 	return user, nil
 }
diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go
index edc3fd14..47624e26 100644
--- a/internal/conf/configuration.go
+++ b/internal/conf/configuration.go
@@ -111,6 +111,23 @@ func (a *APIConfiguration) Validate() error {
 	return nil
 }
 
+type SessionsConfiguration struct {
+	Timebox           *time.Duration `json:"timebox"`
+	InactivityTimeout *time.Duration `json:"inactivity_timeout,omitempty" split_words:"true"`
+}
+
+func (c *SessionsConfiguration) Validate() error {
+	if c.Timebox == nil {
+		return nil
+	}
+
+	if *c.Timebox <= time.Duration(0) {
+		return fmt.Errorf("conf: session timebox duration must be positive when set, was %v", (*c.Timebox).String())
+	}
+
+	return nil
+}
+
 // GlobalConfiguration holds all the configuration that applies to all instances.
 type GlobalConfiguration struct {
 	API                   APIConfiguration
@@ -126,7 +143,7 @@ type GlobalConfiguration struct {
 	RateLimitEmailSent    float64 `split_words:"true" default:"30"`
 	RateLimitSmsSent      float64 `split_words:"true" default:"30"`
 	RateLimitVerify       float64 `split_words:"true" default:"30"`
-	RateLimitTokenRefresh float64 `split_words:"true" default:"30"`
+	RateLimitTokenRefresh float64 `split_words:"true" default:"150"`
 	RateLimitSso          float64 `split_words:"true" default:"30"`
 
 	SiteURL           string   `json:"site_url" split_words:"true" required:"true"`
@@ -139,6 +156,7 @@ type GlobalConfiguration struct {
 	DisableSignup     bool                     `json:"disable_signup" split_words:"true"`
 	Webhook           WebhookConfig            `json:"webhook" split_words:"true"`
 	Security          SecurityConfiguration    `json:"security"`
+	Sessions          SessionsConfiguration    `json:"sessions"`
 	MFA               MFAConfiguration         `json:"MFA"`
 	Cookie            struct {
 		Key      string `json:"key"`
@@ -516,6 +534,7 @@ func (c *GlobalConfiguration) Validate() error {
 		&c.SMTP,
 		&c.SAML,
 		&c.Security,
+		&c.Sessions,
 	}
 
 	for _, validatable := range validatables {
diff --git a/internal/conf/tracing.go b/internal/conf/tracing.go
index 717cae47..9a1d9bee 100644
--- a/internal/conf/tracing.go
+++ b/internal/conf/tracing.go
@@ -4,7 +4,6 @@ type TracingExporter = string
 
 const (
 	OpenTelemetryTracing TracingExporter = "opentelemetry"
-	OpenTracing          TracingExporter = "opentracing"
 )
 
 type TracingConfig struct {
diff --git a/internal/models/cleanup.go b/internal/models/cleanup.go
index eddaf090..b1e686af 100644
--- a/internal/models/cleanup.go
+++ b/internal/models/cleanup.go
@@ -3,6 +3,7 @@ package models
 import (
 	"fmt"
 	"sync/atomic"
+	"time"
 
 	"github.com/sirupsen/logrus"
 	"go.opentelemetry.io/otel/attribute"
@@ -14,19 +15,22 @@ import (
 	"github.com/supabase/gotrue/internal/storage"
 )
 
-// cleanupNext holds an atomically incrementing value that determines which of
-// the cleanupStatements will be run next.
-var cleanupNext uint32
+type Cleanup struct {
+	SessionTimebox           *time.Duration
+	SessionInactivityTimeout *time.Duration
 
-// cleanupStatements holds all of the possible cleanup raw SQL. Only one at a
-// time is executed using cleanupNext % len(cleanupStatements).
-var CleanupStatements []string
+	cleanupStatements []string
 
-// cleanupAffectedRows tracks an OpenTelemetry metric on the total number of
-// cleaned up rows.
-var cleanupAffectedRows otelasyncint64instrument.Counter
+	// cleanupNext holds an atomically incrementing value that determines which of
+	// the cleanupStatements will be run next.
+	cleanupNext uint32
 
-func init() {
+	// cleanupAffectedRows tracks an OpenTelemetry metric on the total number of
+	// cleaned up rows.
+	cleanupAffectedRows otelasyncint64instrument.Counter
+}
+
+func (c *Cleanup) Setup() {
 	tableRefreshTokens := RefreshToken{}.TableName()
 	tableSessions := Session{}.TableName()
 	tableRelayStates := SAMLRelayState{}.TableName()
@@ -37,7 +41,7 @@ func init() {
 	// as this makes sure that only rows that are not being used in another
 	// transaction are deleted. These deletes are thus very quick and
 	// efficient, as they don't wait on other transactions.
-	CleanupStatements = append(CleanupStatements,
+	c.cleanupStatements = append(c.cleanupStatements,
 		fmt.Sprintf("delete from %q where id in (select id from %q where revoked is true and updated_at < now() - interval '24 hours' limit 100 for update skip locked);", tableRefreshTokens, tableRefreshTokens),
 		fmt.Sprintf("update %q set revoked = true, updated_at = now() where id in (select %q.id from %q join %q on %q.session_id = %q.id where %q.not_after < now() - interval '24 hours' and %q.revoked is false limit 100 for update skip locked);", tableRefreshTokens, tableRefreshTokens, tableRefreshTokens, tableSessions, tableRefreshTokens, tableSessions, tableSessions, tableRefreshTokens),
 		// sessions are deleted after 72 hours to allow refresh tokens
@@ -49,14 +53,32 @@ func init() {
 		fmt.Sprintf("delete from %q where id in (select id from %q where created_at < now() - interval '24 hours' limit 100 for update skip locked);", tableMFAChallenges, tableMFAChallenges),
 	)
 
-	var err error
-	cleanupAffectedRows, err = metricglobal.Meter("gotrue").AsyncInt64().Counter(
+	if c.SessionTimebox != nil {
+		timeboxSeconds := int((*c.SessionTimebox).Seconds())
+
+		c.cleanupStatements = append(c.cleanupStatements, fmt.Sprintf("delete from %q where id in (select id from %q where created_at + interval '%d seconds' < now() - interval '24 hours' limit 100 for update skip locked);", tableSessions, tableSessions, timeboxSeconds))
+	}
+
+	if c.SessionInactivityTimeout != nil {
+		inactivitySeconds := int((*c.SessionTimebox).Seconds())
+
+		// delete sessions with a refreshed_at column
+		c.cleanupStatements = append(c.cleanupStatements, fmt.Sprintf("delete from %q where id in (select id from %q where refreshed_at is not null and refreshed_at + interval '%d seconds' < now() - interval '24 hours' limit 100 for update skip locked);", tableSessions, tableSessions, inactivitySeconds))
+
+		// delete sessions without a refreshed_at column by looking for
+		// unrevoked refresh_tokens
+		c.cleanupStatements = append(c.cleanupStatements, fmt.Sprintf("delete from %q where id in (select %q.id as id from %q, %q where %q.session_id = %q.id and %q.refreshed_at is null and %q.revoked is false and %q.updated_at + interval '%d seconds' < now() - interval '24 hours' limit 100 for update skip locked)", tableSessions, tableSessions, tableSessions, tableRefreshTokens, tableRefreshTokens, tableSessions, tableSessions, tableRefreshTokens, tableRefreshTokens, inactivitySeconds))
+	}
+
+	cleanupAffectedRows, err := metricglobal.Meter("gotrue").AsyncInt64().Counter(
 		"gotrue_cleanup_affected_rows",
 		metricinstrument.WithDescription("Number of affected rows from cleaning up stale entities"),
 	)
 	if err != nil {
 		logrus.WithError(err).Error("unable to get gotrue.gotrue_cleanup_rows counter metric")
 	}
+
+	c.cleanupAffectedRows = cleanupAffectedRows
 }
 
 // Cleanup removes stale entities in the database. You can call it on each
@@ -65,7 +87,7 @@ func init() {
 // not affect performance of other database jobs. Note that calling this does
 // not clean up the whole database, but does a small piecemeal clean up each
 // time when called.
-func Cleanup(db *storage.Connection) (int, error) {
+func (c *Cleanup) Clean(db *storage.Connection) (int, error) {
 	ctx, span := observability.Tracer("gotrue").Start(db.Context(), "database-cleanup")
 	defer span.End()
 
@@ -73,8 +95,8 @@ func Cleanup(db *storage.Connection) (int, error) {
 	defer span.SetAttributes(attribute.Int64("gotrue.cleanup.affected_rows", int64(affectedRows)))
 
 	if err := db.WithContext(ctx).Transaction(func(tx *storage.Connection) error {
-		nextIndex := atomic.AddUint32(&cleanupNext, 1) % uint32(len(CleanupStatements))
-		statement := CleanupStatements[nextIndex]
+		nextIndex := atomic.AddUint32(&c.cleanupNext, 1) % uint32(len(c.cleanupStatements))
+		statement := c.cleanupStatements[nextIndex]
 
 		count, terr := tx.RawQuery(statement).ExecWithCount()
 		if terr != nil {
@@ -88,8 +110,8 @@ func Cleanup(db *storage.Connection) (int, error) {
 		return affectedRows, err
 	}
 
-	if cleanupAffectedRows != nil {
-		cleanupAffectedRows.Observe(ctx, int64(affectedRows))
+	if c.cleanupAffectedRows != nil {
+		c.cleanupAffectedRows.Observe(ctx, int64(affectedRows))
 	}
 
 	return affectedRows, nil
diff --git a/internal/models/refresh_token.go b/internal/models/refresh_token.go
index 3a15b0b0..1707a9bb 100644
--- a/internal/models/refresh_token.go
+++ b/internal/models/refresh_token.go
@@ -10,6 +10,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/supabase/gotrue/internal/crypto"
 	"github.com/supabase/gotrue/internal/storage"
+	"github.com/supabase/gotrue/internal/utilities"
 )
 
 // RefreshToken is the database model for refresh tokens.
@@ -41,6 +42,14 @@ type GrantParams struct {
 	FactorID *uuid.UUID
 
 	SessionNotAfter *time.Time
+
+	UserAgent string
+	IP        string
+}
+
+func (g *GrantParams) FillGrantParams(r *http.Request) {
+	g.UserAgent = r.Header.Get("User-Agent")
+	g.IP = utilities.GetIPAddress(r)
 }
 
 // GrantAuthenticatedUser creates a refresh token for the provided user.
@@ -110,7 +119,6 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok
 	if oldToken != nil {
 		token.Parent = storage.NullString(oldToken.Token)
 		token.SessionId = oldToken.SessionId
-
 	}
 
 	if token.SessionId == nil {
@@ -129,6 +137,14 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok
 			session.NotAfter = params.SessionNotAfter
 		}
 
+		if params.UserAgent != "" {
+			session.UserAgent = &params.UserAgent
+		}
+
+		if params.IP != "" {
+			session.IP = &params.IP
+		}
+
 		if err := tx.Create(session); err != nil {
 			return nil, errors.Wrap(err, "error creating new session")
 		}
diff --git a/internal/models/sessions.go b/internal/models/sessions.go
index d6ea13b9..94bb9cf8 100644
--- a/internal/models/sessions.go
+++ b/internal/models/sessions.go
@@ -58,14 +58,21 @@ func (s sortAMREntries) Swap(i, j int) {
 }
 
 type Session struct {
-	ID        uuid.UUID  `json:"-" db:"id"`
-	UserID    uuid.UUID  `json:"user_id" db:"user_id"`
-	NotAfter  *time.Time `json:"not_after,omitempty" db:"not_after"`
+	ID     uuid.UUID `json:"-" db:"id"`
+	UserID uuid.UUID `json:"user_id" db:"user_id"`
+
+	// NotAfter is overriden by timeboxed sessions.
+	NotAfter *time.Time `json:"not_after,omitempty" db:"not_after"`
+
 	CreatedAt time.Time  `json:"created_at" db:"created_at"`
 	UpdatedAt time.Time  `json:"updated_at" db:"updated_at"`
 	FactorID  *uuid.UUID `json:"factor_id" db:"factor_id"`
 	AMRClaims []AMRClaim `json:"amr,omitempty" has_many:"amr_claims"`
 	AAL       *string    `json:"aal" db:"aal"`
+
+	RefreshedAt *time.Time `json:"refreshed_at,omitempty" db:"refreshed_at"`
+	UserAgent   *string    `json:"user_agent,omitempty" db:"user_agent"`
+	IP          *string    `json:"ip,omitempty" db:"ip"`
 }
 
 func (Session) TableName() string {
@@ -73,6 +80,30 @@ func (Session) TableName() string {
 	return tableName
 }
 
+func (s *Session) LastRefreshedAt(refreshTokenTime *time.Time) time.Time {
+	refreshedAt := s.RefreshedAt
+
+	if refreshedAt == nil || refreshedAt.IsZero() {
+		if refreshTokenTime != nil {
+			rtt := *refreshTokenTime
+
+			if rtt.IsZero() {
+				return s.CreatedAt
+			} else if rtt.After(s.CreatedAt) {
+				return rtt
+			}
+		}
+
+		return s.CreatedAt
+	}
+
+	return *refreshedAt
+}
+
+func (s *Session) UpdateOnlyRefreshInfo(tx *storage.Connection) error {
+	return tx.UpdateOnly(s, "refreshed_at", "user_agent", "ip")
+}
+
 func NewSession() (*Session, error) {
 	id := uuid.Must(uuid.NewV4())
 
@@ -227,3 +258,16 @@ func (s *Session) GetAAL() string {
 func (s *Session) IsAAL2() bool {
 	return s.GetAAL() == AAL2.String()
 }
+
+// FindCurrentlyActiveRefreshToken returns the currently active refresh
+// token in the session. This is the last created (ordered by the serial
+// primary key) non-revoked refresh token for the session.
+func (s *Session) FindCurrentlyActiveRefreshToken(tx *storage.Connection) (*RefreshToken, error) {
+	var activeRefreshToken RefreshToken
+
+	if err := tx.Q().Where("session_id = ? and revoked is false", s.ID).Order("id desc").First(&activeRefreshToken); err != nil {
+		return nil, err
+	}
+
+	return &activeRefreshToken, nil
+}
diff --git a/internal/models/user.go b/internal/models/user.go
index fabe7010..ed83c288 100644
--- a/internal/models/user.go
+++ b/internal/models/user.go
@@ -531,8 +531,8 @@ func FindUsersInAudience(tx *storage.Connection, aud string, pageParams *Paginat
 func FindUserByEmailChangeCurrentAndAudience(tx *storage.Connection, email, token, aud string) (*User, error) {
 	return findUser(
 		tx,
-		"instance_id = ? and LOWER(email) = ? and email_change_token_current = ? and aud = ? and is_sso_user = false",
-		uuid.Nil, strings.ToLower(email), token, aud,
+		"instance_id = ? and LOWER(email) = ? and aud = ? and is_sso_user = false and (email_change_token_current = 'pkce_' || ? or email_change_token_current = ?)",
+		uuid.Nil, strings.ToLower(email), aud, token, token,
 	)
 }
 
@@ -540,8 +540,8 @@ func FindUserByEmailChangeCurrentAndAudience(tx *storage.Connection, email, toke
 func FindUserByEmailChangeNewAndAudience(tx *storage.Connection, email, token, aud string) (*User, error) {
 	return findUser(
 		tx,
-		"instance_id = ? and LOWER(email_change) = ? and email_change_token_new = ? and aud = ? and is_sso_user = false",
-		uuid.Nil, strings.ToLower(email), token, aud,
+		"instance_id = ? and LOWER(email_change) = ? and aud = ? and is_sso_user = false and (email_change_token_new = 'pkce_' || ? or email_change_token_new = ?)",
+		uuid.Nil, strings.ToLower(email), aud, token, token,
 	)
 }
 
@@ -640,58 +640,41 @@ func (u *User) UpdateBannedUntil(tx *storage.Connection) error {
 }
 
 // RemoveUnconfirmedIdentities removes potentially malicious unconfirmed identities from a user (if any)
-func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection) error {
+func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection, identity *Identity) error {
 	if u.IsConfirmed() {
 		return nil
 	}
 
+	// user is unconfirmed so the password should be reset
 	u.EncryptedPassword = ""
-
 	if terr := tx.UpdateOnly(u, "encrypted_password"); terr != nil {
 		return terr
 	}
 
-	if providersList, ok := u.AppMetaData["providers"].([]string); ok {
-		// user has "providers" metadata, and the "email" provider
-		// should be removed from it
-
-		var confirmedProviders []string
-
-		for _, provider := range providersList {
-			if provider != "email" {
-				confirmedProviders = append(confirmedProviders, provider)
-			}
-		}
-
-		u.AppMetaData["providers"] = confirmedProviders
-
-		if len(confirmedProviders) > 0 {
-			u.AppMetaData["provider"] = confirmedProviders[0]
-		} else {
-			u.AppMetaData["provider"] = nil
-		}
+	// user is unconfirmed so existing user_metadata should be overwritten
+	// to use the current identity metadata
+	u.UserMetaData = identity.IdentityData
 
+	// user is unconfirmed so none of the providers associated to it are verified yet
+	// only the current provider should be kept
+	if _, ok := u.AppMetaData["providers"].([]string); ok {
+		u.AppMetaData["providers"] = []string{identity.Provider}
+		u.AppMetaData["provider"] = identity.Provider
 		if terr := u.UpdateAppMetaData(tx, u.AppMetaData); terr != nil {
 			return terr
 		}
 	}
 
-	// finally, remove any identity with the "email" provider
-
-	var confirmedProviders []Identity
-
-	for i, identity := range u.Identities {
-		if identity.Provider == "email" {
+	// finally, remove all identities except the current identity being authenticated
+	for i := range u.Identities {
+		identityId := u.Identities[i].Provider + u.Identities[i].ID
+		identityIdToKeep := identity.Provider + identity.ID
+		if identityId != identityIdToKeep {
 			if terr := tx.Destroy(&u.Identities[i]); terr != nil {
 				return terr
 			}
-		} else {
-			confirmedProviders = append(confirmedProviders, identity)
 		}
 	}
-
-	u.Identities = confirmedProviders
-
 	return nil
 }
 
diff --git a/internal/observability/request-tracing.go b/internal/observability/request-tracing.go
index b5f3ee1d..c3592554 100644
--- a/internal/observability/request-tracing.go
+++ b/internal/observability/request-tracing.go
@@ -141,9 +141,36 @@ func RequestTracing() func(http.Handler) http.Handler {
 			defer traceChiRouteURLParamsSafely(r)
 			defer countStatusCodesSafely(&writer, r, statusCodes)
 
+			originalUserAgent := r.Header.Get("X-Gotrue-Original-User-Agent")
+			if originalUserAgent != "" {
+				r.Header.Set("User-Agent", originalUserAgent)
+			}
+
 			next.ServeHTTP(&writer, r)
+
+			if originalUserAgent != "" {
+				r.Header.Set("X-Gotrue-Original-User-Agent", originalUserAgent)
+				r.Header.Set("User-Agent", "stripped")
+			}
 		}
 
-		return otelhttp.NewHandler(http.HandlerFunc(fn), "api")
+		otelHandler := otelhttp.NewHandler(http.HandlerFunc(fn), "api")
+
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			// there is a vulnerability with otelhttp where
+			// User-Agent strings are kept in RAM indefinitely and
+			// can be used as an easy way to resource exhaustion;
+			// so this code strips the User-Agent header before
+			// it's passed to be traced by otelhttp, and then is
+			// returned back to the middleware
+			// https://github.com/supabase/gotrue/security/dependabot/11
+			userAgent := r.UserAgent()
+			if userAgent != "" {
+				r.Header.Set("X-Gotrue-Original-User-Agent", userAgent)
+				r.Header.Set("User-Agent", "stripped")
+			}
+
+			otelHandler.ServeHTTP(w, r)
+		})
 	}
 }
diff --git a/internal/observability/tracing.go b/internal/observability/tracing.go
index 0fad5492..661585b4 100644
--- a/internal/observability/tracing.go
+++ b/internal/observability/tracing.go
@@ -3,7 +3,6 @@ package observability
 import (
 	"context"
 	"fmt"
-	"net"
 	"sync"
 	"time"
 
@@ -11,10 +10,6 @@ import (
 	"github.com/supabase/gotrue/internal/conf"
 	"github.com/supabase/gotrue/internal/utilities"
 
-	"github.com/opentracing/opentracing-go"
-	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
-	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
-
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
@@ -106,19 +101,6 @@ func enableOpenTelemetryTracing(ctx context.Context, tc *conf.TracingConfig) err
 	return nil
 }
 
-func enableOpenTracing(tc *conf.TracingConfig) {
-	tracerOps := []tracer.StartOption{
-		tracer.WithServiceName(tc.ServiceName),
-		tracer.WithAgentAddr(net.JoinHostPort(tc.Host, tc.Port)),
-	}
-
-	for k, v := range tc.Tags {
-		tracerOps = append(tracerOps, tracer.WithGlobalTag(k, v))
-	}
-
-	opentracing.SetGlobalTracer(opentracer.New(tracerOps...))
-}
-
 var (
 	tracingOnce sync.Once
 )
@@ -134,17 +116,12 @@ func ConfigureTracing(ctx context.Context, tc *conf.TracingConfig) error {
 	var err error
 
 	tracingOnce.Do(func() {
-		opentracing.SetGlobalTracer(opentracing.NoopTracer{})
-
 		if tc.Enabled {
-			switch tc.Exporter {
-			case conf.OpenTelemetryTracing:
+			if tc.Exporter == conf.OpenTelemetryTracing {
 				if err = enableOpenTelemetryTracing(ctx, tc); err != nil {
 					logrus.WithError(err).Error("unable to start OTLP trace exporter")
 				}
 
-			case conf.OpenTracing:
-				enableOpenTracing(tc)
 			}
 		}
 	})
diff --git a/internal/storage/helper.go b/internal/storage/helper.go
index f2b0decb..23599840 100644
--- a/internal/storage/helper.go
+++ b/internal/storage/helper.go
@@ -25,3 +25,7 @@ func (s NullString) Value() (driver.Value, error) {
 	}
 	return string(s), nil
 }
+
+func (s NullString) String() string {
+	return string(s)
+}
diff --git a/internal/utilities/request.go b/internal/utilities/request.go
index 0313798e..3c419fa6 100644
--- a/internal/utilities/request.go
+++ b/internal/utilities/request.go
@@ -24,7 +24,12 @@ func GetIPAddress(r *http.Request) string {
 
 			for _, ip := range ips {
 				if ip != "" {
-					return ip
+					parsed := net.ParseIP(ip)
+					if parsed == nil {
+						continue
+					}
+
+					return parsed.String()
 				}
 			}
 		}
diff --git a/migrations/00_init_auth_schema.up.sql b/migrations/00_init_auth_schema.up.sql
index 06475d2e..a040095a 100644
--- a/migrations/00_init_auth_schema.up.sql
+++ b/migrations/00_init_auth_schema.up.sql
@@ -75,7 +75,7 @@ CREATE TABLE IF NOT EXISTS {{ index .Options "Namespace" }}.schema_migrations (
 	"version" varchar(255) NOT NULL,
 	CONSTRAINT schema_migrations_pkey PRIMARY KEY ("version")
 );
-comment on table auth.schema_migrations is 'Auth: Manages updates to the auth system.';
+comment on table {{ index .Options "Namespace" }}.schema_migrations is 'Auth: Manages updates to the auth system.';
 		
 -- Gets the User ID from the request cookie
 create or replace function {{ index .Options "Namespace" }}.uid() returns uuid as $$
diff --git a/migrations/20221003041349_add_mfa_schema.up.sql b/migrations/20221003041349_add_mfa_schema.up.sql
index 88c6421c..a44654ae 100644
--- a/migrations/20221003041349_add_mfa_schema.up.sql
+++ b/migrations/20221003041349_add_mfa_schema.up.sql
@@ -47,4 +47,4 @@ create table if not exists {{ index .Options "Namespace" }}.mfa_amr_claims(
     constraint mfa_amr_claims_session_id_authentication_method_pkey unique(session_id, authentication_method),
     constraint mfa_amr_claims_session_id_fkey foreign key(session_id) references {{ index .Options "Namespace" }}.sessions(id) on delete cascade
 );
-comment on table auth.mfa_amr_claims is 'auth: stores authenticator method reference claims for multi factor authentication';
+comment on table {{ index .Options "Namespace" }}.mfa_amr_claims is 'auth: stores authenticator method reference claims for multi factor authentication';
diff --git a/migrations/20221125140132_backfill_email_identity.up.sql b/migrations/20221125140132_backfill_email_identity.up.sql
index 313d9fb3..497acdd7 100644
--- a/migrations/20221125140132_backfill_email_identity.up.sql
+++ b/migrations/20221125140132_backfill_email_identity.up.sql
@@ -1,7 +1,7 @@
 -- backfill the auth.identities column by adding an email identity 
 -- for all auth.users with an email and password 
 
-insert into auth.identities (id, user_id, identity_data, provider, last_sign_in_at, created_at, updated_at)
+insert into {{ index .Options "Namespace" }}.identities (id, user_id, identity_data, provider, last_sign_in_at, created_at, updated_at)
 select id, id as user_id, jsonb_build_object('sub', id, 'email', email) as identity_data, 'email' as provider, null as last_sign_in_at, '2022-11-25' as created_at, '2022-11-25' as updated_at
-from auth.users as users
-where encrypted_password != '' and email is not null and not exists(select user_id from auth.identities where user_id = users.id);
+from {{ index .Options "Namespace" }}.users as users
+where encrypted_password != '' and email is not null and not exists(select user_id from {{ index .Options "Namespace" }}.identities where user_id = users.id);
diff --git a/migrations/20231027141322_add_session_refresh_columns.up.sql b/migrations/20231027141322_add_session_refresh_columns.up.sql
new file mode 100644
index 00000000..79efba9b
--- /dev/null
+++ b/migrations/20231027141322_add_session_refresh_columns.up.sql
@@ -0,0 +1,4 @@
+alter table if exists {{ index .Options "Namespace" }}.sessions
+  add column if not exists refreshed_at timestamp without time zone,
+  add column if not exists user_agent text,
+  add column if not exists ip inet;


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1f4dc774..77b466cb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,7 +13,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.20.x]
+        go-version: [1.21.x]
     runs-on: ubuntu-20.04
     services:
       postgres:
diff --git a/internal/api/external_github_test.go b/internal/api/external_github_test.go
index fecedc8e..d4f8a014 100644
--- a/internal/api/external_github_test.go
+++ b/internal/api/external_github_test.go
@@ -279,7 +279,7 @@ func (ts *ExternalTestSuite) TestSignupExternalGitHubErrorWhenVerifiedFalse() {
 	ts.Require().NoError(err)
 	ts.Equal("unauthorized_client", v.Get("error"))
 	ts.Equal("401", v.Get("error_code"))
-	ts.Equal("Unverified email with github", v.Get("error_description"))
+	ts.Equal("Unverified email with github. A confirmation email has been sent to your github email.", v.Get("error_description"))
 	assertAuthorizationFailure(ts, u, "", "", "")
 }
 
diff --git a/internal/api/external_kakao_test.go b/internal/api/external_kakao_test.go
index cb86a97f..6d4d3b21 100644
--- a/internal/api/external_kakao_test.go
+++ b/internal/api/external_kakao_test.go
@@ -209,7 +209,7 @@ func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenVerifiedFalse() {
 	ts.Require().NoError(err)
 	ts.Equal("unauthorized_client", v.Get("error"))
 	ts.Equal("401", v.Get("error_code"))
-	ts.Equal("Unverified email with kakao", v.Get("error_description"))
+	ts.Equal("Unverified email with kakao. A confirmation email has been sent to your kakao email.", v.Get("error_description"))
 	assertAuthorizationFailure(ts, u, "", "", "")
 }
 
diff --git a/internal/api/mfa_test.go b/internal/api/mfa_test.go
index 36090e6d..e3686c9a 100644
--- a/internal/api/mfa_test.go
+++ b/internal/api/mfa_test.go
@@ -147,6 +147,7 @@ func (ts *MFATestSuite) TestEnrollFactor() {
 				qrCode := enrollResp.TOTP.QRCode
 				hasSVGStartAndEnd := strings.Contains(qrCode, "<svg") && strings.Contains(qrCode, "</svg>")
 				require.True(ts.T(), hasSVGStartAndEnd)
+				require.Equal(ts.T(), c.friendlyName, enrollResp.FriendlyName)
 			}
 		})
 	}
diff --git a/internal/api/opentelemetry-tracer_test.go b/internal/api/opentelemetry-tracer_test.go
index 62689943..ec9fa1f3 100644
--- a/internal/api/opentelemetry-tracer_test.go
+++ b/internal/api/opentelemetry-tracer_test.go
@@ -63,9 +63,11 @@ func (ts *OpenTelemetryTracerTestSuite) TestOpenTelemetryTracer_Spans() {
 
 	w := httptest.NewRecorder()
 	req := httptest.NewRequest(http.MethodPost, "http://localhost/something1", nil)
+	req.Header.Set("User-Agent", "whatever")
 	ts.API.handler.ServeHTTP(w, req)
 
 	req = httptest.NewRequest(http.MethodGet, "http://localhost/something2", nil)
+	req.Header.Set("User-Agent", "whatever")
 	ts.API.handler.ServeHTTP(w, req)
 
 	spanStubs := exporter.GetSpans()
@@ -80,6 +82,9 @@ func (ts *OpenTelemetryTracerTestSuite) TestOpenTelemetryTracer_Spans() {
 		statusCode1 := getAttribute(attributes1, semconv.HTTPStatusCodeKey)
 		assert.Equal(ts.T(), int64(404), statusCode1.AsInt64())
 
+		userAgent1 := getAttribute(attributes1, semconv.HTTPUserAgentKey)
+		assert.Equal(ts.T(), "stripped", userAgent1.AsString())
+
 		attributes2 := spans[1].Attributes()
 		method2 := getAttribute(attributes2, semconv.HTTPMethodKey)
 		assert.Equal(ts.T(), "GET", method2.AsString())
@@ -87,5 +92,8 @@ func (ts *OpenTelemetryTracerTestSuite) TestOpenTelemetryTracer_Spans() {
 		assert.Equal(ts.T(), "http://localhost/something2", url2.AsString())
 		statusCode2 := getAttribute(attributes2, semconv.HTTPStatusCodeKey)
 		assert.Equal(ts.T(), int64(404), statusCode2.AsInt64())
+
+		userAgent2 := getAttribute(attributes2, semconv.HTTPUserAgentKey)
+		assert.Equal(ts.T(), "stripped", userAgent2.AsString())
 	}
 }
diff --git a/internal/api/opentracer_test.go b/internal/api/opentracer_test.go
deleted file mode 100644
index 411e731d..00000000
--- a/internal/api/opentracer_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package api
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/opentracing/opentracing-go"
-	"github.com/opentracing/opentracing-go/mocktracer"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"github.com/stretchr/testify/suite"
-	"github.com/supabase/gotrue/internal/conf"
-	"github.com/supabase/gotrue/internal/storage"
-)
-
-type OpenTracerTestSuite struct {
-	suite.Suite
-	API    *API
-	Config *conf.GlobalConfiguration
-}
-
-func TestOpenTracer(t *testing.T) {
-	api, config, err := setupAPIForTestWithCallback(func(config *conf.GlobalConfiguration, conn *storage.Connection) {
-		if config != nil {
-			config.Tracing.Enabled = true
-			config.Tracing.Exporter = conf.OpenTracing
-		}
-	})
-
-	require.NoError(t, err)
-
-	ts := &OpenTracerTestSuite{
-		API:    api,
-		Config: config,
-	}
-	defer api.db.Close()
-
-	suite.Run(t, ts)
-}
-
-func (ts *OpenTracerTestSuite) TestOpenTracer_Spans() {
-	mt := mocktracer.New()
-	opentracing.SetGlobalTracer(mt)
-
-	w := httptest.NewRecorder()
-	req := httptest.NewRequest(http.MethodPost, "http://localhost/something1", nil)
-	ts.API.handler.ServeHTTP(w, req)
-	req = httptest.NewRequest(http.MethodGet, "http://localhost/something2", nil)
-	ts.API.handler.ServeHTTP(w, req)
-
-	spans := mt.FinishedSpans()
-	if assert.Equal(ts.T(), 2, len(spans)) {
-		assert.Equal(ts.T(), "POST", spans[0].Tag("http.method"))
-		assert.Equal(ts.T(), "/something1", spans[0].Tag("http.url"))
-		assert.Equal(ts.T(), "POST /something1", spans[0].Tag("resource.name"))
-		assert.Equal(ts.T(), "404", spans[0].Tag("http.status_code"))
-		assert.NotEmpty(ts.T(), spans[0].Tag("http.request_id"))
-
-		assert.Equal(ts.T(), "GET", spans[1].Tag("http.method"))
-		assert.Equal(ts.T(), "/something2", spans[1].Tag("http.url"))
-		assert.Equal(ts.T(), "GET /something2", spans[1].Tag("resource.name"))
-		assert.Equal(ts.T(), "404", spans[1].Tag("http.status_code"))
-		assert.NotEmpty(ts.T(), spans[1].Tag("http.request_id"))
-	}
-}
diff --git a/internal/api/token_test.go b/internal/api/token_test.go
index 27dcfb96..b07d8ad2 100644
--- a/internal/api/token_test.go
+++ b/internal/api/token_test.go
@@ -60,6 +60,122 @@ func (ts *TokenTestSuite) SetupTest() {
 	require.NoError(ts.T(), err, "Error creating refresh token")
 }
 
+func (ts *TokenTestSuite) TestSessionTimebox() {
+	timebox := 10 * time.Second
+
+	ts.API.config.Sessions.Timebox = &timebox
+	ts.API.overrideTime = func() time.Time {
+		return time.Now().Add(timebox).Add(time.Second)
+	}
+
+	defer func() {
+		ts.API.overrideTime = nil
+	}()
+
+	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)
+
+	var firstResult struct {
+		Error            string `json:"error"`
+		ErrorDescription string `json:"error_description"`
+	}
+
+	assert.NoError(ts.T(), json.NewDecoder(w.Result().Body).Decode(&firstResult))
+	assert.Equal(ts.T(), "invalid_grant", firstResult.Error)
+	assert.Equal(ts.T(), "Invalid Refresh Token: Session Expired", firstResult.ErrorDescription)
+}
+
+func (ts *TokenTestSuite) TestSessionInactivityTimeout() {
+	inactivityTimeout := 10 * time.Second
+
+	ts.API.config.Sessions.InactivityTimeout = &inactivityTimeout
+	ts.API.overrideTime = func() time.Time {
+		return time.Now().Add(inactivityTimeout).Add(time.Second)
+	}
+
+	defer func() {
+		ts.API.config.Sessions.InactivityTimeout = nil
+		ts.API.overrideTime = nil
+	}()
+
+	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)
+
+	var firstResult struct {
+		Error            string `json:"error"`
+		ErrorDescription string `json:"error_description"`
+	}
+
+	assert.NoError(ts.T(), json.NewDecoder(w.Result().Body).Decode(&firstResult))
+	assert.Equal(ts.T(), "invalid_grant", firstResult.Error)
+	assert.Equal(ts.T(), "Invalid Refresh Token: Session Expired (Inactivity)", firstResult.ErrorDescription)
+}
+
+func (ts *TokenTestSuite) TestFailedToSaveRefreshTokenResultCase() {
+	var buffer bytes.Buffer
+
+	// first refresh
+	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)
+
+	var firstResult struct {
+		RefreshToken string `json:"refresh_token"`
+	}
+
+	assert.NoError(ts.T(), json.NewDecoder(w.Result().Body).Decode(&firstResult))
+	assert.NotEmpty(ts.T(), firstResult.RefreshToken)
+
+	// pretend that the browser wasn't able to save the firstResult,
+	// run again with the first refresh token
+	buffer = bytes.Buffer{}
+
+	// second refresh with the reused refresh token
+	require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{
+		"refresh_token": ts.RefreshToken.Token,
+	}))
+
+	w = httptest.NewRecorder()
+	ts.API.handler.ServeHTTP(w, req)
+	assert.Equal(ts.T(), http.StatusOK, w.Code)
+
+	var secondResult struct {
+		RefreshToken string `json:"refresh_token"`
+	}
+
+	assert.NoError(ts.T(), json.NewDecoder(w.Result().Body).Decode(&secondResult))
+	assert.NotEmpty(ts.T(), secondResult.RefreshToken)
+
+	// new refresh token is not being issued but the active one from
+	// the first refresh that failed to save is stored
+	assert.Equal(ts.T(), firstResult.RefreshToken, secondResult.RefreshToken)
+}
+
 func (ts *TokenTestSuite) TestRateLimitTokenRefresh() {
 	var buffer bytes.Buffer
 	req := httptest.NewRequest(http.MethodPost, "http://localhost/token", &buffer)
@@ -248,7 +364,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() {
 			desc:                        "Invalid refresh, revoke third token",
 			refreshTokenRotationEnabled: true,
 			reuseInterval:               0,
-			refreshToken:                second.Token,
+			refreshToken:                first.Token,
 			expectedCode:                http.StatusBadRequest,
 			expectedBody: map[string]interface{}{
 				"error":             "invalid_grant",
diff --git a/internal/models/cleanup_test.go b/internal/models/cleanup_test.go
index c38cf72f..79052c51 100644
--- a/internal/models/cleanup_test.go
+++ b/internal/models/cleanup_test.go
@@ -1,8 +1,8 @@
 package models
 
 import (
-	"fmt"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/require"
 
@@ -10,29 +10,24 @@ import (
 	"github.com/supabase/gotrue/internal/storage/test"
 )
 
-func TestCleanupSQL(t *testing.T) {
+func TestCleanup(t *testing.T) {
 	globalConfig, err := conf.LoadGlobal(modelsTestConfig)
 	require.NoError(t, err)
 	conn, err := test.SetupDBConnection(globalConfig)
 	require.NoError(t, err)
 
-	for _, statement := range CleanupStatements {
-		_, err := conn.RawQuery(statement).ExecWithCount()
-		require.NoError(t, err, statement)
+	sessionTimebox := 10 * time.Second
+	sessionInactivityTimeout := 5 * time.Second
+
+	cleanup := &Cleanup{
+		SessionTimebox:           &sessionTimebox,
+		SessionInactivityTimeout: &sessionInactivityTimeout,
 	}
-}
 
-func TestCleanup(t *testing.T) {
-	globalConfig, err := conf.LoadGlobal(modelsTestConfig)
-	require.NoError(t, err)
-	conn, err := test.SetupDBConnection(globalConfig)
-	require.NoError(t, err)
+	cleanup.Setup()
 
-	for _, statement := range CleanupStatements {
-		_, err := Cleanup(conn)
-		if err != nil {
-			fmt.Printf("%v %t\n", err, err)
-		}
-		require.NoError(t, err, statement)
+	for i := 0; i < 100; i += 1 {
+		_, err := cleanup.Clean(conn)
+		require.NoError(t, err)
 	}
 }
diff --git a/internal/models/linking_test.go b/internal/models/linking_test.go
index 8bc60a77..ecb80dc8 100644
--- a/internal/models/linking_test.go
+++ b/internal/models/linking_test.go
@@ -71,7 +71,7 @@ func (ts *AccountLinkingTestSuite) TestCreateAccountDecisionWithAccounts() {
 	require.NoError(ts.T(), err)
 	require.NoError(ts.T(), ts.db.Create(identityB))
 
-	// when there are no accounts in the system -- conventional provider
+	// when the email doesn't exist in the system -- conventional provider
 	decision, err := DetermineAccountLinking(ts.db, "provider", "abcdefgh", []string{"other@example.com"})
 	require.NoError(ts.T(), err)
 
@@ -82,18 +82,6 @@ func (ts *AccountLinkingTestSuite) TestCreateAccountDecisionWithAccounts() {
 	require.NoError(ts.T(), err)
 
 	require.Equal(ts.T(), decision.Decision, CreateAccount)
-
-	// when looking for an email that doesn't exist at all
-	decision, err = DetermineAccountLinking(ts.db, "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387", "abcdefgh", []string{"other@samltest.id"})
-	require.NoError(ts.T(), err)
-
-	require.Equal(ts.T(), decision.Decision, CreateAccount)
-
-	// when looking for an email that doesn't exist in the SSO linking domain
-	decision, err = DetermineAccountLinking(ts.db, "sso:f06f9e3d-ff92-4c47-a179-7acf1fda6387", "abcdefgh", []string{"text@example.com"})
-	require.NoError(ts.T(), err)
-
-	require.Equal(ts.T(), decision.Decision, CreateAccount)
 }
 
 func (ts *AccountLinkingTestSuite) TestAccountExists() {
diff --git a/internal/models/user_test.go b/internal/models/user_test.go
index 564e6fd8..f0b85a43 100644
--- a/internal/models/user_test.go
+++ b/internal/models/user_test.go
@@ -215,40 +215,37 @@ func (ts *UserTestSuite) TestRemoveUnconfirmedIdentities() {
 	require.NoError(ts.T(), err)
 	require.NoError(ts.T(), ts.db.Create(idEmail))
 
-	user.Identities = append(user.Identities, *idEmail)
-
 	idPhone, err := NewIdentity(user, "phone", map[string]interface{}{
 		"sub": "+29382983298",
 	})
 	require.NoError(ts.T(), err)
 	require.NoError(ts.T(), ts.db.Create(idPhone))
 
-	user.Identities = append(user.Identities, *idPhone)
-
 	idTwitter, err := NewIdentity(user, "twitter", map[string]interface{}{
 		"sub": "test_twitter_user_id",
 	})
 	require.NoError(ts.T(), err)
 	require.NoError(ts.T(), ts.db.Create(idTwitter))
 
-	user.Identities = append(user.Identities, *idTwitter)
+	user.Identities = append(user.Identities, *idEmail, *idPhone, *idTwitter)
 
 	// reload the user
 	require.NoError(ts.T(), ts.db.Load(user))
 
 	require.False(ts.T(), user.IsConfirmed(), "user's email must not be confirmed")
 
-	require.NoError(ts.T(), user.RemoveUnconfirmedIdentities(ts.db))
+	require.NoError(ts.T(), user.RemoveUnconfirmedIdentities(ts.db, idTwitter))
 
+	// reload the user to check that identities are deleted from the db too
+	require.NoError(ts.T(), ts.db.Load(user))
 	require.Empty(ts.T(), user.EncryptedPassword, "password still remains in user")
 
-	require.Len(ts.T(), user.Identities, 2, "only two identity must be remaining")
-	require.Equal(ts.T(), idPhone.ID, user.Identities[0].ID, "remaining identity is not the expected one")
-	require.Equal(ts.T(), idTwitter.ID, user.Identities[1].ID, "remaining identity is not the expected one")
+	require.Len(ts.T(), user.Identities, 1, "only one identity must be remaining")
+	require.Equal(ts.T(), idTwitter.ID, user.Identities[0].ID, "remaining identity is not the expected one")
 
 	require.NotNil(ts.T(), user.AppMetaData)
-	require.Equal(ts.T(), user.AppMetaData["provider"], "phone")
-	require.Equal(ts.T(), user.AppMetaData["providers"], []string{"phone", "twitter"})
+	require.Equal(ts.T(), user.AppMetaData["provider"], "twitter")
+	require.Equal(ts.T(), user.AppMetaData["providers"], []string{"twitter"})
 }
 
 func (ts *UserTestSuite) TestConfirmEmailChange() {