Repository: SkygearIO/skygear-server
Pull request #1017 (2019-08-27 02:55:03)

Use Redis as session store
ref #1010 


Patch:
diff --git a/cmd/auth/main.go b/cmd/auth/main.go
index 903b30b2..fd0fc656 100644
--- a/cmd/auth/main.go
+++ b/cmd/auth/main.go
@@ -10,6 +10,8 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/skygeario/skygear-server/pkg/core/redis"
+
 	"github.com/joho/godotenv"
 	"github.com/kelseyhightower/envconfig"
 
@@ -34,10 +36,11 @@ import (
 
 type configuration struct {
 	Standalone                        bool
-	StandaloneTenantConfigurationFile string `envconfig:"STANDALONE_TENANT_CONFIG_FILE" default:"standalone-tenant-config.yaml"`
-	PathPrefix                        string `envconfig:"PATH_PREFIX"`
-	Host                              string `default:"localhost:3000"`
-	ValidHosts                        string `envconfig:"VALID_HOSTS"`
+	StandaloneTenantConfigurationFile string              `envconfig:"STANDALONE_TENANT_CONFIG_FILE" default:"standalone-tenant-config.yaml"`
+	PathPrefix                        string              `envconfig:"PATH_PREFIX"`
+	Host                              string              `default:"localhost:3000"`
+	ValidHosts                        string              `envconfig:"VALID_HOSTS"`
+	Redis                             redis.Configuration `envconfig:"REDIS"`
 }
 
 /*
@@ -79,6 +82,9 @@ func main() {
 	if configuration.ValidHosts == "" {
 		configuration.ValidHosts = configuration.Host
 	}
+	if configuration.Redis.Host == "" {
+		log.Fatal("REDIS_HOST is not provided")
+	}
 
 	// default template initialization
 	templateEngine := template.NewEngine()
@@ -88,11 +94,12 @@ func main() {
 	logging.SetModule("auth")
 
 	asyncTaskExecutor := async.NewExecutor()
+	dbPool := db.NewPool()
+	redisPool := redis.NewPool(configuration.Redis)
 	authDependency := auth.DependencyMap{
 		AsyncTaskExecutor: asyncTaskExecutor,
 		TemplateEngine:    templateEngine,
 	}
-	dbPool := db.NewPool()
 
 	task.AttachVerifyCodeSendTask(asyncTaskExecutor, authDependency)
 	task.AttachPwHousekeeperTask(asyncTaskExecutor, authDependency)
@@ -100,6 +107,13 @@ func main() {
 
 	authContextResolverFactory := resolver.AuthContextResolverFactory{}
 
+	setupCtxFn := server.SetupContextFunc(func(ctx context.Context) context.Context {
+		return redis.WithRedis(ctx, redisPool)
+	})
+	cleanupCtxFn := server.CleanupContextFunc(func(ctx context.Context) {
+		redis.CloseConn(ctx)
+	})
+
 	var srv server.Server
 	if configuration.Standalone {
 		filename := configuration.StandaloneTenantConfigurationFile
@@ -122,7 +136,7 @@ func main() {
 
 		serverOption := server.DefaultOption()
 		serverOption.GearPathPrefix = configuration.PathPrefix
-		srv = server.NewServerWithOption(configuration.Host, authContextResolverFactory, dbPool, serverOption)
+		srv = server.NewServerWithOption(configuration.Host, authContextResolverFactory, dbPool, setupCtxFn, cleanupCtxFn, serverOption)
 		srv.Use(middleware.TenantConfigurationMiddleware{
 			ConfigurationProvider: middleware.ConfigurationProviderFunc(func(_ *http.Request) (config.TenantConfiguration, error) {
 				return *tenantConfig, nil
@@ -132,7 +146,7 @@ func main() {
 		srv.Use(middleware.RequestIDMiddleware{}.Handle)
 		srv.Use(middleware.CORSMiddleware{}.Handle)
 	} else {
-		srv = server.NewServer(configuration.Host, authContextResolverFactory, dbPool)
+		srv = server.NewServer(configuration.Host, authContextResolverFactory, dbPool, setupCtxFn, cleanupCtxFn)
 	}
 
 	handler.AttachSignupHandler(&srv, authDependency)
diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go
index 0deba6f4..cf71f472 100644
--- a/cmd/gateway/main.go
+++ b/cmd/gateway/main.go
@@ -13,6 +13,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/logging"
 	coreMiddleware "github.com/skygeario/skygear-server/pkg/core/middleware"
+	"github.com/skygeario/skygear-server/pkg/core/redis"
 	"github.com/skygeario/skygear-server/pkg/core/server"
 	"github.com/skygeario/skygear-server/pkg/gateway"
 	gatewayConfig "github.com/skygeario/skygear-server/pkg/gateway/config"
@@ -72,6 +73,7 @@ func main() {
 
 	gatewayDependency := gateway.DependencyMap{}
 	dbPool := db.NewPool()
+	redisPool := redis.NewPool(config.Redis)
 
 	rr := mux.NewRouter()
 	rr.HandleFunc("/_healthz", HealthCheckHandler)
@@ -111,6 +113,7 @@ func main() {
 	cr.Use(func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			r = db.InitRequestDBContext(r, dbPool)
+			r = r.WithContext(redis.WithRedis(r.Context(), redisPool))
 			next.ServeHTTP(w, r)
 		})
 	})
diff --git a/docker-compose.yml b/docker-compose.yml
index 4338314d..833aa164 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,6 +13,13 @@ services:
     - db_data:/var/lib/postgresql/data
     ports:
     - "5432:5432"
+  redis:
+    image: redis:5.0
+    command: redis-server --appendonly yes
+    volumes:
+      - redis_data:/data
+    ports:
+      - "6379:6379"
   gateway:
     build:
       dockerfile: ./cmd/gateway/Dockerfile
@@ -21,21 +28,27 @@ services:
     - "DATABASE_URL=postgres://postgres:@db/postgres?sslmode=disable"
     - "HOST=0.0.0.0:3001"
     - "AUTH_LIVE_URL=http://auth:3000"
+    - "REDIS_HOST=redis"
     ports:
     - "3001:3001"
     depends_on:
     - db
+    - redis
   auth:
     build:
       dockerfile: ./cmd/auth/Dockerfile
       context: .
     environment:
     - "HOST=0.0.0.0:3000"
+    - "REDIS_HOST=redis"
     ports:
     - "3000:3000"
     depends_on:
     - db
+    - redis
 
 volumes:
   db_data:
-    driver: local
\ No newline at end of file
+    driver: local
+  redis_data:
+    driver: local
diff --git a/go.mod b/go.mod
index 3983a5ed..dbd05fb3 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/go-gomail/gomail v0.0.0-20150902115704-41f357289737
 	github.com/go-sql-driver/mysql v1.4.1 // indirect
 	github.com/golang/mock v1.3.1
+	github.com/gomodule/redigo v2.0.0+incompatible
 	github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
 	github.com/gorilla/context v1.1.1 // indirect
 	github.com/gorilla/mux v1.6.2
diff --git a/go.sum b/go.sum
index f0990e6a..a082d159 100644
--- a/go.sum
+++ b/go.sum
@@ -28,6 +28,8 @@ github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
diff --git a/pkg/auth/dependency/hook/deliverer_impl.go b/pkg/auth/dependency/hook/deliverer_impl.go
index 2107ab84..4b28699a 100644
--- a/pkg/auth/dependency/hook/deliverer_impl.go
+++ b/pkg/auth/dependency/hook/deliverer_impl.go
@@ -9,7 +9,7 @@ import (
 	"net/url"
 	gotime "time"
 
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
+	"github.com/skygeario/skygear-server/pkg/core/time"
 
 	"github.com/skygeario/skygear-server/pkg/core/crypto"
 	"github.com/skygeario/skygear-server/pkg/core/http"
diff --git a/pkg/auth/dependency/hook/provider_impl.go b/pkg/auth/dependency/hook/provider_impl.go
index aac8fa41..b776b474 100644
--- a/pkg/auth/dependency/hook/provider_impl.go
+++ b/pkg/auth/dependency/hook/provider_impl.go
@@ -7,12 +7,12 @@ import (
 
 	corehttp "github.com/skygeario/skygear-server/pkg/core/http"
 
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
 	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
+	"github.com/skygeario/skygear-server/pkg/core/time"
 )
 
 type providerImpl struct {
@@ -186,7 +186,7 @@ func (provider *providerImpl) makeContext() event.Context {
 		principalID = nil
 	} else {
 		userID = &provider.AuthContext.AuthInfo().ID
-		principalID = &provider.AuthContext.Token().PrincipalID
+		principalID = &provider.AuthContext.Session().PrincipalID
 	}
 
 	return event.Context{
diff --git a/pkg/auth/dependency/userverify/provider.go b/pkg/auth/dependency/userverify/provider.go
index 1cb8da1b..7dd083e2 100644
--- a/pkg/auth/dependency/userverify/provider.go
+++ b/pkg/auth/dependency/userverify/provider.go
@@ -3,9 +3,9 @@ package userverify
 import (
 	gotime "time"
 
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
 	"github.com/skygeario/skygear-server/pkg/core/config"
+	"github.com/skygeario/skygear-server/pkg/core/time"
 
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 
diff --git a/pkg/auth/handler/change_password.go b/pkg/auth/handler/change_password.go
index d93930a4..11104136 100644
--- a/pkg/auth/handler/change_password.go
+++ b/pkg/auth/handler/change_password.go
@@ -4,6 +4,8 @@ import (
 	"encoding/json"
 	"net/http"
 
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 
 	"github.com/skygeario/skygear-server/pkg/auth"
@@ -18,7 +20,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
 	"github.com/skygeario/skygear-server/pkg/core/db"
@@ -114,7 +115,7 @@ type ChangePasswordHandler struct {
 	PasswordAuthProvider password.Provider          `dependency:"PasswordAuthProvider"`
 	IdentityProvider     principal.IdentityProvider `dependency:"IdentityProvider"`
 	PasswordChecker      *authAudit.PasswordChecker `dependency:"PasswordChecker"`
-	TokenStore           authtoken.Store            `dependency:"TokenStore"`
+	SessionProvider      session.Provider           `dependency:"SessionProvider"`
 	TxContext            db.TxContext               `dependency:"TxContext"`
 	UserProfileStore     userprofile.Store          `dependency:"UserProfileStore"`
 	HookProvider         hook.Provider              `dependency:"HookProvider"`
@@ -154,7 +155,7 @@ func (h ChangePasswordHandler) Handle(req interface{}) (resp interface{}, err er
 
 	principal := principals[0]
 	for _, p := range principals {
-		if p.ID == h.AuthContext.Token().PrincipalID {
+		if p.ID == h.AuthContext.Session().PrincipalID {
 			principal = p
 		}
 		if !p.IsSamePassword(payload.OldPassword) {
@@ -174,16 +175,13 @@ func (h ChangePasswordHandler) Handle(req interface{}) (resp interface{}, err er
 		return
 	}
 
-	// generate access-token
-	token, err := h.TokenStore.NewToken(authinfo.ID, h.AuthContext.Token().PrincipalID)
+	// refresh session
+	session := h.AuthContext.Session()
+	err = h.SessionProvider.Refresh(h.AuthContext.Session())
 	if err != nil {
 		panic(err)
 	}
 
-	if err = h.TokenStore.Put(&token); err != nil {
-		panic(err)
-	}
-
 	// Get Profile
 	var userProfile userprofile.UserProfile
 	if userProfile, err = h.UserProfileStore.GetUserProfile(authinfo.ID); err != nil {
@@ -207,7 +205,7 @@ func (h ChangePasswordHandler) Handle(req interface{}) (resp interface{}, err er
 		return
 	}
 
-	resp = model.NewAuthResponse(user, identity, token.AccessToken)
+	resp = model.NewAuthResponse(user, identity, session.AccessToken)
 
 	h.AuditTrail.Log(audit.Entry{
 		UserID: authinfo.ID,
diff --git a/pkg/auth/handler/forgotpwd/reset.go b/pkg/auth/handler/forgotpwd/reset.go
index 6b11d3c7..22cbbc6e 100644
--- a/pkg/auth/handler/forgotpwd/reset.go
+++ b/pkg/auth/handler/forgotpwd/reset.go
@@ -19,7 +19,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
 	"github.com/skygeario/skygear-server/pkg/core/db"
@@ -121,7 +120,6 @@ func (payload ForgotPasswordResetPayload) Validate() error {
 type ForgotPasswordResetHandler struct {
 	CodeGenerator        *forgotpwdemail.CodeGenerator `dependency:"ForgotPasswordCodeGenerator"`
 	PasswordChecker      *authAudit.PasswordChecker    `dependency:"PasswordChecker"`
-	TokenStore           authtoken.Store               `dependency:"TokenStore"`
 	AuthInfoStore        authinfo.Store                `dependency:"AuthInfoStore"`
 	UserProfileStore     userprofile.Store             `dependency:"UserProfileStore"`
 	HookProvider         hook.Provider                 `dependency:"HookProvider"`
diff --git a/pkg/auth/handler/forgotpwd/reset_form.go b/pkg/auth/handler/forgotpwd/reset_form.go
index 5fa08769..a443ca38 100644
--- a/pkg/auth/handler/forgotpwd/reset_form.go
+++ b/pkg/auth/handler/forgotpwd/reset_form.go
@@ -19,7 +19,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/task"
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
 	"github.com/skygeario/skygear-server/pkg/core/db"
@@ -100,7 +99,6 @@ func (payload *ForgotPasswordResetFormPayload) Validate() error {
 type ForgotPasswordResetFormHandler struct {
 	CodeGenerator             *forgotpwdemail.CodeGenerator             `dependency:"ForgotPasswordCodeGenerator"`
 	PasswordChecker           *audit.PasswordChecker                    `dependency:"PasswordChecker"`
-	TokenStore                authtoken.Store                           `dependency:"TokenStore"`
 	AuthInfoStore             authinfo.Store                            `dependency:"AuthInfoStore"`
 	PasswordAuthProvider      password.Provider                         `dependency:"PasswordAuthProvider"`
 	UserProfileStore          userprofile.Store                         `dependency:"UserProfileStore"`
diff --git a/pkg/auth/handler/login.go b/pkg/auth/handler/login.go
index e94a10fa..ab31a561 100644
--- a/pkg/auth/handler/login.go
+++ b/pkg/auth/handler/login.go
@@ -13,9 +13,9 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/model"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
 	"github.com/skygeario/skygear-server/pkg/core/inject"
@@ -104,7 +104,7 @@ func (p LoginRequestPayload) Validate() error {
 		@Callback user_sync {UserSyncEvent}
 */
 type LoginHandler struct {
-	TokenStore           authtoken.Store            `dependency:"TokenStore"`
+	SessionProvider      session.Provider           `dependency:"SessionProvider"`
 	AuthInfoStore        authinfo.Store             `dependency:"AuthInfoStore"`
 	PasswordAuthProvider password.Provider          `dependency:"PasswordAuthProvider"`
 	IdentityProvider     principal.IdentityProvider `dependency:"IdentityProvider"`
@@ -191,16 +191,12 @@ func (h LoginHandler) Handle(req interface{}) (resp interface{}, err error) {
 		return
 	}
 
-	// generate access-token
-	token, err := h.TokenStore.NewToken(fetchedAuthInfo.ID, principal.ID)
+	// generate session
+	session, err := h.SessionProvider.Create(fetchedAuthInfo.ID, principal.ID)
 	if err != nil {
 		panic(err)
 	}
 
-	if err = h.TokenStore.Put(&token); err != nil {
-		panic(err)
-	}
-
 	// Get Profile
 	var userProfile userprofile.UserProfile
 	if userProfile, err = h.UserProfileStore.GetUserProfile(fetchedAuthInfo.ID); err != nil {
@@ -238,7 +234,7 @@ func (h LoginHandler) Handle(req interface{}) (resp interface{}, err error) {
 		return
 	}
 
-	resp = model.NewAuthResponse(user, identity, token.AccessToken)
+	resp = model.NewAuthResponse(user, identity, session.AccessToken)
 
 	return
 }
diff --git a/pkg/auth/handler/logout.go b/pkg/auth/handler/logout.go
index 4350ce55..1427e11f 100644
--- a/pkg/auth/handler/logout.go
+++ b/pkg/auth/handler/logout.go
@@ -11,13 +11,12 @@ import (
 	authModel "github.com/skygeario/skygear-server/pkg/auth/model"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
 	"github.com/skygeario/skygear-server/pkg/core/inject"
-	"github.com/skygeario/skygear-server/pkg/core/model"
 	"github.com/skygeario/skygear-server/pkg/core/server"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 )
@@ -55,19 +54,6 @@ func (f LogoutHandlerFactory) ProvideAuthzPolicy() authz.Policy {
 	)
 }
 
-// LogoutRequestPayload is request payload of logout handler
-type LogoutRequestPayload struct {
-	AccessToken string
-}
-
-// Validate request payload
-func (p LogoutRequestPayload) Validate() error {
-	if p.AccessToken == "" {
-		return skyerr.NewError(skyerr.NotAuthenticated, "missing access token")
-	}
-	return nil
-}
-
 /*
 	@Operation POST /logout - Logout current session
 		Logout current session.
@@ -85,7 +71,7 @@ type LogoutHandler struct {
 	AuthContext      coreAuth.ContextGetter     `dependency:"AuthContextGetter"`
 	UserProfileStore userprofile.Store          `dependency:"UserProfileStore"`
 	IdentityProvider principal.IdentityProvider `dependency:"IdentityProvider"`
-	TokenStore       authtoken.Store            `dependency:"TokenStore"`
+	SessionProvider  session.Provider           `dependency:"SessionProvider"`
 	AuditTrail       audit.Trail                `dependency:"AuditTrail"`
 	HookProvider     hook.Provider              `dependency:"HookProvider"`
 	TxContext        db.TxContext               `dependency:"TxContext"`
@@ -97,35 +83,25 @@ func (h LogoutHandler) WithTx() bool {
 
 // DecodeRequest decode request payload
 func (h LogoutHandler) DecodeRequest(request *http.Request) (handler.RequestPayload, error) {
-	payload := LogoutRequestPayload{}
-	payload.AccessToken = model.GetAccessToken(request)
-	return payload, nil
+	return handler.EmptyRequestPayload{}, nil
 }
 
 // Handle api request
 func (h LogoutHandler) Handle(req interface{}) (resp interface{}, err error) {
-	payload := req.(LogoutRequestPayload)
-
-	accessToken := payload.AccessToken
-
-	if err = h.TokenStore.Delete(accessToken); err != nil {
-		if _, notfound := err.(*authtoken.NotFoundError); notfound {
-			err = nil
-		}
-	}
-	if err != nil {
+	if err = h.SessionProvider.Invalidate(h.AuthContext.Session().ID); err != nil {
 		err = skyerr.MakeError(err)
-	} else {
-		resp = map[string]string{}
+		return
 	}
 
+	resp = map[string]string{}
+
 	var profile userprofile.UserProfile
 	if profile, err = h.UserProfileStore.GetUserProfile(h.AuthContext.AuthInfo().ID); err != nil {
 		return
 	}
 
 	var principal principal.Principal
-	if principal, err = h.IdentityProvider.GetPrincipalByID(h.AuthContext.Token().PrincipalID); err != nil {
+	if principal, err = h.IdentityProvider.GetPrincipalByID(h.AuthContext.Session().PrincipalID); err != nil {
 		return
 	}
 
diff --git a/pkg/auth/handler/me.go b/pkg/auth/handler/me.go
index b8c2d3e8..9992f600 100644
--- a/pkg/auth/handler/me.go
+++ b/pkg/auth/handler/me.go
@@ -11,7 +11,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/model"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
 	"github.com/skygeario/skygear-server/pkg/core/db"
@@ -64,7 +63,6 @@ func (f MeHandlerFactory) ProvideAuthzPolicy() authz.Policy {
 type MeHandler struct {
 	AuthContext          coreAuth.ContextGetter     `dependency:"AuthContextGetter"`
 	TxContext            db.TxContext               `dependency:"TxContext"`
-	TokenStore           authtoken.Store            `dependency:"TokenStore"`
 	AuthInfoStore        authinfo.Store             `dependency:"AuthInfoStore"`
 	UserProfileStore     userprofile.Store          `dependency:"UserProfileStore"`
 	PasswordAuthProvider password.Provider          `dependency:"PasswordAuthProvider"`
@@ -81,7 +79,7 @@ func (h MeHandler) DecodeRequest(request *http.Request) (handler.RequestPayload,
 
 func (h MeHandler) Handle(req interface{}) (resp interface{}, err error) {
 	authInfo := h.AuthContext.AuthInfo()
-	principalID := h.AuthContext.Token().PrincipalID
+	principalID := h.AuthContext.Session().PrincipalID
 
 	// Get Profile
 	var userProfile userprofile.UserProfile
diff --git a/pkg/auth/handler/reset_password.go b/pkg/auth/handler/reset_password.go
index 29b0bcdd..ced6d60b 100644
--- a/pkg/auth/handler/reset_password.go
+++ b/pkg/auth/handler/reset_password.go
@@ -16,7 +16,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
 	"github.com/skygeario/skygear-server/pkg/core/db"
@@ -107,7 +106,6 @@ func (p ResetPasswordRequestPayload) Validate() error {
 type ResetPasswordHandler struct {
 	PasswordChecker      *authAudit.PasswordChecker `dependency:"PasswordChecker"`
 	UserProfileStore     userprofile.Store          `dependency:"UserProfileStore"`
-	TokenStore           authtoken.Store            `dependency:"TokenStore"`
 	AuthInfoStore        authinfo.Store             `dependency:"AuthInfoStore"`
 	PasswordAuthProvider password.Provider          `dependency:"PasswordAuthProvider"`
 	AuditTrail           audit.Trail                `dependency:"AuditTrail"`
diff --git a/pkg/auth/handler/signup.go b/pkg/auth/handler/signup.go
index bcbaf7c2..eda7b22b 100644
--- a/pkg/auth/handler/signup.go
+++ b/pkg/auth/handler/signup.go
@@ -23,10 +23,10 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
 	"github.com/skygeario/skygear-server/pkg/core/auth/metadata"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -161,7 +161,7 @@ func (p SignupRequestPayload) duplicatedLoginIDs() bool {
 type SignupHandler struct {
 	PasswordChecker         *authAudit.PasswordChecker                         `dependency:"PasswordChecker"`
 	UserProfileStore        userprofile.Store                                  `dependency:"UserProfileStore"`
-	TokenStore              authtoken.Store                                    `dependency:"TokenStore"`
+	SessionProvider         session.Provider                                   `dependency:"SessionProvider"`
 	AuthInfoStore           authinfo.Store                                     `dependency:"AuthInfoStore"`
 	PasswordAuthProvider    password.Provider                                  `dependency:"PasswordAuthProvider"`
 	IdentityProvider        principal.IdentityProvider                         `dependency:"IdentityProvider"`
@@ -290,16 +290,12 @@ func (h SignupHandler) Handle(req interface{}) (resp interface{}, err error) {
 		Event:  audit.EventSignup,
 	})
 
-	// Create auth token
-	tkn, err := h.TokenStore.NewToken(info.ID, loginPrincipal.PrincipalID())
+	// generate session
+	session, err := h.SessionProvider.Create(info.ID, loginPrincipal.PrincipalID())
 	if err != nil {
 		panic(err)
 	}
 
-	if err = h.TokenStore.Put(&tkn); err != nil {
-		panic(err)
-	}
-
 	// Populate the activity time to user
 	info.LastSeenAt = &now
 	if err = h.AuthInfoStore.UpdateAuth(&info); err != nil {
@@ -319,7 +315,7 @@ func (h SignupHandler) Handle(req interface{}) (resp interface{}, err error) {
 		return
 	}
 
-	resp = model.NewAuthResponse(user, loginIdentity, tkn.AccessToken)
+	resp = model.NewAuthResponse(user, loginIdentity, session.AccessToken)
 
 	if h.WelcomeEmailEnabled {
 		h.sendWelcomeEmail(user, payload.LoginIDs)
diff --git a/pkg/auth/handler/sso/auth_handler.go b/pkg/auth/handler/sso/auth_handler.go
index 45920c42..e1cacb25 100644
--- a/pkg/auth/handler/sso/auth_handler.go
+++ b/pkg/auth/handler/sso/auth_handler.go
@@ -13,7 +13,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal/oauth"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 
 	"github.com/skygeario/skygear-server/pkg/auth"
@@ -98,7 +98,7 @@ type AuthHandler struct {
 	OAuthAuthProvider       oauth.Provider              `dependency:"OAuthAuthProvider"`
 	IdentityProvider        principal.IdentityProvider  `dependency:"IdentityProvider"`
 	AuthInfoStore           authinfo.Store              `dependency:"AuthInfoStore"`
-	TokenStore              authtoken.Store             `dependency:"TokenStore"`
+	SessionProvider         session.Provider            `dependency:"SessionProvider"`
 	AuthHandlerHTMLProvider sso.AuthHandlerHTMLProvider `dependency:"AuthHandlerHTMLProvider"`
 	ProviderFactory         *sso.ProviderFactory        `dependency:"SSOProviderFactory"`
 	UserProfileStore        userprofile.Store           `dependency:"UserProfileStore"`
@@ -225,7 +225,7 @@ func (h AuthHandler) getAuthInfo(payload AuthRequestPayload) (oauthAuthInfo sso.
 
 func (h AuthHandler) handle(oauthAuthInfo sso.AuthInfo, state sso.State) (resp interface{}, err error) {
 	respHandler := respHandler{
-		TokenStore:          h.TokenStore,
+		SessionProvider:     h.SessionProvider,
 		AuthInfoStore:       h.AuthInfoStore,
 		OAuthAuthProvider:   h.OAuthAuthProvider,
 		IdentityProvider:    h.IdentityProvider,
diff --git a/pkg/auth/handler/sso/customtoken.go b/pkg/auth/handler/sso/customtoken.go
index 83e3e8d1..a6fe2f99 100644
--- a/pkg/auth/handler/sso/customtoken.go
+++ b/pkg/auth/handler/sso/customtoken.go
@@ -18,9 +18,9 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -140,7 +140,7 @@ func (payload customTokenLoginPayload) Validate() error {
 type CustomTokenLoginHandler struct {
 	TxContext                db.TxContext                    `dependency:"TxContext"`
 	UserProfileStore         userprofile.Store               `dependency:"UserProfileStore"`
-	TokenStore               authtoken.Store                 `dependency:"TokenStore"`
+	SessionProvider          session.Provider                `dependency:"SessionProvider"`
 	AuthInfoStore            authinfo.Store                  `dependency:"AuthInfoStore"`
 	CustomTokenAuthProvider  customtoken.Provider            `dependency:"CustomTokenAuthProvider"`
 	IdentityProvider         principal.IdentityProvider      `dependency:"IdentityProvider"`
@@ -279,16 +279,12 @@ func (h CustomTokenLoginHandler) Handle(req interface{}) (resp interface{}, err
 		}
 	}
 
-	// Create auth token
-	tkn, err := h.TokenStore.NewToken(info.ID, principal.ID)
+	// generate session
+	session, err := h.SessionProvider.Create(info.ID, principal.ID)
 	if err != nil {
 		panic(err)
 	}
 
-	if err = h.TokenStore.Put(&tkn); err != nil {
-		panic(err)
-	}
-
 	var sessionCreateReason event.SessionCreateReason
 	if createNewUser {
 		sessionCreateReason = event.SessionCreateReasonSignup
@@ -321,7 +317,7 @@ func (h CustomTokenLoginHandler) Handle(req interface{}) (resp interface{}, err
 		return
 	}
 
-	resp = model.NewAuthResponse(user, identity, tkn.AccessToken)
+	resp = model.NewAuthResponse(user, identity, session.AccessToken)
 
 	// TODO: audit trail
 	if createNewUser && h.WelcomeEmailEnabled {
diff --git a/pkg/auth/handler/sso/login.go b/pkg/auth/handler/sso/login.go
index e842c5f0..92204253 100644
--- a/pkg/auth/handler/sso/login.go
+++ b/pkg/auth/handler/sso/login.go
@@ -18,9 +18,9 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -113,7 +113,7 @@ type LoginHandler struct {
 	OAuthAuthProvider   oauth.Provider             `dependency:"OAuthAuthProvider"`
 	IdentityProvider    principal.IdentityProvider `dependency:"IdentityProvider"`
 	AuthInfoStore       authinfo.Store             `dependency:"AuthInfoStore"`
-	TokenStore          authtoken.Store            `dependency:"TokenStore"`
+	SessionProvider     session.Provider           `dependency:"SessionProvider"`
 	ProviderFactory     *sso.ProviderFactory       `dependency:"SSOProviderFactory"`
 	UserProfileStore    userprofile.Store          `dependency:"UserProfileStore"`
 	HookProvider        hook.Provider              `dependency:"HookProvider"`
@@ -168,7 +168,7 @@ func (h LoginHandler) Handle(req interface{}) (resp interface{}, err error) {
 	}
 
 	handler := respHandler{
-		TokenStore:          h.TokenStore,
+		SessionProvider:     h.SessionProvider,
 		AuthInfoStore:       h.AuthInfoStore,
 		OAuthAuthProvider:   h.OAuthAuthProvider,
 		IdentityProvider:    h.IdentityProvider,
diff --git a/pkg/auth/handler/sso/sso.go b/pkg/auth/handler/sso/sso.go
index f185029e..a210b37e 100644
--- a/pkg/auth/handler/sso/sso.go
+++ b/pkg/auth/handler/sso/sso.go
@@ -13,7 +13,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/task"
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/skydb"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 )
@@ -29,7 +29,7 @@ import (
 type ssoProviderParameter string
 
 type respHandler struct {
-	TokenStore          authtoken.Store
+	SessionProvider     session.Provider
 	AuthInfoStore       authinfo.Store
 	OAuthAuthProvider   oauth.Provider
 	IdentityProvider    principal.IdentityProvider
@@ -78,15 +78,11 @@ func (h respHandler) loginActionResp(oauthAuthInfo sso.AuthInfo, loginState sso.
 		}
 	}
 
-	// Create auth token
-	var token authtoken.Token
-	token, err = h.TokenStore.NewToken(info.ID, principal.ID)
+	// generate session
+	session, err := h.SessionProvider.Create(info.ID, principal.ID)
 	if err != nil {
 		panic(err)
 	}
-	if err = h.TokenStore.Put(&token); err != nil {
-		panic(err)
-	}
 
 	var sessionCreateReason event.SessionCreateReason
 	if createNewUser {
@@ -120,7 +116,7 @@ func (h respHandler) loginActionResp(oauthAuthInfo sso.AuthInfo, loginState sso.
 		return
 	}
 
-	resp = model.NewAuthResponse(user, identity, token.AccessToken)
+	resp = model.NewAuthResponse(user, identity, session.AccessToken)
 
 	if createNewUser &&
 		h.WelcomeEmailEnabled &&
diff --git a/pkg/auth/handler/sso/unlink.go b/pkg/auth/handler/sso/unlink.go
index c7b40e2d..9af295b1 100644
--- a/pkg/auth/handler/sso/unlink.go
+++ b/pkg/auth/handler/sso/unlink.go
@@ -104,7 +104,7 @@ func (h UnlinkHandler) Handle(req interface{}) (resp interface{}, err error) {
 	}
 
 	// principalID can be missing
-	principalID := h.AuthContext.Token().PrincipalID
+	principalID := h.AuthContext.Session().PrincipalID
 	if principalID != "" && principalID == principal.ID {
 		err = skyerr.NewError(skyerr.CurrentIdentityBeingDeleted, "Cannot delete current identity")
 		return
diff --git a/pkg/auth/inject.go b/pkg/auth/inject.go
index bf8977ea..473ef618 100644
--- a/pkg/auth/inject.go
+++ b/pkg/auth/inject.go
@@ -4,8 +4,10 @@ import (
 	"context"
 	"net/http"
 
+	"github.com/gomodule/redigo/redis"
+
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/passwordhistory"
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
+	"github.com/skygeario/skygear-server/pkg/core/time"
 
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/forgotpwdemail"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/hook"
@@ -36,6 +38,7 @@ import (
 type DependencyMap struct {
 	TemplateEngine    *template.Engine
 	AsyncTaskExecutor *async.Executor
+	RedisPool         *redis.Pool
 }
 
 // Provide provides dependency instance by name
@@ -158,8 +161,8 @@ func (m DependencyMap) Provide(
 		return newAuthContext()
 	case "TxContext":
 		return db.NewTxContextWithContext(ctx, tConfig)
-	case "TokenStore":
-		return coreAuth.NewDefaultTokenStore(ctx, tConfig)
+	case "SessionProvider":
+		return coreAuth.NewDefaultSessionProvider(ctx, tConfig)
 	case "AuthInfoStore":
 		return newAuthInfoStore()
 	case "PasswordChecker":
diff --git a/pkg/core/auth/authn/resolver/default.go b/pkg/core/auth/authn/resolver/default.go
index a0b58011..48f0194e 100644
--- a/pkg/core/auth/authn/resolver/default.go
+++ b/pkg/core/auth/authn/resolver/default.go
@@ -7,7 +7,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authn"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/model"
 )
@@ -16,40 +16,38 @@ type AuthContextResolverFactory struct{}
 
 func (f AuthContextResolverFactory) NewResolver(ctx context.Context, tenantConfig config.TenantConfiguration) authn.AuthContextResolver {
 	r := &DefaultAuthContextResolver{
-		TokenStore:    auth.NewDefaultTokenStore(ctx, tenantConfig),
-		AuthInfoStore: auth.NewDefaultAuthInfoStore(ctx, tenantConfig),
+		SessionProvider: auth.NewDefaultSessionProvider(ctx, tenantConfig),
+		AuthInfoStore:   auth.NewDefaultAuthInfoStore(ctx, tenantConfig),
 	}
 	return r
 }
 
 type DefaultAuthContextResolver struct {
-	TokenStore    authtoken.Store
-	AuthInfoStore authinfo.Store
+	SessionProvider session.Provider
+	AuthInfoStore   authinfo.Store
 }
 
 func (r DefaultAuthContextResolver) Resolve(req *http.Request, ctx auth.ContextSetter) (err error) {
 	keyType := model.GetAccessKeyType(req)
+	ctx.SetAccessKeyType(keyType)
 
-	var (
-		token    *authtoken.Token
-		authInfo *authinfo.AuthInfo
-	)
-
-	if keyType == model.MasterAccessKey {
-		token, authInfo, err = masterkeyAuthContextResolver{
-			TokenStore:    r.TokenStore,
-			AuthInfoStore: r.AuthInfoStore,
-		}.Resolve(req)
-	} else {
-		token, authInfo, err = nonMasterkeyAuthContextResolver{
-			TokenStore:    r.TokenStore,
-			AuthInfoStore: r.AuthInfoStore,
-		}.Resolve(req)
+	token := model.GetAccessToken(req)
+	s, err := r.SessionProvider.GetByToken(token, session.TokenKindAccessToken)
+	if err != nil {
+		if err == session.ErrSessionNotFound {
+			err = nil
+		}
+		return
 	}
 
-	ctx.SetAccessKeyType(keyType)
-	ctx.SetAuthInfo(authInfo)
-	ctx.SetToken(token)
+	info := &authinfo.AuthInfo{}
+	err = r.AuthInfoStore.GetAuth(s.UserID, info)
+	if err != nil {
+		return
+	}
+
+	ctx.SetAuthInfo(info)
+	ctx.SetSession(s)
 
 	return
 }
diff --git a/pkg/core/auth/authn/resolver/masterkey.go b/pkg/core/auth/authn/resolver/masterkey.go
deleted file mode 100644
index 18d0080d..00000000
--- a/pkg/core/auth/authn/resolver/masterkey.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package resolver
-
-import (
-	"net/http"
-
-	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
-	"github.com/skygeario/skygear-server/pkg/core/model"
-	"github.com/skygeario/skygear-server/pkg/core/skydb"
-)
-
-type masterkeyAuthContextResolver struct {
-	TokenStore    authtoken.Store
-	AuthInfoStore authinfo.Store
-}
-
-func (r masterkeyAuthContextResolver) Resolve(req *http.Request) (token *authtoken.Token, authInfo *authinfo.AuthInfo, err error) {
-	tokenStr := model.GetAccessToken(req)
-	token = &authtoken.Token{}
-	r.TokenStore.Get(tokenStr, token)
-
-	if token.AuthInfoID == "" {
-		token.AuthInfoID = "_god"
-	}
-
-	info := &authinfo.AuthInfo{}
-	if err = r.AuthInfoStore.GetAuth(token.AuthInfoID, info); err == skydb.ErrUserNotFound {
-		info.ID = token.AuthInfoID
-
-		if err = r.AuthInfoStore.CreateAuth(info); err == skydb.ErrUserDuplicated {
-			// user already exists, error can be ignored
-			err = nil
-		}
-	}
-
-	authInfo = info
-
-	return
-}
diff --git a/pkg/core/auth/authn/resolver/non_masterkey.go b/pkg/core/auth/authn/resolver/non_masterkey.go
deleted file mode 100644
index cfc97973..00000000
--- a/pkg/core/auth/authn/resolver/non_masterkey.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package resolver
-
-import (
-	"net/http"
-
-	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
-	"github.com/skygeario/skygear-server/pkg/core/model"
-)
-
-type nonMasterkeyAuthContextResolver struct {
-	TokenStore    authtoken.Store
-	AuthInfoStore authinfo.Store
-}
-
-func (r nonMasterkeyAuthContextResolver) Resolve(req *http.Request) (token *authtoken.Token, authInfo *authinfo.AuthInfo, err error) {
-	tokenStr := model.GetAccessToken(req)
-
-	token = &authtoken.Token{}
-	err = r.TokenStore.Get(tokenStr, token)
-	if err != nil {
-		if _, isNotFoundErr := err.(*authtoken.NotFoundError); isNotFoundErr {
-			// bypass token not found error
-			err = nil
-			return
-		}
-
-		// TODO:
-		// handle error properly
-		return
-	}
-
-	info := &authinfo.AuthInfo{}
-	err = r.AuthInfoStore.GetAuth(token.AuthInfoID, info)
-	if err != nil {
-		// TODO:
-		// handle error properly
-		return
-	}
-
-	authInfo = info
-
-	return
-}
diff --git a/pkg/core/auth/authtoken/jwt.go b/pkg/core/auth/authtoken/jwt.go
deleted file mode 100644
index 9b1d975b..00000000
--- a/pkg/core/auth/authtoken/jwt.go
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright 2015-present Oursky Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package authtoken
-
-import (
-	"errors"
-	"time"
-
-	jwt "github.com/dgrijalva/jwt-go"
-	"github.com/skygeario/skygear-server/pkg/core/uuid"
-)
-
-type authTokenClaims struct {
-	jwt.StandardClaims
-	PrincipalID string `json:"skygear:principal_id"`
-}
-
-// JWTStore implements TokenStore by encoding user information into
-// the access token string. This store does not keep state.
-type JWTStore struct {
-	appName string
-	secret  string
-	expiry  int64
-}
-
-// NewJWTStore creates a JWT token store.
-func NewJWTStore(appName string, secret string, expiry int64) *JWTStore {
-	if secret == "" {
-		panic("jwt store is not configured with a secret")
-	}
-	store := JWTStore{
-		appName: appName,
-		secret:  secret,
-		expiry:  expiry,
-	}
-	return &store
-}
-
-// NewToken creates a new token for this token store.
-func (r *JWTStore) NewToken(authInfoID string, principalID string) (Token, error) {
-	claims := authTokenClaims{
-		StandardClaims: jwt.StandardClaims{
-			Id:       uuid.New(),
-			IssuedAt: time.Now().Unix(),
-			Issuer:   r.appName,
-			Subject:  authInfoID,
-		},
-		PrincipalID: principalID,
-	}
-
-	if r.expiry > 0 {
-		claims.ExpiresAt = time.Now().Unix() + r.expiry
-	}
-
-	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
-	signedString, err := jwtToken.SignedString([]byte(r.secret))
-	if err != nil {
-		return Token{}, err
-	}
-
-	token := Token{}
-	r.setTokenFromClaims(claims, &token)
-	token.AccessToken = signedString
-	return token, nil
-}
-
-// Get decodes and verifies the access token for user information. It returns
-// the access token containing information about the user.
-func (r *JWTStore) Get(accessToken string, token *Token) error {
-	claims := authTokenClaims{}
-	jwtToken, err := jwt.ParseWithClaims(accessToken, &claims, func(token *jwt.Token) (interface{}, error) {
-		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
-			return nil, &NotFoundError{accessToken, errors.New("unexpected algorithm in token")}
-		}
-		return []byte(r.secret), nil
-	})
-
-	if err != nil {
-		return &NotFoundError{accessToken, err}
-	}
-
-	if jwtToken.Valid {
-		token.AccessToken = accessToken
-		r.setTokenFromClaims(claims, token)
-	} else {
-		return &NotFoundError{accessToken, errors.New("invalid token")}
-	}
-
-	// The token is considered valid by the JWTStore. (i.e. the token
-	// has a valid signature and the signature is verified with the secret.)
-	//
-	// In skygear-server, the `InjectAuthIfPresent` preprocessor is
-	// responsible for checking if the token is still valid. A token
-	// might become invalid if the user has changed the password after the
-	// token is generated.
-	return nil
-}
-
-func (r *JWTStore) setTokenFromClaims(claims authTokenClaims, token *Token) {
-	if claims.ExpiresAt > 0 {
-		token.ExpiredAt = time.Unix(claims.ExpiresAt, 0)
-	} else {
-		token.ExpiredAt = time.Time{}
-	}
-	if claims.IssuedAt > 0 {
-		token.IssuedAt = time.Unix(claims.IssuedAt, 0)
-	} else {
-		token.IssuedAt = time.Time{}
-	}
-	token.AppName = claims.Issuer
-	token.AuthInfoID = claims.Subject
-	token.PrincipalID = claims.PrincipalID
-}
-
-// Put does nothing because the JWT token store does not store token.
-func (r *JWTStore) Put(token *Token) error {
-	return nil
-}
-
-// Delete does nothing because the JWT token store does not store token.
-func (r *JWTStore) Delete(accessToken string) error {
-	return nil
-}
-
-// this ensures that our structure conform to certain interfaces.
-var (
-	_ Store = &JWTStore{}
-)
diff --git a/pkg/core/auth/authtoken/mock_store.go b/pkg/core/auth/authtoken/mock_store.go
deleted file mode 100644
index a7c7ed25..00000000
--- a/pkg/core/auth/authtoken/mock_store.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package authtoken
-
-import (
-	"errors"
-	"time"
-)
-
-// MockStore is the memory implementation of auth token store
-type MockStore struct {
-	TokenMap map[string]Token
-}
-
-// NewMockStore create mock store with empty map
-func NewMockStore() *MockStore {
-	return &MockStore{
-		TokenMap: map[string]Token{},
-	}
-}
-
-func (s *MockStore) NewToken(authInfoID string, principalID string) (Token, error) {
-	return New("mockAppName", authInfoID, principalID, time.Time{}), nil
-}
-
-func (s *MockStore) Get(accessToken string, token *Token) error {
-	t, ok := s.TokenMap[accessToken]
-	if !ok {
-		return &NotFoundError{token.AccessToken, errors.New("not found")}
-	}
-	token = &t
-	return nil
-}
-
-func (s *MockStore) Put(token *Token) error {
-	s.TokenMap[token.AccessToken] = *token
-	return nil
-}
-
-func (s *MockStore) Delete(accessToken string) error {
-	_, ok := s.TokenMap[accessToken]
-	if ok {
-		delete(s.TokenMap, accessToken)
-	}
-
-	return nil
-}
-
-func (s *MockStore) GetTokensByAuthInfoID(authInfoID string) []Token {
-	tokens := []Token{}
-	for _, token := range s.TokenMap {
-		if token.AuthInfoID == authInfoID {
-			tokens = append(tokens, token)
-		}
-	}
-	return tokens
-}
diff --git a/pkg/core/auth/authtoken/store.go b/pkg/core/auth/authtoken/store.go
deleted file mode 100644
index a576e103..00000000
--- a/pkg/core/auth/authtoken/store.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package authtoken
-
-type Store interface {
-	NewToken(authInfoID string, principalID string) (Token, error)
-	Get(accessToken string, token *Token) error
-	Put(token *Token) error
-	Delete(accessToken string) error
-}
diff --git a/pkg/core/auth/authtoken/token.go b/pkg/core/auth/authtoken/token.go
deleted file mode 100644
index 22e82da9..00000000
--- a/pkg/core/auth/authtoken/token.go
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright 2015-present Oursky Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package authtoken
-
-import (
-	"encoding/json"
-	"fmt"
-	"time"
-
-	"github.com/skygeario/skygear-server/pkg/core/uuid"
-)
-
-// Token is an expiry access token associated to a AuthInfo.
-type Token struct {
-	AccessToken string    `json:"accessToken" redis:"accessToken"`
-	ExpiredAt   time.Time `json:"expiredAt" redis:"expiredAt"`
-	AppName     string    `json:"appName" redis:"appName"`
-	AuthInfoID  string    `json:"authInfoID" redis:"authInfoID"`
-	PrincipalID string    `json:"principalID" redis:"principalID"`
-	IssuedAt    time.Time `json:"issuedAt" redis:"issuedAt"`
-}
-
-// MarshalJSON implements the json.Marshaler interface.
-func (t Token) MarshalJSON() ([]byte, error) {
-	var expireAt, issuedAt jsonStamp
-	if !t.ExpiredAt.IsZero() {
-		expireAt = jsonStamp(t.ExpiredAt)
-	}
-	if !t.IssuedAt.IsZero() {
-		issuedAt = jsonStamp(t.IssuedAt)
-	}
-	return json.Marshal(&jsonToken{
-		t.AccessToken,
-		expireAt,
-		t.AppName,
-		t.AuthInfoID,
-		issuedAt,
-	})
-}
-
-// UnmarshalJSON implements the json.Unmarshaler interface.
-func (t *Token) UnmarshalJSON(data []byte) (err error) {
-	token := jsonToken{}
-	if err := json.Unmarshal(data, &token); err != nil {
-		return err
-	}
-	var expireAt, issuedAt time.Time
-	if !time.Time(token.ExpiredAt).IsZero() {
-		expireAt = time.Time(token.ExpiredAt)
-	}
-	if !time.Time(token.IssuedAt).IsZero() {
-		issuedAt = time.Time(token.IssuedAt)
-	}
-	t.AccessToken = token.AccessToken
-	t.ExpiredAt = expireAt
-	t.AppName = token.AppName
-	t.AuthInfoID = token.AuthInfoID
-	t.IssuedAt = issuedAt
-	return nil
-}
-
-type jsonToken struct {
-	AccessToken string    `json:"accessToken"`
-	ExpiredAt   jsonStamp `json:"expiredAt"`
-	AppName     string    `json:"appName"`
-	AuthInfoID  string    `json:"authInfoID"`
-	IssuedAt    jsonStamp `json:"issuedAt"`
-}
-
-type jsonStamp time.Time
-
-// MarshalJSON implements the json.Marshaler interface.
-func (t jsonStamp) MarshalJSON() ([]byte, error) {
-	tt := time.Time(t)
-	if tt.IsZero() {
-		return json.Marshal(0)
-	}
-	return json.Marshal(tt.UnixNano())
-}
-
-// UnmarshalJSON implements the json.Unmarshaler interface.
-func (t *jsonStamp) UnmarshalJSON(data []byte) (err error) {
-	var i int64
-	if err := json.Unmarshal(data, &i); err != nil {
-		return err
-	}
-
-	if i == 0 {
-		*t = jsonStamp{}
-		return nil
-	}
-	*t = jsonStamp(time.Unix(0, i))
-	return nil
-}
-
-// New creates a new Token ready for use given a authInfoID and
-// expiredAt date. If expiredAt is passed an empty Time, the token
-// does not expire.
-func New(appName string, authInfoID string, principalID string, expiredAt time.Time) Token {
-	return Token{
-		// NOTE(limouren): I am not sure if it is good to use UUID
-		// as access token.
-		AccessToken: uuid.New(),
-		ExpiredAt:   expiredAt,
-		AppName:     appName,
-		AuthInfoID:  authInfoID,
-		PrincipalID: principalID,
-		IssuedAt:    time.Now().UTC(),
-	}
-}
-
-// IsExpired determines whether the Token has expired now or not.
-func (t *Token) IsExpired() bool {
-	return !t.ExpiredAt.IsZero() && t.ExpiredAt.Before(time.Now().UTC())
-}
-
-// NotFoundError is the error returned by Get if a TokenStore
-// cannot find the requested token or the fetched token is expired.
-type NotFoundError struct {
-	AccessToken string
-	Err         error
-}
-
-func (e *NotFoundError) Error() string {
-	return fmt.Sprintf("get %#v: %v", e.AccessToken, e.Err)
-}
diff --git a/pkg/core/auth/authz/policy/authinfo.go b/pkg/core/auth/authz/policy/authinfo.go
index 02e907cf..f82e32ea 100644
--- a/pkg/core/auth/authz/policy/authinfo.go
+++ b/pkg/core/auth/authz/policy/authinfo.go
@@ -24,9 +24,9 @@ func RequireAuthenticated(r *http.Request, ctx auth.ContextGetter) error {
 		// Due to precision, the issue time of the token can be before
 		// AuthInfo.TokenValidSince. We consider the token still valid
 		// if the token is issued within 1 second before tokenValidSince.
-		token := ctx.Token()
-		if !token.IssuedAt.IsZero() &&
-			!token.IssuedAt.After(tokenValidSince.Add(-1*time.Second)) {
+		session := ctx.Session()
+		if !session.AccessTokenCreatedAt.IsZero() &&
+			!session.AccessTokenCreatedAt.After(tokenValidSince.Add(-1*time.Second)) {
 			return skyerr.NewError(skyerr.NotAuthenticated, "require authenticated user")
 		}
 	}
diff --git a/pkg/core/auth/context.go b/pkg/core/auth/context.go
index 158e43b2..f8d3180a 100644
--- a/pkg/core/auth/context.go
+++ b/pkg/core/auth/context.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/model"
 )
 
@@ -19,21 +19,21 @@ var (
 type ContextGetter interface {
 	AccessKeyType() model.KeyType
 	AuthInfo() *authinfo.AuthInfo
-	Token() *authtoken.Token
+	Session() *session.Session
 }
 
 // ContextSetter provides interface for setting authentication data
 type ContextSetter interface {
 	SetAccessKeyType(model.KeyType)
 	SetAuthInfo(*authinfo.AuthInfo)
-	SetToken(*authtoken.Token)
+	SetSession(*session.Session)
 }
 
 // TODO: handle thread safety
 type contextContainer struct {
 	accessKeyType model.KeyType
 	authInfo      *authinfo.AuthInfo
-	token         *authtoken.Token
+	session       *session.Session
 }
 
 type authContext struct {
@@ -65,9 +65,9 @@ func (a *authContext) AuthInfo() *authinfo.AuthInfo {
 	return container.authInfo
 }
 
-func (a *authContext) Token() *authtoken.Token {
+func (a *authContext) Session() *session.Session {
 	container := a.container()
-	return container.token
+	return container.session
 }
 
 func (a *authContext) SetAccessKeyType(keyType model.KeyType) {
@@ -80,9 +80,9 @@ func (a *authContext) SetAuthInfo(authInfo *authinfo.AuthInfo) {
 	container.authInfo = authInfo
 }
 
-func (a *authContext) SetToken(token *authtoken.Token) {
+func (a *authContext) SetSession(session *session.Session) {
 	container := a.container()
-	container.token = token
+	container.session = session
 }
 
 func (a *authContext) container() *contextContainer {
diff --git a/pkg/core/auth/default.go b/pkg/core/auth/default.go
index 7a42bb72..4b12a968 100644
--- a/pkg/core/auth/default.go
+++ b/pkg/core/auth/default.go
@@ -5,14 +5,15 @@ import (
 
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
 	pqAuthInfo "github.com/skygeario/skygear-server/pkg/core/auth/authinfo/pq"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	redisSession "github.com/skygeario/skygear-server/pkg/core/auth/session/redis"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/logging"
 )
 
-func NewDefaultTokenStore(ctx context.Context, tConfig config.TenantConfiguration) authtoken.Store {
-	return authtoken.NewJWTStore(tConfig.AppName, tConfig.UserConfig.TokenStore.Secret, tConfig.UserConfig.TokenStore.Expiry)
+func NewDefaultSessionProvider(ctx context.Context, tConfig config.TenantConfiguration) session.Provider {
+	return session.NewProvider(redisSession.NewStore(ctx, tConfig.AppID))
 }
 
 func NewDefaultAuthInfoStore(ctx context.Context, tConfig config.TenantConfiguration) authinfo.Store {
diff --git a/pkg/core/auth/mock_context.go b/pkg/core/auth/mock_context.go
deleted file mode 100644
index f323ba52..00000000
--- a/pkg/core/auth/mock_context.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package auth
-
-import (
-	authinfo "github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	authtoken "github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
-	model "github.com/skygeario/skygear-server/pkg/core/model"
-)
-
-// MockContext is mock of authContext
-type MockContext struct {
-	container *contextContainer
-}
-
-// NewMockContextGetter create empty auth context
-func NewMockContextGetter() *MockContext {
-	container := &contextContainer{}
-	return &MockContext{container: container}
-}
-
-// NewMockContextGetterWithDefaultUser creates auth context with default user
-func NewMockContextGetterWithDefaultUser() ContextGetter {
-	return NewMockContextGetterWithUser("faseng.cat.id", "faseng.cat.principal.id", true, map[string]bool{})
-}
-
-// NewMockContextGetterWithUnverifiedUser creates auth context with unverified user
-func NewMockContextGetterWithUnverifiedUser(verifyInfo map[string]bool) ContextGetter {
-	return NewMockContextGetterWithUser("faseng.cat.id", "faseng.cat.principal.id", false, verifyInfo)
-}
-
-// NewMockContextGetterWithUser creates auth context with user
-func NewMockContextGetterWithUser(userID string, principalID string, verified bool, verifyInfo map[string]bool) ContextGetter {
-	container := &contextContainer{
-		accessKeyType: model.APIAccessKey,
-		authInfo: &authinfo.AuthInfo{
-			ID:         userID,
-			Verified:   verified,
-			VerifyInfo: verifyInfo,
-		},
-		token: &authtoken.Token{
-			AccessToken: "faseng_access_token",
-			PrincipalID: principalID,
-		},
-	}
-	return &MockContext{container: container}
-}
-
-// NewMockContextGetterWithMasterkeyDefaultUser creates auth context with default user and master key
-func NewMockContextGetterWithMasterkeyDefaultUser() ContextGetter {
-	ctx := NewMockContextGetterWithDefaultUser().(*MockContext)
-	ctx.container.accessKeyType = model.MasterAccessKey
-	return ctx
-}
-
-// NewMockContextGetterWithAdminUser creates auth context with admin user
-func NewMockContextGetterWithAdminUser() ContextGetter {
-	container := &contextContainer{
-		accessKeyType: model.APIAccessKey,
-		authInfo: &authinfo.AuthInfo{
-			ID: "chima.cat.id",
-		},
-		token: &authtoken.Token{
-			AccessToken: "chima_access_token",
-		},
-	}
-	return &MockContext{container: container}
-}
-
-// NewMockContextGetterWithMasterKey creates auth context with master key
-func NewMockContextGetterWithMasterKey() ContextGetter {
-	container := &contextContainer{
-		accessKeyType: model.MasterAccessKey,
-	}
-	return &MockContext{container: container}
-}
-
-// NewMockContextGetterWithAPIKey creates auth context with api key
-func NewMockContextGetterWithAPIKey() ContextGetter {
-	container := &contextContainer{
-		accessKeyType: model.APIAccessKey,
-	}
-	return &MockContext{container: container}
-}
-
-// AccessKeyType returns access key type from mock context
-func (m *MockContext) AccessKeyType() model.KeyType {
-	return m.container.accessKeyType
-}
-
-// AuthInfo returns auth info from mock context
-func (m *MockContext) AuthInfo() *authinfo.AuthInfo {
-	return m.container.authInfo
-}
-
-// Token returns token from mock context
-func (m *MockContext) Token() *authtoken.Token {
-	return m.container.token
-}
-
-func (m *MockContext) Set(authInfo *authinfo.AuthInfo, token *authtoken.Token) {
-	m.container.authInfo = authInfo
-	m.container.token = token
-}
diff --git a/pkg/core/auth/session/provider.go b/pkg/core/auth/session/provider.go
new file mode 100644
index 00000000..e2f1590b
--- /dev/null
+++ b/pkg/core/auth/session/provider.go
@@ -0,0 +1,15 @@
+package session
+
+type Provider interface {
+	// Create creates a session for principal
+	Create(userID string, principalID string) (*Session, error)
+	// GetByToken gets the session identified by the token
+	GetByToken(token string, kind TokenKind) (*Session, error)
+	// Access updates the session info when it is being accessed by user
+	Access(*Session) error
+	// Invalidate invalidates session with the ID
+	Invalidate(id string) error
+
+	// Refresh re-generates the access token of the session
+	Refresh(*Session) error
+}
diff --git a/pkg/core/auth/session/provider_impl.go b/pkg/core/auth/session/provider_impl.go
new file mode 100644
index 00000000..f4381cb8
--- /dev/null
+++ b/pkg/core/auth/session/provider_impl.go
@@ -0,0 +1,112 @@
+package session
+
+import (
+	"crypto/subtle"
+	"fmt"
+	"math/rand"
+	"strings"
+
+	corerand "github.com/skygeario/skygear-server/pkg/core/rand"
+	"github.com/skygeario/skygear-server/pkg/core/time"
+	"github.com/skygeario/skygear-server/pkg/core/uuid"
+)
+
+const (
+	tokenAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+	tokenLength   = 32
+)
+
+type providerImpl struct {
+	store Store
+
+	time time.Provider
+	rand *rand.Rand
+}
+
+func NewProvider(store Store) Provider {
+	return &providerImpl{
+		store: store,
+		time:  time.NewProvider(),
+		rand:  corerand.SecureRand,
+	}
+}
+
+func (p *providerImpl) Create(userID string, principalID string) (s *Session, err error) {
+	now := p.time.NowUTC()
+	sess := Session{
+		ID:          uuid.New(),
+		UserID:      userID,
+		PrincipalID: principalID,
+
+		CreatedAt:  now,
+		AccessedAt: now,
+	}
+	p.generateAccessToken(&sess)
+
+	err = p.store.Create(&sess)
+	if err != nil {
+		return
+	}
+
+	return &sess, nil
+}
+
+func (p *providerImpl) GetByToken(token string, kind TokenKind) (*Session, error) {
+	id, ok := decodeTokenSessionID(token)
+	if !ok {
+		return nil, ErrSessionNotFound
+	}
+
+	s, err := p.store.Get(id)
+	if err != nil {
+		return nil, err
+	}
+
+	var expectedToken string
+	switch kind {
+	case TokenKindAccessToken:
+		expectedToken = s.AccessToken
+	default:
+		return nil, ErrSessionNotFound
+	}
+
+	if subtle.ConstantTimeCompare([]byte(expectedToken), []byte(token)) == 0 {
+		return nil, ErrSessionNotFound
+	}
+
+	return s, nil
+}
+
+func (p *providerImpl) Access(s *Session) error {
+	s.AccessedAt = p.time.NowUTC()
+	return p.store.Update(s)
+}
+
+func (p *providerImpl) Invalidate(id string) error {
+	return p.store.Delete(id)
+}
+
+func (p *providerImpl) Refresh(session *Session) error {
+	p.generateAccessToken(session)
+	return p.store.Update(session)
+}
+
+func (p *providerImpl) generateAccessToken(s *Session) {
+	accessToken := corerand.StringWithAlphabet(tokenLength, tokenAlphabet, p.rand)
+	s.AccessToken = encodeToken(s.ID, accessToken)
+	s.AccessTokenCreatedAt = p.time.NowUTC()
+	return
+}
+
+func encodeToken(id string, token string) string {
+	return fmt.Sprintf("%s.%s", id, token)
+}
+
+func decodeTokenSessionID(token string) (id string, ok bool) {
+	parts := strings.Split(token, ".")
+	if len(parts) != 2 {
+		return
+	}
+	id, ok = parts[0], true
+	return
+}
diff --git a/pkg/core/auth/session/provider_mock.go b/pkg/core/auth/session/provider_mock.go
new file mode 100644
index 00000000..f0116f24
--- /dev/null
+++ b/pkg/core/auth/session/provider_mock.go
@@ -0,0 +1,79 @@
+package session
+
+import (
+	"fmt"
+
+	"github.com/skygeario/skygear-server/pkg/core/time"
+)
+
+type MockProvider struct {
+	Time    time.Provider
+	counter int
+
+	Sessions map[string]Session
+}
+
+var _ Provider = &MockProvider{}
+
+func NewMockProvider() *MockProvider {
+	return &MockProvider{
+		Time:     &time.MockProvider{},
+		Sessions: map[string]Session{},
+	}
+}
+
+func (p *MockProvider) Create(userID string, principalID string) (s *Session, err error) {
+	now := p.Time.NowUTC()
+	id := fmt.Sprintf("%s-%s-%d", userID, principalID, p.counter)
+	sess := Session{
+		ID:          id,
+		UserID:      userID,
+		PrincipalID: principalID,
+
+		CreatedAt:            now,
+		AccessedAt:           now,
+		AccessToken:          "access-token-" + id,
+		AccessTokenCreatedAt: now,
+	}
+	p.counter++
+
+	p.Sessions[sess.ID] = sess
+
+	return &sess, nil
+}
+
+func (p *MockProvider) GetByToken(token string, kind TokenKind) (*Session, error) {
+	for _, s := range p.Sessions {
+		var expectedToken string
+		switch kind {
+		case TokenKindAccessToken:
+			expectedToken = s.AccessToken
+		default:
+			continue
+		}
+
+		if expectedToken != token {
+			continue
+		}
+
+		return &s, nil
+	}
+	return nil, ErrSessionNotFound
+}
+
+func (p *MockProvider) Access(s *Session) error {
+	s.AccessedAt = p.Time.NowUTC()
+	p.Sessions[s.ID] = *s
+	return nil
+}
+
+func (p *MockProvider) Invalidate(id string) error {
+	delete(p.Sessions, id)
+	return nil
+}
+
+func (p *MockProvider) Refresh(session *Session) error {
+	session.AccessToken = fmt.Sprintf("access-token-%s-%d", session.ID, p.counter)
+	p.counter++
+	return nil
+}
diff --git a/pkg/core/auth/session/redis/keys.go b/pkg/core/auth/session/redis/keys.go
new file mode 100644
index 00000000..6778ea5a
--- /dev/null
+++ b/pkg/core/auth/session/redis/keys.go
@@ -0,0 +1,7 @@
+package redis
+
+import "fmt"
+
+func sessionKey(appID string, sessionID string) string {
+	return fmt.Sprintf("%s:session:%s", appID, sessionID)
+}
diff --git a/pkg/core/auth/session/redis/store.go b/pkg/core/auth/session/redis/store.go
new file mode 100644
index 00000000..2016f3d1
--- /dev/null
+++ b/pkg/core/auth/session/redis/store.go
@@ -0,0 +1,72 @@
+package redis
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	goredis "github.com/gomodule/redigo/redis"
+
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	"github.com/skygeario/skygear-server/pkg/core/redis"
+)
+
+type store struct {
+	ctx   context.Context
+	appID string
+}
+
+var _ session.Store = &store{}
+
+var errSessionCreateFailed = fmt.Errorf("cannot create session")
+
+func NewStore(ctx context.Context, appID string) session.Store {
+	return &store{ctx: ctx, appID: appID}
+}
+
+func (s *store) Create(sess *session.Session) (err error) {
+	json, err := json.Marshal(sess)
+	if err != nil {
+		return
+	}
+	conn := redis.GetConn(s.ctx)
+	key := sessionKey(s.appID, sess.ID)
+	_, err = goredis.String(conn.Do("SET", key, json, "NX"))
+	if err == goredis.ErrNil {
+		err = errSessionCreateFailed
+	}
+	return
+}
+
+func (s *store) Update(sess *session.Session) (err error) {
+	data, err := json.Marshal(sess)
+	if err != nil {
+		return
+	}
+	conn := redis.GetConn(s.ctx)
+	key := sessionKey(s.appID, sess.ID)
+	_, err = goredis.String(conn.Do("SET", key, data, "XX"))
+	if err == goredis.ErrNil {
+		err = session.ErrSessionNotFound
+	}
+	return
+}
+
+func (s *store) Get(id string) (sess *session.Session, err error) {
+	conn := redis.GetConn(s.ctx)
+	key := sessionKey(s.appID, id)
+	data, err := goredis.Bytes(conn.Do("GET", key))
+	if err == goredis.ErrNil {
+		err = session.ErrSessionNotFound
+		return
+	}
+	err = json.Unmarshal(data, &sess)
+	return
+}
+
+func (s *store) Delete(id string) (err error) {
+	conn := redis.GetConn(s.ctx)
+	key := sessionKey(s.appID, id)
+	_, err = conn.Do("DEL", key)
+	return
+}
diff --git a/pkg/core/auth/session/session.go b/pkg/core/auth/session/session.go
new file mode 100644
index 00000000..80bfcf03
--- /dev/null
+++ b/pkg/core/auth/session/session.go
@@ -0,0 +1,24 @@
+package session
+
+import (
+	"time"
+)
+
+type TokenKind string
+
+const (
+	TokenKindAccessToken TokenKind = "access-token"
+)
+
+// Session represents a session of user logged in with a principal.
+type Session struct {
+	ID          string `json:"id"`
+	UserID      string `json:"user_id"`
+	PrincipalID string `json:"principal_id"`
+
+	CreatedAt  time.Time `json:"created_at"`
+	AccessedAt time.Time `json:"accessed_at"`
+
+	AccessToken          string    `json:"access_token"`
+	AccessTokenCreatedAt time.Time `json:"access_token_created_at"`
+}
diff --git a/pkg/core/auth/session/store.go b/pkg/core/auth/session/store.go
new file mode 100644
index 00000000..40d685e7
--- /dev/null
+++ b/pkg/core/auth/session/store.go
@@ -0,0 +1,16 @@
+package session
+
+import "fmt"
+
+var ErrSessionNotFound = fmt.Errorf("session is not found")
+
+type Store interface {
+	// Create creates a session in the store. It must not allow overwriting existing sessions.
+	Create(s *Session) error
+	// Update updates a session in the store. It must return `ErrSessionNotFound` when the session does not exist.
+	Update(s *Session) error
+	// Get returns the session with id in the store. It must return `ErrSessionNotFound` when the session does not exist.
+	Get(id string) (*Session, error)
+	// Delete deletes the session with id in the store. It must treat deleting non-existent session as successful.
+	Delete(id string) error
+}
diff --git a/pkg/core/auth/session/store_mock.go b/pkg/core/auth/session/store_mock.go
new file mode 100644
index 00000000..d4700bd2
--- /dev/null
+++ b/pkg/core/auth/session/store_mock.go
@@ -0,0 +1,44 @@
+package session
+
+import "fmt"
+
+type MockStore struct {
+	Sessions map[string]Session
+}
+
+var _ Store = &MockStore{}
+
+func NewMockStore() *MockStore {
+	return &MockStore{
+		Sessions: map[string]Session{},
+	}
+}
+
+func (s *MockStore) Create(sess *Session) error {
+	if _, exists := s.Sessions[sess.ID]; exists {
+		return fmt.Errorf("cannot create session")
+	}
+	s.Sessions[sess.ID] = *sess
+	return nil
+}
+
+func (s *MockStore) Update(sess *Session) error {
+	if _, exists := s.Sessions[sess.ID]; !exists {
+		return ErrSessionNotFound
+	}
+	s.Sessions[sess.ID] = *sess
+	return nil
+}
+
+func (s *MockStore) Get(id string) (*Session, error) {
+	sess, exists := s.Sessions[id]
+	if !exists {
+		return nil, ErrSessionNotFound
+	}
+	return &sess, nil
+}
+
+func (s *MockStore) Delete(id string) error {
+	delete(s.Sessions, id)
+	return nil
+}
diff --git a/pkg/core/config/tenant.go b/pkg/core/config/tenant.go
index 33fee7b1..e4afa27d 100644
--- a/pkg/core/config/tenant.go
+++ b/pkg/core/config/tenant.go
@@ -415,7 +415,6 @@ type UserConfiguration struct {
 	URLPrefix        string                        `json:"url_prefix,omitempty" yaml:"url_prefix" msg:"url_prefix"`
 	CORS             CORSConfiguration             `json:"cors,omitempty" yaml:"cors" msg:"cors"`
 	Auth             AuthConfiguration             `json:"auth,omitempty" yaml:"auth" msg:"auth"`
-	TokenStore       TokenStoreConfiguration       `json:"token_store,omitempty" yaml:"token_store" msg:"token_store"`
 	UserAudit        UserAuditConfiguration        `json:"user_audit,omitempty" yaml:"user_audit" msg:"user_audit"`
 	ForgotPassword   ForgotPasswordConfiguration   `json:"forgot_password,omitempty" yaml:"forgot_password" msg:"forgot_password"`
 	WelcomeEmail     WelcomeEmailConfiguration     `json:"welcome_email,omitempty" yaml:"welcome_email" msg:"welcome_email"`
@@ -463,11 +462,6 @@ type LoginIDKeyConfiguration struct {
 	Maximum *int           `json:"maximum,omitempty" yaml:"maximum" msg:"maximum"`
 }
 
-type TokenStoreConfiguration struct {
-	Secret string `json:"secret,omitempty" yaml:"secret" msg:"secret"`
-	Expiry int64  `json:"expiry,omitempty" yaml:"expiry" msg:"expiry"`
-}
-
 type UserAuditConfiguration struct {
 	Enabled         bool                  `json:"enabled,omitempty" yaml:"enabled" msg:"enabled"`
 	TrailHandlerURL string                `json:"trail_handler_url,omitempty" yaml:"trail_handler_url" msg:"trail_handler_url"`
diff --git a/pkg/core/config/tenant_gen.go b/pkg/core/config/tenant_gen.go
index b01e9476..1ba04a23 100644
--- a/pkg/core/config/tenant_gen.go
+++ b/pkg/core/config/tenant_gen.go
@@ -3974,7 +3974,7 @@ func (z *TenantConfiguration) Msgsize() (s int) {
 }
 
 // DecodeMsg implements msgp.Decodable
-func (z *TokenStoreConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
+func (z *TwilioConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
 	var zmbt uint32
@@ -3989,122 +3989,6 @@ func (z *TokenStoreConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 			return
 		}
 		switch msgp.UnsafeString(field) {
-		case "secret":
-			z.Secret, err = dc.ReadString()
-			if err != nil {
-				return
-			}
-		case "expiry":
-			z.Expiry, err = dc.ReadInt64()
-			if err != nil {
-				return
-			}
-		default:
-			err = dc.Skip()
-			if err != nil {
-				return
-			}
-		}
-	}
-	return
-}
-
-// EncodeMsg implements msgp.Encodable
-func (z TokenStoreConfiguration) EncodeMsg(en *msgp.Writer) (err error) {
-	// map header, size 2
-	// write "secret"
-	err = en.Append(0x82, 0xa6, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74)
-	if err != nil {
-		return err
-	}
-	err = en.WriteString(z.Secret)
-	if err != nil {
-		return
-	}
-	// write "expiry"
-	err = en.Append(0xa6, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79)
-	if err != nil {
-		return err
-	}
-	err = en.WriteInt64(z.Expiry)
-	if err != nil {
-		return
-	}
-	return
-}
-
-// MarshalMsg implements msgp.Marshaler
-func (z TokenStoreConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
-	o = msgp.Require(b, z.Msgsize())
-	// map header, size 2
-	// string "secret"
-	o = append(o, 0x82, 0xa6, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74)
-	o = msgp.AppendString(o, z.Secret)
-	// string "expiry"
-	o = append(o, 0xa6, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79)
-	o = msgp.AppendInt64(o, z.Expiry)
-	return
-}
-
-// UnmarshalMsg implements msgp.Unmarshaler
-func (z *TokenStoreConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
-	var field []byte
-	_ = field
-	var zvls uint32
-	zvls, bts, err = msgp.ReadMapHeaderBytes(bts)
-	if err != nil {
-		return
-	}
-	for zvls > 0 {
-		zvls--
-		field, bts, err = msgp.ReadMapKeyZC(bts)
-		if err != nil {
-			return
-		}
-		switch msgp.UnsafeString(field) {
-		case "secret":
-			z.Secret, bts, err = msgp.ReadStringBytes(bts)
-			if err != nil {
-				return
-			}
-		case "expiry":
-			z.Expiry, bts, err = msgp.ReadInt64Bytes(bts)
-			if err != nil {
-				return
-			}
-		default:
-			bts, err = msgp.Skip(bts)
-			if err != nil {
-				return
-			}
-		}
-	}
-	o = bts
-	return
-}
-
-// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
-func (z TokenStoreConfiguration) Msgsize() (s int) {
-	s = 1 + 7 + msgp.StringPrefixSize + len(z.Secret) + 7 + msgp.Int64Size
-	return
-}
-
-// DecodeMsg implements msgp.Decodable
-func (z *TwilioConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
-	var field []byte
-	_ = field
-	var zjfj uint32
-	zjfj, err = dc.ReadMapHeader()
-	if err != nil {
-		return
-	}
-	for zjfj > 0 {
-		zjfj--
-		field, err = dc.ReadMapKeyPtr()
-		if err != nil {
-			return
-		}
-		switch msgp.UnsafeString(field) {
 		case "account_sid":
 			z.AccountSID, err = dc.ReadString()
 			if err != nil {
@@ -4183,13 +4067,13 @@ func (z TwilioConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
 func (z *TwilioConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zzak uint32
-	zzak, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zvls uint32
+	zvls, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zzak > 0 {
-		zzak--
+	for zvls > 0 {
+		zvls--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -4231,13 +4115,13 @@ func (z TwilioConfiguration) Msgsize() (s int) {
 func (z *UserAuditConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
-	var zbtz uint32
-	zbtz, err = dc.ReadMapHeader()
+	var zjfj uint32
+	zjfj, err = dc.ReadMapHeader()
 	if err != nil {
 		return
 	}
-	for zbtz > 0 {
-		zbtz--
+	for zjfj > 0 {
+		zjfj--
 		field, err = dc.ReadMapKeyPtr()
 		if err != nil {
 			return
@@ -4324,13 +4208,13 @@ func (z *UserAuditConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
 func (z *UserAuditConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zsym uint32
-	zsym, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zzak uint32
+	zzak, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zsym > 0 {
-		zsym--
+	for zzak > 0 {
+		zzak--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -4372,13 +4256,13 @@ func (z *UserAuditConfiguration) Msgsize() (s int) {
 func (z *UserConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
-	var zgeu uint32
-	zgeu, err = dc.ReadMapHeader()
+	var zbtz uint32
+	zbtz, err = dc.ReadMapHeader()
 	if err != nil {
 		return
 	}
-	for zgeu > 0 {
-		zgeu--
+	for zbtz > 0 {
+		zbtz--
 		field, err = dc.ReadMapKeyPtr()
 		if err != nil {
 			return
@@ -4400,13 +4284,13 @@ func (z *UserConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 				return
 			}
 		case "cors":
-			var zdtr uint32
-			zdtr, err = dc.ReadMapHeader()
+			var zsym uint32
+			zsym, err = dc.ReadMapHeader()
 			if err != nil {
 				return
 			}
-			for zdtr > 0 {
-				zdtr--
+			for zsym > 0 {
+				zsym--
 				field, err = dc.ReadMapKeyPtr()
 				if err != nil {
 					return
@@ -4429,44 +4313,14 @@ func (z *UserConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 			if err != nil {
 				return
 			}
-		case "token_store":
-			var zzqm uint32
-			zzqm, err = dc.ReadMapHeader()
-			if err != nil {
-				return
-			}
-			for zzqm > 0 {
-				zzqm--
-				field, err = dc.ReadMapKeyPtr()
-				if err != nil {
-					return
-				}
-				switch msgp.UnsafeString(field) {
-				case "secret":
-					z.TokenStore.Secret, err = dc.ReadString()
-					if err != nil {
-						return
-					}
-				case "expiry":
-					z.TokenStore.Expiry, err = dc.ReadInt64()
-					if err != nil {
-						return
-					}
-				default:
-					err = dc.Skip()
-					if err != nil {
-						return
-					}
-				}
-			}
 		case "user_audit":
-			var zdqi uint32
-			zdqi, err = dc.ReadMapHeader()
+			var zgeu uint32
+			zgeu, err = dc.ReadMapHeader()
 			if err != nil {
 				return
 			}
-			for zdqi > 0 {
-				zdqi--
+			for zgeu > 0 {
+				zgeu--
 				field, err = dc.ReadMapKeyPtr()
 				if err != nil {
 					return
@@ -4505,13 +4359,13 @@ func (z *UserConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 				return
 			}
 		case "sso":
-			var zyco uint32
-			zyco, err = dc.ReadMapHeader()
+			var zdtr uint32
+			zdtr, err = dc.ReadMapHeader()
 			if err != nil {
 				return
 			}
-			for zyco > 0 {
-				zyco--
+			for zdtr > 0 {
+				zdtr--
 				field, err = dc.ReadMapKeyPtr()
 				if err != nil {
 					return
@@ -4540,13 +4394,13 @@ func (z *UserConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 				return
 			}
 		case "hook":
-			var zhgh uint32
-			zhgh, err = dc.ReadMapHeader()
+			var zzqm uint32
+			zzqm, err = dc.ReadMapHeader()
 			if err != nil {
 				return
 			}
-			for zhgh > 0 {
-				zhgh--
+			for zzqm > 0 {
+				zzqm--
 				field, err = dc.ReadMapKeyPtr()
 				if err != nil {
 					return
@@ -4576,9 +4430,9 @@ func (z *UserConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 
 // EncodeMsg implements msgp.Encodable
 func (z *UserConfiguration) EncodeMsg(en *msgp.Writer) (err error) {
-	// map header, size 12
+	// map header, size 11
 	// write "api_key"
-	err = en.Append(0x8c, 0xa7, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79)
+	err = en.Append(0x8b, 0xa7, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79)
 	if err != nil {
 		return err
 	}
@@ -4624,26 +4478,6 @@ func (z *UserConfiguration) EncodeMsg(en *msgp.Writer) (err error) {
 	if err != nil {
 		return
 	}
-	// write "token_store"
-	// map header, size 2
-	// write "secret"
-	err = en.Append(0xab, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x82, 0xa6, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74)
-	if err != nil {
-		return err
-	}
-	err = en.WriteString(z.TokenStore.Secret)
-	if err != nil {
-		return
-	}
-	// write "expiry"
-	err = en.Append(0xa6, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79)
-	if err != nil {
-		return err
-	}
-	err = en.WriteInt64(z.TokenStore.Expiry)
-	if err != nil {
-		return
-	}
 	// write "user_audit"
 	// map header, size 3
 	// write "enabled"
@@ -4737,9 +4571,9 @@ func (z *UserConfiguration) EncodeMsg(en *msgp.Writer) (err error) {
 // MarshalMsg implements msgp.Marshaler
 func (z *UserConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
 	o = msgp.Require(b, z.Msgsize())
-	// map header, size 12
+	// map header, size 11
 	// string "api_key"
-	o = append(o, 0x8c, 0xa7, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79)
+	o = append(o, 0x8b, 0xa7, 0x61, 0x70, 0x69, 0x5f, 0x6b, 0x65, 0x79)
 	o = msgp.AppendString(o, z.APIKey)
 	// string "master_key"
 	o = append(o, 0xaa, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79)
@@ -4758,14 +4592,6 @@ func (z *UserConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
 	if err != nil {
 		return
 	}
-	// string "token_store"
-	// map header, size 2
-	// string "secret"
-	o = append(o, 0xab, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x82, 0xa6, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74)
-	o = msgp.AppendString(o, z.TokenStore.Secret)
-	// string "expiry"
-	o = append(o, 0xa6, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79)
-	o = msgp.AppendInt64(o, z.TokenStore.Expiry)
 	// string "user_audit"
 	// map header, size 3
 	// string "enabled"
@@ -4824,13 +4650,13 @@ func (z *UserConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
 func (z *UserConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zovg uint32
-	zovg, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zdqi uint32
+	zdqi, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zovg > 0 {
-		zovg--
+	for zdqi > 0 {
+		zdqi--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -4852,13 +4678,13 @@ func (z *UserConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 				return
 			}
 		case "cors":
-			var zsey uint32
-			zsey, bts, err = msgp.ReadMapHeaderBytes(bts)
+			var zyco uint32
+			zyco, bts, err = msgp.ReadMapHeaderBytes(bts)
 			if err != nil {
 				return
 			}
-			for zsey > 0 {
-				zsey--
+			for zyco > 0 {
+				zyco--
 				field, bts, err = msgp.ReadMapKeyZC(bts)
 				if err != nil {
 					return
@@ -4881,44 +4707,14 @@ func (z *UserConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 			if err != nil {
 				return
 			}
-		case "token_store":
-			var zcjp uint32
-			zcjp, bts, err = msgp.ReadMapHeaderBytes(bts)
-			if err != nil {
-				return
-			}
-			for zcjp > 0 {
-				zcjp--
-				field, bts, err = msgp.ReadMapKeyZC(bts)
-				if err != nil {
-					return
-				}
-				switch msgp.UnsafeString(field) {
-				case "secret":
-					z.TokenStore.Secret, bts, err = msgp.ReadStringBytes(bts)
-					if err != nil {
-						return
-					}
-				case "expiry":
-					z.TokenStore.Expiry, bts, err = msgp.ReadInt64Bytes(bts)
-					if err != nil {
-						return
-					}
-				default:
-					bts, err = msgp.Skip(bts)
-					if err != nil {
-						return
-					}
-				}
-			}
 		case "user_audit":
-			var zjhy uint32
-			zjhy, bts, err = msgp.ReadMapHeaderBytes(bts)
+			var zhgh uint32
+			zhgh, bts, err = msgp.ReadMapHeaderBytes(bts)
 			if err != nil {
 				return
 			}
-			for zjhy > 0 {
-				zjhy--
+			for zhgh > 0 {
+				zhgh--
 				field, bts, err = msgp.ReadMapKeyZC(bts)
 				if err != nil {
 					return
@@ -4957,13 +4753,13 @@ func (z *UserConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 				return
 			}
 		case "sso":
-			var znuf uint32
-			znuf, bts, err = msgp.ReadMapHeaderBytes(bts)
+			var zovg uint32
+			zovg, bts, err = msgp.ReadMapHeaderBytes(bts)
 			if err != nil {
 				return
 			}
-			for znuf > 0 {
-				znuf--
+			for zovg > 0 {
+				zovg--
 				field, bts, err = msgp.ReadMapKeyZC(bts)
 				if err != nil {
 					return
@@ -4992,13 +4788,13 @@ func (z *UserConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 				return
 			}
 		case "hook":
-			var znjj uint32
-			znjj, bts, err = msgp.ReadMapHeaderBytes(bts)
+			var zsey uint32
+			zsey, bts, err = msgp.ReadMapHeaderBytes(bts)
 			if err != nil {
 				return
 			}
-			for znjj > 0 {
-				znjj--
+			for zsey > 0 {
+				zsey--
 				field, bts, err = msgp.ReadMapKeyZC(bts)
 				if err != nil {
 					return
@@ -5029,16 +4825,16 @@ func (z *UserConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 
 // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
 func (z *UserConfiguration) Msgsize() (s int) {
-	s = 1 + 8 + msgp.StringPrefixSize + len(z.APIKey) + 11 + msgp.StringPrefixSize + len(z.MasterKey) + 11 + msgp.StringPrefixSize + len(z.URLPrefix) + 5 + 1 + 7 + msgp.StringPrefixSize + len(z.CORS.Origin) + 5 + z.Auth.Msgsize() + 12 + 1 + 7 + msgp.StringPrefixSize + len(z.TokenStore.Secret) + 7 + msgp.Int64Size + 11 + 1 + 8 + msgp.BoolSize + 18 + msgp.StringPrefixSize + len(z.UserAudit.TrailHandlerURL) + 9 + z.UserAudit.Password.Msgsize() + 16 + z.ForgotPassword.Msgsize() + 14 + z.WelcomeEmail.Msgsize() + 4 + 1 + 13 + z.SSO.CustomToken.Msgsize() + 6 + z.SSO.OAuth.Msgsize() + 18 + z.UserVerification.Msgsize() + 5 + 1 + 7 + msgp.StringPrefixSize + len(z.Hook.Secret)
+	s = 1 + 8 + msgp.StringPrefixSize + len(z.APIKey) + 11 + msgp.StringPrefixSize + len(z.MasterKey) + 11 + msgp.StringPrefixSize + len(z.URLPrefix) + 5 + 1 + 7 + msgp.StringPrefixSize + len(z.CORS.Origin) + 5 + z.Auth.Msgsize() + 11 + 1 + 8 + msgp.BoolSize + 18 + msgp.StringPrefixSize + len(z.UserAudit.TrailHandlerURL) + 9 + z.UserAudit.Password.Msgsize() + 16 + z.ForgotPassword.Msgsize() + 14 + z.WelcomeEmail.Msgsize() + 4 + 1 + 13 + z.SSO.CustomToken.Msgsize() + 6 + z.SSO.OAuth.Msgsize() + 18 + z.UserVerification.Msgsize() + 5 + 1 + 7 + msgp.StringPrefixSize + len(z.Hook.Secret)
 	return
 }
 
 // DecodeMsg implements msgp.Decodable
 func (z *UserVerificationCodeFormat) DecodeMsg(dc *msgp.Reader) (err error) {
 	{
-		var zhhj string
-		zhhj, err = dc.ReadString()
-		(*z) = UserVerificationCodeFormat(zhhj)
+		var zcjp string
+		zcjp, err = dc.ReadString()
+		(*z) = UserVerificationCodeFormat(zcjp)
 	}
 	if err != nil {
 		return
@@ -5065,9 +4861,9 @@ func (z UserVerificationCodeFormat) MarshalMsg(b []byte) (o []byte, err error) {
 // UnmarshalMsg implements msgp.Unmarshaler
 func (z *UserVerificationCodeFormat) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	{
-		var zuvr string
-		zuvr, bts, err = msgp.ReadStringBytes(bts)
-		(*z) = UserVerificationCodeFormat(zuvr)
+		var zjhy string
+		zjhy, bts, err = msgp.ReadStringBytes(bts)
+		(*z) = UserVerificationCodeFormat(zjhy)
 	}
 	if err != nil {
 		return
@@ -5086,13 +4882,13 @@ func (z UserVerificationCodeFormat) Msgsize() (s int) {
 func (z *UserVerificationConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
-	var zvml uint32
-	zvml, err = dc.ReadMapHeader()
+	var zhhj uint32
+	zhhj, err = dc.ReadMapHeader()
 	if err != nil {
 		return
 	}
-	for zvml > 0 {
-		zvml--
+	for zhhj > 0 {
+		zhhj--
 		field, err = dc.ReadMapKeyPtr()
 		if err != nil {
 			return
@@ -5110,9 +4906,9 @@ func (z *UserVerificationConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 			}
 		case "criteria":
 			{
-				var zpyv string
-				zpyv, err = dc.ReadString()
-				z.Criteria = UserVerificationCriteria(zpyv)
+				var zuvr string
+				zuvr, err = dc.ReadString()
+				z.Criteria = UserVerificationCriteria(zuvr)
 			}
 			if err != nil {
 				return
@@ -5128,31 +4924,31 @@ func (z *UserVerificationConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 				return
 			}
 		case "login_id_keys":
-			var zlur uint32
-			zlur, err = dc.ReadMapHeader()
+			var zusq uint32
+			zusq, err = dc.ReadMapHeader()
 			if err != nil {
 				return
 			}
-			if z.LoginIDKeys == nil && zlur > 0 {
-				z.LoginIDKeys = make(map[string]UserVerificationKeyConfiguration, zlur)
+			if z.LoginIDKeys == nil && zusq > 0 {
+				z.LoginIDKeys = make(map[string]UserVerificationKeyConfiguration, zusq)
 			} else if len(z.LoginIDKeys) > 0 {
 				for key, _ := range z.LoginIDKeys {
 					delete(z.LoginIDKeys, key)
 				}
 			}
-			for zlur > 0 {
-				zlur--
-				var zusq string
-				var zfgq UserVerificationKeyConfiguration
-				zusq, err = dc.ReadString()
+			for zusq > 0 {
+				zusq--
+				var znuf string
+				var znjj UserVerificationKeyConfiguration
+				znuf, err = dc.ReadString()
 				if err != nil {
 					return
 				}
-				err = zfgq.DecodeMsg(dc)
+				err = znjj.DecodeMsg(dc)
 				if err != nil {
 					return
 				}
-				z.LoginIDKeys[zusq] = zfgq
+				z.LoginIDKeys[znuf] = znjj
 			}
 		default:
 			err = dc.Skip()
@@ -5221,12 +5017,12 @@ func (z *UserVerificationConfiguration) EncodeMsg(en *msgp.Writer) (err error) {
 	if err != nil {
 		return
 	}
-	for zusq, zfgq := range z.LoginIDKeys {
-		err = en.WriteString(zusq)
+	for znuf, znjj := range z.LoginIDKeys {
+		err = en.WriteString(znuf)
 		if err != nil {
 			return
 		}
-		err = zfgq.EncodeMsg(en)
+		err = znjj.EncodeMsg(en)
 		if err != nil {
 			return
 		}
@@ -5256,9 +5052,9 @@ func (z *UserVerificationConfiguration) MarshalMsg(b []byte) (o []byte, err erro
 	// string "login_id_keys"
 	o = append(o, 0xad, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x73)
 	o = msgp.AppendMapHeader(o, uint32(len(z.LoginIDKeys)))
-	for zusq, zfgq := range z.LoginIDKeys {
-		o = msgp.AppendString(o, zusq)
-		o, err = zfgq.MarshalMsg(o)
+	for znuf, znjj := range z.LoginIDKeys {
+		o = msgp.AppendString(o, znuf)
+		o, err = znjj.MarshalMsg(o)
 		if err != nil {
 			return
 		}
@@ -5270,13 +5066,13 @@ func (z *UserVerificationConfiguration) MarshalMsg(b []byte) (o []byte, err erro
 func (z *UserVerificationConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zupi uint32
-	zupi, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zfgq uint32
+	zfgq, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zupi > 0 {
-		zupi--
+	for zfgq > 0 {
+		zfgq--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -5294,9 +5090,9 @@ func (z *UserVerificationConfiguration) UnmarshalMsg(bts []byte) (o []byte, err
 			}
 		case "criteria":
 			{
-				var zfvi string
-				zfvi, bts, err = msgp.ReadStringBytes(bts)
-				z.Criteria = UserVerificationCriteria(zfvi)
+				var zvml string
+				zvml, bts, err = msgp.ReadStringBytes(bts)
+				z.Criteria = UserVerificationCriteria(zvml)
 			}
 			if err != nil {
 				return
@@ -5312,31 +5108,31 @@ func (z *UserVerificationConfiguration) UnmarshalMsg(bts []byte) (o []byte, err
 				return
 			}
 		case "login_id_keys":
-			var zzrg uint32
-			zzrg, bts, err = msgp.ReadMapHeaderBytes(bts)
+			var zpyv uint32
+			zpyv, bts, err = msgp.ReadMapHeaderBytes(bts)
 			if err != nil {
 				return
 			}
-			if z.LoginIDKeys == nil && zzrg > 0 {
-				z.LoginIDKeys = make(map[string]UserVerificationKeyConfiguration, zzrg)
+			if z.LoginIDKeys == nil && zpyv > 0 {
+				z.LoginIDKeys = make(map[string]UserVerificationKeyConfiguration, zpyv)
 			} else if len(z.LoginIDKeys) > 0 {
 				for key, _ := range z.LoginIDKeys {
 					delete(z.LoginIDKeys, key)
 				}
 			}
-			for zzrg > 0 {
-				var zusq string
-				var zfgq UserVerificationKeyConfiguration
-				zzrg--
-				zusq, bts, err = msgp.ReadStringBytes(bts)
+			for zpyv > 0 {
+				var znuf string
+				var znjj UserVerificationKeyConfiguration
+				zpyv--
+				znuf, bts, err = msgp.ReadStringBytes(bts)
 				if err != nil {
 					return
 				}
-				bts, err = zfgq.UnmarshalMsg(bts)
+				bts, err = znjj.UnmarshalMsg(bts)
 				if err != nil {
 					return
 				}
-				z.LoginIDKeys[zusq] = zfgq
+				z.LoginIDKeys[znuf] = znjj
 			}
 		default:
 			bts, err = msgp.Skip(bts)
@@ -5353,9 +5149,9 @@ func (z *UserVerificationConfiguration) UnmarshalMsg(bts []byte) (o []byte, err
 func (z *UserVerificationConfiguration) Msgsize() (s int) {
 	s = 1 + 11 + msgp.StringPrefixSize + len(z.URLPrefix) + 20 + msgp.BoolSize + 9 + msgp.StringPrefixSize + len(string(z.Criteria)) + 15 + msgp.StringPrefixSize + len(z.ErrorRedirect) + 15 + msgp.StringPrefixSize + len(z.ErrorHTMLURL) + 14 + msgp.MapHeaderSize
 	if z.LoginIDKeys != nil {
-		for zusq, zfgq := range z.LoginIDKeys {
-			_ = zfgq
-			s += msgp.StringPrefixSize + len(zusq) + zfgq.Msgsize()
+		for znuf, znjj := range z.LoginIDKeys {
+			_ = znjj
+			s += msgp.StringPrefixSize + len(znuf) + znjj.Msgsize()
 		}
 	}
 	return
@@ -5364,9 +5160,9 @@ func (z *UserVerificationConfiguration) Msgsize() (s int) {
 // DecodeMsg implements msgp.Decodable
 func (z *UserVerificationCriteria) DecodeMsg(dc *msgp.Reader) (err error) {
 	{
-		var zbmy string
-		zbmy, err = dc.ReadString()
-		(*z) = UserVerificationCriteria(zbmy)
+		var zlur string
+		zlur, err = dc.ReadString()
+		(*z) = UserVerificationCriteria(zlur)
 	}
 	if err != nil {
 		return
@@ -5393,9 +5189,9 @@ func (z UserVerificationCriteria) MarshalMsg(b []byte) (o []byte, err error) {
 // UnmarshalMsg implements msgp.Unmarshaler
 func (z *UserVerificationCriteria) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	{
-		var zarl string
-		zarl, bts, err = msgp.ReadStringBytes(bts)
-		(*z) = UserVerificationCriteria(zarl)
+		var zupi string
+		zupi, bts, err = msgp.ReadStringBytes(bts)
+		(*z) = UserVerificationCriteria(zupi)
 	}
 	if err != nil {
 		return
@@ -5414,13 +5210,13 @@ func (z UserVerificationCriteria) Msgsize() (s int) {
 func (z *UserVerificationKeyConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
-	var zctz uint32
-	zctz, err = dc.ReadMapHeader()
+	var zfvi uint32
+	zfvi, err = dc.ReadMapHeader()
 	if err != nil {
 		return
 	}
-	for zctz > 0 {
-		zctz--
+	for zfvi > 0 {
+		zfvi--
 		field, err = dc.ReadMapKeyPtr()
 		if err != nil {
 			return
@@ -5428,9 +5224,9 @@ func (z *UserVerificationKeyConfiguration) DecodeMsg(dc *msgp.Reader) (err error
 		switch msgp.UnsafeString(field) {
 		case "code_format":
 			{
-				var zljl string
-				zljl, err = dc.ReadString()
-				z.CodeFormat = UserVerificationCodeFormat(zljl)
+				var zzrg string
+				zzrg, err = dc.ReadString()
+				z.CodeFormat = UserVerificationCodeFormat(zzrg)
 			}
 			if err != nil {
 				return
@@ -5462,9 +5258,9 @@ func (z *UserVerificationKeyConfiguration) DecodeMsg(dc *msgp.Reader) (err error
 			}
 		case "provider":
 			{
-				var zziv string
-				zziv, err = dc.ReadString()
-				z.Provider = UserVerificationProvider(zziv)
+				var zbmy string
+				zbmy, err = dc.ReadString()
+				z.Provider = UserVerificationProvider(zbmy)
 			}
 			if err != nil {
 				return
@@ -5600,13 +5396,13 @@ func (z *UserVerificationKeyConfiguration) MarshalMsg(b []byte) (o []byte, err e
 func (z *UserVerificationKeyConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zabj uint32
-	zabj, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zarl uint32
+	zarl, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zabj > 0 {
-		zabj--
+	for zarl > 0 {
+		zarl--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -5614,9 +5410,9 @@ func (z *UserVerificationKeyConfiguration) UnmarshalMsg(bts []byte) (o []byte, e
 		switch msgp.UnsafeString(field) {
 		case "code_format":
 			{
-				var zmlx string
-				zmlx, bts, err = msgp.ReadStringBytes(bts)
-				z.CodeFormat = UserVerificationCodeFormat(zmlx)
+				var zctz string
+				zctz, bts, err = msgp.ReadStringBytes(bts)
+				z.CodeFormat = UserVerificationCodeFormat(zctz)
 			}
 			if err != nil {
 				return
@@ -5648,9 +5444,9 @@ func (z *UserVerificationKeyConfiguration) UnmarshalMsg(bts []byte) (o []byte, e
 			}
 		case "provider":
 			{
-				var zvbw string
-				zvbw, bts, err = msgp.ReadStringBytes(bts)
-				z.Provider = UserVerificationProvider(zvbw)
+				var zljl string
+				zljl, bts, err = msgp.ReadStringBytes(bts)
+				z.Provider = UserVerificationProvider(zljl)
 			}
 			if err != nil {
 				return
@@ -5680,9 +5476,9 @@ func (z *UserVerificationKeyConfiguration) Msgsize() (s int) {
 // DecodeMsg implements msgp.Decodable
 func (z *UserVerificationProvider) DecodeMsg(dc *msgp.Reader) (err error) {
 	{
-		var zgvb string
-		zgvb, err = dc.ReadString()
-		(*z) = UserVerificationProvider(zgvb)
+		var zziv string
+		zziv, err = dc.ReadString()
+		(*z) = UserVerificationProvider(zziv)
 	}
 	if err != nil {
 		return
@@ -5709,9 +5505,9 @@ func (z UserVerificationProvider) MarshalMsg(b []byte) (o []byte, err error) {
 // UnmarshalMsg implements msgp.Unmarshaler
 func (z *UserVerificationProvider) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	{
-		var zqzg string
-		zqzg, bts, err = msgp.ReadStringBytes(bts)
-		(*z) = UserVerificationProvider(zqzg)
+		var zabj string
+		zabj, bts, err = msgp.ReadStringBytes(bts)
+		(*z) = UserVerificationProvider(zabj)
 	}
 	if err != nil {
 		return
@@ -5730,13 +5526,13 @@ func (z UserVerificationProvider) Msgsize() (s int) {
 func (z *UserVerificationProviderConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
-	var zexy uint32
-	zexy, err = dc.ReadMapHeader()
+	var zmlx uint32
+	zmlx, err = dc.ReadMapHeader()
 	if err != nil {
 		return
 	}
-	for zexy > 0 {
-		zexy--
+	for zmlx > 0 {
+		zmlx--
 		field, err = dc.ReadMapKeyPtr()
 		if err != nil {
 			return
@@ -5854,13 +5650,13 @@ func (z *UserVerificationProviderConfiguration) MarshalMsg(b []byte) (o []byte,
 func (z *UserVerificationProviderConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zakb uint32
-	zakb, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zvbw uint32
+	zvbw, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zakb > 0 {
-		zakb--
+	for zvbw > 0 {
+		zvbw--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -5912,13 +5708,13 @@ func (z *UserVerificationProviderConfiguration) Msgsize() (s int) {
 func (z *WelcomeEmailConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 	var field []byte
 	_ = field
-	var zsdj uint32
-	zsdj, err = dc.ReadMapHeader()
+	var zgvb uint32
+	zgvb, err = dc.ReadMapHeader()
 	if err != nil {
 		return
 	}
-	for zsdj > 0 {
-		zsdj--
+	for zgvb > 0 {
+		zgvb--
 		field, err = dc.ReadMapKeyPtr()
 		if err != nil {
 			return
@@ -5961,9 +5757,9 @@ func (z *WelcomeEmailConfiguration) DecodeMsg(dc *msgp.Reader) (err error) {
 			}
 		case "destination":
 			{
-				var zsgp string
-				zsgp, err = dc.ReadString()
-				z.Destination = WelcomeEmailDestination(zsgp)
+				var zqzg string
+				zqzg, err = dc.ReadString()
+				z.Destination = WelcomeEmailDestination(zqzg)
 			}
 			if err != nil {
 				return
@@ -6091,13 +5887,13 @@ func (z *WelcomeEmailConfiguration) MarshalMsg(b []byte) (o []byte, err error) {
 func (z *WelcomeEmailConfiguration) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	var field []byte
 	_ = field
-	var zngc uint32
-	zngc, bts, err = msgp.ReadMapHeaderBytes(bts)
+	var zexy uint32
+	zexy, bts, err = msgp.ReadMapHeaderBytes(bts)
 	if err != nil {
 		return
 	}
-	for zngc > 0 {
-		zngc--
+	for zexy > 0 {
+		zexy--
 		field, bts, err = msgp.ReadMapKeyZC(bts)
 		if err != nil {
 			return
@@ -6140,9 +5936,9 @@ func (z *WelcomeEmailConfiguration) UnmarshalMsg(bts []byte) (o []byte, err erro
 			}
 		case "destination":
 			{
-				var zwfl string
-				zwfl, bts, err = msgp.ReadStringBytes(bts)
-				z.Destination = WelcomeEmailDestination(zwfl)
+				var zakb string
+				zakb, bts, err = msgp.ReadStringBytes(bts)
+				z.Destination = WelcomeEmailDestination(zakb)
 			}
 			if err != nil {
 				return
@@ -6167,9 +5963,9 @@ func (z *WelcomeEmailConfiguration) Msgsize() (s int) {
 // DecodeMsg implements msgp.Decodable
 func (z *WelcomeEmailDestination) DecodeMsg(dc *msgp.Reader) (err error) {
 	{
-		var zdif string
-		zdif, err = dc.ReadString()
-		(*z) = WelcomeEmailDestination(zdif)
+		var zsdj string
+		zsdj, err = dc.ReadString()
+		(*z) = WelcomeEmailDestination(zsdj)
 	}
 	if err != nil {
 		return
@@ -6196,9 +5992,9 @@ func (z WelcomeEmailDestination) MarshalMsg(b []byte) (o []byte, err error) {
 // UnmarshalMsg implements msgp.Unmarshaler
 func (z *WelcomeEmailDestination) UnmarshalMsg(bts []byte) (o []byte, err error) {
 	{
-		var zibu string
-		zibu, bts, err = msgp.ReadStringBytes(bts)
-		(*z) = WelcomeEmailDestination(zibu)
+		var zsgp string
+		zsgp, bts, err = msgp.ReadStringBytes(bts)
+		(*z) = WelcomeEmailDestination(zsgp)
 	}
 	if err != nil {
 		return
diff --git a/pkg/core/redis/context.go b/pkg/core/redis/context.go
new file mode 100644
index 00000000..6a7dd420
--- /dev/null
+++ b/pkg/core/redis/context.go
@@ -0,0 +1,39 @@
+package redis
+
+import (
+	"context"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type contextKeyType struct{}
+
+var contextKey = contextKeyType{}
+
+type redisContext struct {
+	pool *redis.Pool
+	conn redis.Conn
+}
+
+func WithRedis(ctx context.Context, pool *redis.Pool) context.Context {
+	redisCtx := &redisContext{pool: pool, conn: nil}
+	return context.WithValue(ctx, contextKey, redisCtx)
+}
+
+func GetConn(ctx context.Context) redis.Conn {
+	redisCtx := ctx.Value(contextKey).(*redisContext)
+	if redisCtx.conn == nil {
+		redisCtx.conn = redisCtx.pool.Get()
+	}
+	return redisCtx.conn
+}
+
+func CloseConn(ctx context.Context) error {
+	redisCtx := ctx.Value(contextKey).(*redisContext)
+	if redisCtx.conn == nil {
+		return nil
+	}
+	conn := redisCtx.conn
+	redisCtx.conn = nil
+	return conn.Close()
+}
diff --git a/pkg/core/redis/pool.go b/pkg/core/redis/pool.go
new file mode 100644
index 00000000..1b7db086
--- /dev/null
+++ b/pkg/core/redis/pool.go
@@ -0,0 +1,37 @@
+package redis
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/gomodule/redigo/redis"
+)
+
+type Configuration struct {
+	Host     string `envconfig:"HOST"`
+	Port     int    `envconfig:"PORT" default:"6379"`
+	Password string `envconfig:"PASSWORD"`
+	DB       int    `envconfig:"DB" default:"0"`
+}
+
+func NewPool(config Configuration) *redis.Pool {
+	hostPort := fmt.Sprintf("%s:%d", config.Host, config.Port)
+	// TODO(pool): configurable / profile for good value?
+	return &redis.Pool{
+		MaxIdle:     5,
+		IdleTimeout: 5 * time.Minute,
+		Dial: func() (conn redis.Conn, err error) {
+			conn, err = redis.Dial(
+				"tcp",
+				hostPort,
+				redis.DialDatabase(config.DB),
+				redis.DialPassword(config.Password),
+			)
+			return
+		},
+		TestOnBorrow: func(conn redis.Conn, t time.Time) (err error) {
+			_, err = conn.Do("PING")
+			return
+		},
+	}
+}
diff --git a/pkg/core/server/server.go b/pkg/core/server/server.go
index dae935b1..174c6649 100644
--- a/pkg/core/server/server.go
+++ b/pkg/core/server/server.go
@@ -1,6 +1,7 @@
 package server
 
 import (
+	"context"
 	"encoding/json"
 	"io"
 	"net/http"
@@ -19,6 +20,12 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/handler"
 )
 
+// SetupContextFunc setup context for usage in handler
+type SetupContextFunc func(context.Context) context.Context
+
+// CleanupContextFunc cleanup context after handler completed
+type CleanupContextFunc func(context.Context)
+
 // Server embeds a net/http server and has a gorillax mux internally
 type Server struct {
 	*http.Server
@@ -26,6 +33,8 @@ type Server struct {
 	router                     *mux.Router
 	authContextResolverFactory authn.AuthContextResolverFactory
 	dbPool                     db.Pool
+	setupCtxFn                 SetupContextFunc
+	cleanupCtxFn               CleanupContextFunc
 }
 
 // NewServer create a new Server with default option
@@ -33,11 +42,15 @@ func NewServer(
 	addr string,
 	authContextResolverFactory authn.AuthContextResolverFactory,
 	dbPool db.Pool,
+	setupCtxFn SetupContextFunc,
+	cleanupCtxFn CleanupContextFunc,
 ) Server {
 	return NewServerWithOption(
 		addr,
 		authContextResolverFactory,
 		dbPool,
+		setupCtxFn,
+		cleanupCtxFn,
 		DefaultOption(),
 	)
 }
@@ -47,6 +60,8 @@ func NewServerWithOption(
 	addr string,
 	authContextResolverFactory authn.AuthContextResolverFactory,
 	dbPool db.Pool,
+	setupCtxFn SetupContextFunc,
+	cleanupCtxFn CleanupContextFunc,
 	option Option,
 ) Server {
 	router := mux.NewRouter()
@@ -69,6 +84,8 @@ func NewServerWithOption(
 		},
 		authContextResolverFactory: authContextResolverFactory,
 		dbPool:                     dbPool,
+		setupCtxFn:                 setupCtxFn,
+		cleanupCtxFn:               cleanupCtxFn,
 	}
 
 	if option.RecoverPanic {
@@ -90,6 +107,12 @@ func (s *Server) Handle(path string, hf handler.Factory) *mux.Route {
 
 		r = auth.InitRequestAuthContext(r)
 		r = db.InitRequestDBContext(r, s.dbPool)
+		if s.setupCtxFn != nil {
+			r = r.WithContext(s.setupCtxFn(r.Context()))
+		}
+		if s.cleanupCtxFn != nil {
+			defer s.cleanupCtxFn(r.Context())
+		}
 
 		h := hf.NewHandler(r)
 
diff --git a/pkg/auth/dependency/time/mock_provider.go b/pkg/core/time/mock_provider.go
similarity index 100%
rename from pkg/auth/dependency/time/mock_provider.go
rename to pkg/core/time/mock_provider.go
diff --git a/pkg/auth/dependency/time/provider.go b/pkg/core/time/provider.go
similarity index 100%
rename from pkg/auth/dependency/time/provider.go
rename to pkg/core/time/provider.go
diff --git a/pkg/core/validation/schema.go b/pkg/core/validation/schema.go
index 4da83799..d078f4d1 100644
--- a/pkg/core/validation/schema.go
+++ b/pkg/core/validation/schema.go
@@ -31,7 +31,6 @@ const (
 				},
 				"cors": { "$ref": "#CORSConfiguration" },
 				"auth": { "$ref": "#AuthConfiguration" },
-				"token_store": { "$ref": "#TokenStoreConfiguration" },
 				"user_audit": { "$ref": "#UserAuditConfiguration" },
 				"forgot_password": { "$ref": "#ForgotPasswordConfiguration" },
 				"welcome_email": { "$ref": "#WelcomeEmailConfiguration" },
@@ -39,7 +38,7 @@ const (
 				"user_verification": { "$ref": "#UserVerificationConfiguration" },
 				"hook": { "$ref": "#HookUserConfiguration" }
 			},
-			"required": ["api_key", "master_key", "auth", "token_store", "hook"]
+			"required": ["api_key", "master_key", "auth", "hook"]
 		},
 		"CORSConfiguration": {
 			"$id": "#CORSConfiguration",
@@ -80,15 +79,6 @@ const (
 			},
 			"required": ["type"]
 		},
-		"TokenStoreConfiguration": {
-			"$id": "#TokenStoreConfiguration",
-			"type": "object",
-			"properties": {
-				"secret": { "$ref": "#NonEmptyString" },
-				"expiry": { "$ref": "#NonNegativeInteger" }
-			},
-			"required": ["secret"]
-		},
 		"UserAuditConfiguration": {
 			"$id": "#UserAuditConfiguration",
 			"type": "object",
diff --git a/pkg/gateway/config/config.go b/pkg/gateway/config/config.go
index 0843b4ae..8e4f500a 100644
--- a/pkg/gateway/config/config.go
+++ b/pkg/gateway/config/config.go
@@ -7,16 +7,18 @@ import (
 	"github.com/kelseyhightower/envconfig"
 
 	"github.com/skygeario/skygear-server/pkg/core/logging"
+	"github.com/skygeario/skygear-server/pkg/core/redis"
 	"github.com/skygeario/skygear-server/pkg/gateway/model"
 )
 
 // Configuration is gateway startup configuration
 type Configuration struct {
 	Standalone                        bool
-	StandaloneTenantConfigurationFile string        `envconfig:"STANDALONE_TENANT_CONFIG_FILE" default:"standalone-tenant-config.yaml"`
-	Host                              string        `envconfig:"HOST" default:"localhost:3001"`
-	ConnectionStr                     string        `envconfig:"DATABASE_URL"`
-	Auth                              GearURLConfig `envconfig:"AUTH"`
+	StandaloneTenantConfigurationFile string              `envconfig:"STANDALONE_TENANT_CONFIG_FILE" default:"standalone-tenant-config.yaml"`
+	Host                              string              `envconfig:"HOST" default:"localhost:3001"`
+	ConnectionStr                     string              `envconfig:"DATABASE_URL"`
+	Auth                              GearURLConfig       `envconfig:"AUTH"`
+	Redis                             redis.Configuration `envconfig:"REDIS"`
 }
 
 // ReadFromEnv reads from environment variable and update the configuration.
diff --git a/pkg/gateway/inject.go b/pkg/gateway/inject.go
index efe16a37..116b3075 100644
--- a/pkg/gateway/inject.go
+++ b/pkg/gateway/inject.go
@@ -20,8 +20,8 @@ func (m DependencyMap) Provide(
 	tConfig config.TenantConfiguration,
 ) interface{} {
 	switch dependencyName {
-	case "TokenStore":
-		return coreAuth.NewDefaultTokenStore(ctx, tConfig)
+	case "SessionProvider":
+		return coreAuth.NewDefaultSessionProvider(ctx, tConfig)
 	case "AuthInfoStore":
 		return coreAuth.NewDefaultAuthInfoStore(ctx, tConfig)
 	case "TxContext":
diff --git a/pkg/gateway/middleware/authinfo.go b/pkg/gateway/middleware/authinfo.go
index 816db27f..043559f9 100644
--- a/pkg/gateway/middleware/authinfo.go
+++ b/pkg/gateway/middleware/authinfo.go
@@ -5,7 +5,7 @@ import (
 	"strconv"
 
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	coreHttp "github.com/skygeario/skygear-server/pkg/core/http"
 	"github.com/skygeario/skygear-server/pkg/core/model"
@@ -14,9 +14,9 @@ import (
 // AuthInfoMiddleware injects auth info headers into the request
 // if x-skygear-access-token is present in the request.
 type AuthInfoMiddleware struct {
-	TokenStore    authtoken.Store `dependency:"TokenStore"`
-	AuthInfoStore authinfo.Store  `dependency:"AuthInfoStore"`
-	TxContext     db.TxContext    `dependency:"TxContext"`
+	SessionProvider session.Provider `dependency:"SessionProvider"`
+	AuthInfoStore   authinfo.Store   `dependency:"AuthInfoStore"`
+	TxContext       db.TxContext     `dependency:"TxContext"`
 }
 
 // AuthInfoMiddlewareFactory creates AuthInfoMiddleware per request.
@@ -53,15 +53,14 @@ func (m *AuthInfoMiddleware) Handle(next http.Handler) http.Handler {
 		}
 		defer m.TxContext.RollbackTx()
 
-		token := authtoken.Token{}
-		err = m.TokenStore.Get(accessToken, &token)
+		session, err := m.SessionProvider.GetByToken(accessToken, session.TokenKindAccessToken)
 		if err != nil {
 			http.Error(w, "invalid access token", http.StatusUnauthorized)
 			return
 		}
 
 		authInfo := authinfo.AuthInfo{}
-		err = m.AuthInfoStore.GetAuth(token.AuthInfoID, &authInfo)
+		err = m.AuthInfoStore.GetAuth(session.UserID, &authInfo)
 		if err != nil {
 			http.Error(w, "invalid access token", http.StatusUnauthorized)
 			return
@@ -74,5 +73,7 @@ func (m *AuthInfoMiddleware) Handle(next http.Handler) http.Handler {
 		r.Header.Set(coreHttp.HeaderAuthInfoID, id)
 		r.Header.Set(coreHttp.HeaderAuthInfoVerified, strconv.FormatBool(verified))
 		r.Header.Set(coreHttp.HeaderAuthInfoDisabled, strconv.FormatBool(disabled))
+
+		err = m.SessionProvider.Access(session)
 	})
 }
diff --git a/standalone-tenant-config.sample.yaml b/standalone-tenant-config.sample.yaml
index bc9b6e48..e56b4d8e 100644
--- a/standalone-tenant-config.sample.yaml
+++ b/standalone-tenant-config.sample.yaml
@@ -13,8 +13,6 @@ user_config:
   url_prefix: http://localhost:3000/_auth
   api_key: api_key
   master_key: master_key
-  token_store:
-    secret: tokenstoresecret
   hook:
     secret: hooksecret
   welcome_email:


Test patch:
diff --git a/pkg/auth/dependency/hook/deliverer_impl_test.go b/pkg/auth/dependency/hook/deliverer_impl_test.go
index 43a54a31..0301306a 100644
--- a/pkg/auth/dependency/hook/deliverer_impl_test.go
+++ b/pkg/auth/dependency/hook/deliverer_impl_test.go
@@ -9,12 +9,12 @@ import (
 
 	"github.com/h2non/gock"
 
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/http"
+	"github.com/skygeario/skygear-server/pkg/core/time"
 
 	. "github.com/smartystreets/goconvey/convey"
 )
diff --git a/pkg/auth/dependency/hook/provider_impl_test.go b/pkg/auth/dependency/hook/provider_impl_test.go
index affbf226..c2c45b50 100644
--- a/pkg/auth/dependency/hook/provider_impl_test.go
+++ b/pkg/auth/dependency/hook/provider_impl_test.go
@@ -6,14 +6,13 @@ import (
 	"testing"
 	gotime "time"
 
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
+	"github.com/skygeario/skygear-server/pkg/core/time"
+
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -23,7 +22,7 @@ func TestDispatchEvent(t *testing.T) {
 		req, _ := http.NewRequest("POST", "https://www.example.com", nil)
 		timeProvider := time.MockProvider{TimeNowUTC: gotime.Date(2006, 1, 2, 15, 4, 5, 0, gotime.UTC)}
 		store := newMockStore()
-		authContext := auth.NewMockContextGetter()
+		authContext := authtest.NewMockContext()
 		deliverer := newMockDeliverer()
 		authInfoStore := authinfo.NewMockStore()
 		userProfileStore := userprofile.NewMockUserProfileStore()
@@ -146,10 +145,7 @@ func TestDispatchEvent(t *testing.T) {
 			Convey("should include auth info", func() {
 				userID := "user-id"
 				principalID := "principal-id"
-				authContext.Set(
-					&authinfo.AuthInfo{ID: userID},
-					&authtoken.Token{PrincipalID: principalID},
-				)
+				authContext.UseUser(userID, principalID)
 
 				err := provider.DispatchEvent(
 					payload,
diff --git a/pkg/auth/handler/change_password_test.go b/pkg/auth/handler/change_password_test.go
index 624194f1..e294dbff 100644
--- a/pkg/auth/handler/change_password_test.go
+++ b/pkg/auth/handler/change_password_test.go
@@ -1,7 +1,6 @@
 package handler
 
 import (
-	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -20,9 +19,9 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/task"
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -40,14 +39,13 @@ func TestChangePasswordHandler(t *testing.T) {
 	Convey("Test ChangePasswordHandler", t, func() {
 		// fixture
 		userID := "john.doe.id"
-		mockTokenStore := authtoken.NewMockStore()
 		mockTaskQueue := async.NewMockQueue()
 
 		lh := &ChangePasswordHandler{}
 		lh.AuditTrail = audit.NewMockTrail(t)
-		lh.AuthContext = auth.NewMockContextGetterWithUser(userID, "john.doe.principal.id0", true, map[string]bool{})
+		lh.AuthContext = authtest.NewMockContext().UseUser(userID, "john.doe.principal.id0").MarkVerified()
 		lh.AuthInfoStore = authinfo.NewMockStoreWithUser(userID)
-		lh.TokenStore = mockTokenStore
+		lh.SessionProvider = session.NewMockProvider()
 		profileData := map[string]map[string]interface{}{
 			"john.doe.id": map[string]interface{}{},
 		}
@@ -95,10 +93,8 @@ func TestChangePasswordHandler(t *testing.T) {
 			resp := httptest.NewRecorder()
 			h.ServeHTTP(resp, req)
 
-			token := mockTokenStore.GetTokensByAuthInfoID(userID)[0]
-
 			So(resp.Code, ShouldEqual, 200)
-			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`{
+			So(resp.Body.Bytes(), ShouldEqualJSON, `{
 				"result": {
 					"user": {
 						"id": "john.doe.id",
@@ -116,9 +112,9 @@ func TestChangePasswordHandler(t *testing.T) {
 						"realm": "",
 						"type": "password"
 					},
-					"access_token": "%s"
+					"access_token": "access-token-john.doe.id-john.doe.principal.id0-0"
 				}
-			}`, token.AccessToken))
+			}`)
 
 			// should enqueue pw housekeeper task
 			So(mockTaskQueue.TasksName[0], ShouldEqual, task.PwHousekeeperTaskName)
diff --git a/pkg/auth/handler/forgotpwd/reset_test.go b/pkg/auth/handler/forgotpwd/reset_test.go
index d0412558..169ddb73 100644
--- a/pkg/auth/handler/forgotpwd/reset_test.go
+++ b/pkg/auth/handler/forgotpwd/reset_test.go
@@ -24,7 +24,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -79,7 +78,6 @@ func TestForgotPasswordResetHandler(t *testing.T) {
 			},
 		)
 		fh.UserProfileStore = userprofile.NewMockUserProfileStore()
-		fh.TokenStore = authtoken.NewJWTStore("myApp", "secret", 0)
 		fh.CodeGenerator = codeGenerator
 		fh.PasswordChecker = &authAudit.PasswordChecker{}
 		fh.TaskQueue = mockTaskQueue
diff --git a/pkg/auth/handler/login_test.go b/pkg/auth/handler/login_test.go
index 024df0dd..49667f9c 100644
--- a/pkg/auth/handler/login_test.go
+++ b/pkg/auth/handler/login_test.go
@@ -21,7 +21,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/model"
 	coreAudit "github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
@@ -135,11 +135,11 @@ func TestLoginHandler(t *testing.T) {
 				},
 			},
 		)
-		tokenStore := authtoken.NewJWTStore("myApp", "secret", 0)
 
 		h := &LoginHandler{}
 		h.AuthInfoStore = authInfoStore
-		h.TokenStore = tokenStore
+		sessionProvider := session.NewMockProvider()
+		h.SessionProvider = sessionProvider
 		h.PasswordAuthProvider = passwordAuthProvider
 		h.IdentityProvider = principal.NewMockIdentityProvider(h.PasswordAuthProvider)
 		h.AuditTrail = coreAudit.NewMockTrail(t)
@@ -169,10 +169,8 @@ func TestLoginHandler(t *testing.T) {
 
 			// check the token
 			tokenStr := authResp.AccessToken
-			token := authtoken.Token{}
-			tokenStore.Get(tokenStr, &token)
-			So(token.AuthInfoID, ShouldEqual, userID)
-			So(!token.IsExpired(), ShouldBeTrue)
+			s, _ := sessionProvider.GetByToken(tokenStr, session.TokenKindAccessToken)
+			So(s.UserID, ShouldEqual, userID)
 		})
 
 		Convey("login user without login ID key", func() {
@@ -323,8 +321,7 @@ func TestLoginHandler(t *testing.T) {
 
 		lh := &LoginHandler{}
 		lh.AuthInfoStore = authInfoStore
-		mockTokenStore := authtoken.NewMockStore()
-		lh.TokenStore = mockTokenStore
+		lh.SessionProvider = session.NewMockProvider()
 		lh.PasswordAuthProvider = passwordAuthProvider
 		lh.IdentityProvider = principal.NewMockIdentityProvider(lh.PasswordAuthProvider)
 		lh.AuditTrail = coreAudit.NewMockTrail(t)
@@ -348,7 +345,6 @@ func TestLoginHandler(t *testing.T) {
 			h.ServeHTTP(resp, req)
 
 			So(resp.Code, ShouldEqual, 200)
-			token := mockTokenStore.GetTokensByAuthInfoID(userID)[0]
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`{
 				"result": {
 					"user": {
@@ -369,12 +365,9 @@ func TestLoginHandler(t *testing.T) {
 							"email": "john.doe@example.com"
 						}
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-john.doe.principal.id1-0"
 				}
-			}`,
-				userID,
-				token.AccessToken,
-			))
+			}`, userID, userID))
 
 			So(hookProvider.DispatchedEvents, ShouldResemble, []event.Payload{
 				event.SessionCreateEvent{
diff --git a/pkg/auth/handler/logout_test.go b/pkg/auth/handler/logout_test.go
index 1c19f7ba..9eff8e3e 100644
--- a/pkg/auth/handler/logout_test.go
+++ b/pkg/auth/handler/logout_test.go
@@ -12,33 +12,25 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
 	coreAudit "github.com/skygeario/skygear-server/pkg/core/audit"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	"github.com/skygeario/skygear-server/pkg/core/config"
-	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 )
 
 func TestLogoutHandler(t *testing.T) {
-	Convey("Test LogoutRequestPayload", t, func() {
-		Convey("validate valid payload", func() {
-			payload := LogoutRequestPayload{
-				AccessToken: "test_token",
-			}
-			So(payload.Validate(), ShouldBeNil)
-		})
-
-		Convey("validate missing access token", func() {
-			payload := LogoutRequestPayload{}
-			err := payload.Validate()
-			errResponse := err.(skyerr.Error)
-			So(errResponse.Code(), ShouldEqual, skyerr.NotAuthenticated)
-		})
-	})
-
 	Convey("Test LogoutHandler", t, func() {
 		h := &LogoutHandler{}
-		h.AuthContext = auth.NewMockContextGetterWithDefaultUser()
-		h.TokenStore = authtoken.NewJWTStore("myApp", "secret", 0)
+
+		authContext := authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
+		authContext.Session().ID = "session-id"
+		h.AuthContext = authContext
+
+		sessionProvider := session.NewMockProvider()
+		sessionProvider.Sessions[authContext.Session().ID] = *authContext.Session()
+		h.SessionProvider = sessionProvider
+
 		h.UserProfileStore = userprofile.NewMockUserProfileStore()
 		h.AuditTrail = coreAudit.NewMockTrail(t)
 		passwordAuthProvider := password.NewMockProviderWithPrincipalMap(
@@ -62,11 +54,7 @@ func TestLogoutHandler(t *testing.T) {
 		h.HookProvider = hookProvider
 
 		Convey("logout user successfully", func() {
-			token := "test_token"
-			payload := LogoutRequestPayload{
-				AccessToken: token,
-			}
-			resp, err := h.Handle(payload)
+			resp, err := h.Handle(nil)
 			So(resp, ShouldResemble, map[string]string{})
 			So(err, ShouldBeNil)
 
@@ -96,10 +84,7 @@ func TestLogoutHandler(t *testing.T) {
 		})
 
 		Convey("log audit trail when logout", func() {
-			payload := LogoutRequestPayload{
-				AccessToken: "test_token",
-			}
-			h.Handle(payload)
+			h.Handle(nil)
 			mockTrail, _ := h.AuditTrail.(*coreAudit.MockTrail)
 			So(mockTrail.Hook.LastEntry().Message, ShouldEqual, "audit_trail")
 			So(mockTrail.Hook.LastEntry().Data["event"], ShouldEqual, "logout")
diff --git a/pkg/auth/handler/reset_password_test.go b/pkg/auth/handler/reset_password_test.go
index 2105fa17..9656b5da 100644
--- a/pkg/auth/handler/reset_password_test.go
+++ b/pkg/auth/handler/reset_password_test.go
@@ -13,7 +13,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 	. "github.com/smartystreets/goconvey/convey"
@@ -85,7 +84,6 @@ func TestResetPasswordHandler(t *testing.T) {
 				},
 			},
 		)
-		tokenStore := authtoken.NewJWTStore("myApp", "secret", 0)
 		passwordChecker := &authAudit.PasswordChecker{
 			PwMinLength: 6,
 		}
@@ -93,7 +91,6 @@ func TestResetPasswordHandler(t *testing.T) {
 
 		h := &ResetPasswordHandler{}
 		h.AuthInfoStore = authInfoStore
-		h.TokenStore = tokenStore
 		h.UserProfileStore = userprofile.NewMockUserProfileStore()
 		h.PasswordChecker = passwordChecker
 		h.PasswordAuthProvider = passwordAuthProvider
diff --git a/pkg/auth/handler/signup_test.go b/pkg/auth/handler/signup_test.go
index 9373ef00..8497c7a7 100644
--- a/pkg/auth/handler/signup_test.go
+++ b/pkg/auth/handler/signup_test.go
@@ -22,8 +22,8 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/metadata"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -140,8 +140,7 @@ func TestSingupHandler(t *testing.T) {
 
 		sh := &SignupHandler{}
 		sh.AuthInfoStore = authInfoStore
-		mockTokenStore := authtoken.NewMockStore()
-		sh.TokenStore = mockTokenStore
+		sh.SessionProvider = session.NewMockProvider()
 		sh.PasswordChecker = passwordChecker
 		sh.PasswordAuthProvider = passwordAuthProvider
 		mockOAuthProvider := oauth.NewMockProvider([]*oauth.Principal{
@@ -234,7 +233,6 @@ func TestSingupHandler(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			userID := p.UserID
-			token := mockTokenStore.GetTokensByAuthInfoID(userID)[0]
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`{
 				"result": {
 					"user": {
@@ -256,12 +254,9 @@ func TestSingupHandler(t *testing.T) {
 							"email": "john.doe@example.com"
 						}
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-%s-0"
 				}
-			}`,
-				userID,
-				p.ID,
-				token.AccessToken))
+			}`, userID, p.ID, userID, p.ID))
 
 			So(hookProvider.DispatchedEvents, ShouldResemble, []event.Payload{
 				event.UserCreateEvent{
@@ -343,7 +338,6 @@ func TestSingupHandler(t *testing.T) {
 			err := sh.PasswordAuthProvider.GetPrincipalByLoginIDWithRealm("email", "john.doe@example.com", "admin", &p)
 			So(err, ShouldBeNil)
 			userID := p.UserID
-			token := mockTokenStore.GetTokensByAuthInfoID(userID)[0]
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`{
 				"result": {
 					"user": {
@@ -365,12 +359,9 @@ func TestSingupHandler(t *testing.T) {
 							"email": "john.doe@example.com"
 						}
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-%s-0"
 				}
-			}`,
-				userID,
-				p.ID,
-				token.AccessToken))
+			}`, userID, p.ID, userID, p.ID))
 
 			So(hookProvider.DispatchedEvents, ShouldResemble, []event.Payload{
 				event.UserCreateEvent{
@@ -565,7 +556,6 @@ func TestSingupHandler(t *testing.T) {
 		allowedRealms := []string{password.DefaultRealm, "admin"}
 		authInfoStore := authinfo.NewMockStore()
 		passwordAuthProvider := password.NewMockProvider(loginIDsKeys, allowedRealms)
-		tokenStore := authtoken.NewJWTStore("myApp", "secret", 0)
 
 		passwordChecker := &authAudit.PasswordChecker{
 			PwMinLength: 6,
@@ -573,7 +563,7 @@ func TestSingupHandler(t *testing.T) {
 
 		sh := &SignupHandler{}
 		sh.AuthInfoStore = authInfoStore
-		sh.TokenStore = tokenStore
+		sh.SessionProvider = session.NewMockProvider()
 		sh.PasswordChecker = passwordChecker
 		sh.PasswordAuthProvider = passwordAuthProvider
 		sh.IdentityProvider = principal.NewMockIdentityProvider(sh.PasswordAuthProvider)
diff --git a/pkg/auth/handler/sso/auth_handler_test.go b/pkg/auth/handler/sso/auth_handler_test.go
index cf4f36fa..a98f47b6 100644
--- a/pkg/auth/handler/sso/auth_handler_test.go
+++ b/pkg/auth/handler/sso/auth_handler_test.go
@@ -11,8 +11,6 @@ import (
 	"testing"
 	"time"
 
-	coreconfig "github.com/skygeario/skygear-server/pkg/core/config"
-
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/hook"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal/oauth"
@@ -20,10 +18,11 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/metadata"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
+	coreconfig "github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/crypto"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	coreHttp "github.com/skygeario/skygear-server/pkg/core/http"
@@ -92,7 +91,9 @@ func TestAuthHandler(t *testing.T) {
 		providerUserID := "mock_user_id"
 		sh := &AuthHandler{}
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		sh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000",
 			StateJWTSecret: stateJWTSecret,
@@ -122,8 +123,7 @@ func TestAuthHandler(t *testing.T) {
 			map[string]authinfo.AuthInfo{},
 		)
 		sh.AuthInfoStore = authInfoStore
-		mockTokenStore := authtoken.NewMockStore()
-		sh.TokenStore = mockTokenStore
+		sh.SessionProvider = session.NewMockProvider()
 		sh.UserProfileStore = userprofile.NewMockUserProfileStore()
 		sh.AuthHandlerHTMLProvider = sso.NewAuthHandlerHTMLProvider(
 			"https://api.example.com",
@@ -186,7 +186,6 @@ func TestAuthHandler(t *testing.T) {
 				ProviderUserID: providerUserID,
 			})
 			So(err, ShouldBeNil)
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 			So(actual, ShouldEqualJSON, fmt.Sprintf(`
 			{
 				"callback_url": "http://localhost:3000",
@@ -214,13 +213,10 @@ func TestAuthHandler(t *testing.T) {
 								"email": "mock@example.com"
 							}
 						},
-						"access_token": "%s"
+						"access_token": "access-token-%s-%s-0"
 					}
 				}
-			}`,
-				p.UserID,
-				p.ID,
-				token.AccessToken))
+			}`, p.UserID, p.ID, p.UserID, p.ID))
 		})
 
 		Convey("should return html page when ux_mode is web_popup", func() {
@@ -294,7 +290,6 @@ func TestAuthHandler(t *testing.T) {
 				ProviderType:   "google",
 				ProviderUserID: providerUserID,
 			})
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 			So(decoded, ShouldEqualJSON, fmt.Sprintf(`{
 				"result": {
 					"user": {
@@ -319,12 +314,9 @@ func TestAuthHandler(t *testing.T) {
 							"email": "mock@example.com"
 						}
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-%s-0"
 				}
-			}`,
-				p.UserID,
-				p.ID,
-				token.AccessToken))
+			}`, p.UserID, p.ID, p.UserID, p.ID))
 		})
 	})
 
@@ -333,7 +325,9 @@ func TestAuthHandler(t *testing.T) {
 		stateJWTSecret := "secret"
 		sh := &AuthHandler{}
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		sh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000",
 			StateJWTSecret: stateJWTSecret,
@@ -373,8 +367,7 @@ func TestAuthHandler(t *testing.T) {
 			},
 		)
 		sh.AuthInfoStore = authInfoStore
-		mockTokenStore := authtoken.NewMockStore()
-		sh.TokenStore = mockTokenStore
+		sh.SessionProvider = session.NewMockProvider()
 		sh.UserProfileStore = userprofile.NewMockUserProfileStore()
 		sh.AuthHandlerHTMLProvider = sso.NewAuthHandlerHTMLProvider(
 			"https://api.example.com",
@@ -514,7 +507,9 @@ func TestAuthHandler(t *testing.T) {
 
 		sh := &AuthHandler{}
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		sh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000",
 			StateJWTSecret: stateJWTSecret,
@@ -547,8 +542,7 @@ func TestAuthHandler(t *testing.T) {
 			},
 		)
 		sh.AuthInfoStore = authInfoStore
-		mockTokenStore := authtoken.NewMockStore()
-		sh.TokenStore = mockTokenStore
+		sh.SessionProvider = session.NewMockProvider()
 		profileData := map[string]map[string]interface{}{
 			"john.doe.id": map[string]interface{}{},
 		}
@@ -671,7 +665,6 @@ func TestAuthHandler(t *testing.T) {
 				ProviderType:   "google",
 				ProviderUserID: providerUserID,
 			})
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 			So(actual, ShouldEqualJSON, fmt.Sprintf(`
 			{
 				"callback_url": "http://localhost:3000",
@@ -698,7 +691,7 @@ func TestAuthHandler(t *testing.T) {
 								"email": "john.doe@example.com"
 							}
 						},
-						"access_token": "%s"
+						"access_token": "access-token-%s-%s-0"
 					}
 				}
 			}
@@ -706,7 +699,8 @@ func TestAuthHandler(t *testing.T) {
 				p.ID,
 				providerUserID,
 				providerUserID,
-				token.AccessToken))
+				p.UserID,
+				p.ID))
 		})
 
 		Convey("OnUserDuplicate == create", func() {
@@ -744,7 +738,6 @@ func TestAuthHandler(t *testing.T) {
 				ProviderUserID: providerUserID,
 			})
 			So(p.UserID, ShouldNotEqual, "john.doe.id")
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 			So(actual, ShouldEqualJSON, fmt.Sprintf(`
 			{
 				"callback_url": "http://localhost:3000",
@@ -772,7 +765,7 @@ func TestAuthHandler(t *testing.T) {
 								"email": "john.doe@example.com"
 							}
 						},
-						"access_token": "%s"
+						"access_token": "access-token-%s-%s-0"
 					}
 				}
 			}
@@ -780,7 +773,8 @@ func TestAuthHandler(t *testing.T) {
 				p.ID,
 				providerUserID,
 				providerUserID,
-				token.AccessToken))
+				p.UserID,
+				p.ID))
 		})
 	})
 }
diff --git a/pkg/auth/handler/sso/auth_url_test.go b/pkg/auth/handler/sso/auth_url_test.go
index 5d2b04bd..71673a4e 100644
--- a/pkg/auth/handler/sso/auth_url_test.go
+++ b/pkg/auth/handler/sso/auth_url_test.go
@@ -10,12 +10,12 @@ import (
 	"testing"
 
 	jwt "github.com/dgrijalva/jwt-go"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	. "github.com/smartystreets/goconvey/convey"
 
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal/password"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	coreconfig "github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
@@ -70,7 +70,9 @@ func TestAuthURLPayload(t *testing.T) {
 func TestAuthURLHandler(t *testing.T) {
 	Convey("Test TestAuthURLHandler", t, func() {
 		h := &AuthURLHandler{}
-		h.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		h.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000/auth",
 			StateJWTSecret: "secret",
diff --git a/pkg/auth/handler/sso/customtoken_test.go b/pkg/auth/handler/sso/customtoken_test.go
index 314ccc5d..a4e67775 100644
--- a/pkg/auth/handler/sso/customtoken_test.go
+++ b/pkg/auth/handler/sso/customtoken_test.go
@@ -22,8 +22,8 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
 	"github.com/skygeario/skygear-server/pkg/core/auth/metadata"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -39,7 +39,6 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 	}()
 
 	Convey("Test CustomTokenLoginHandler", t, func() {
-		mockTokenStore := authtoken.NewMockStore()
 		lh := &CustomTokenLoginHandler{}
 		issuer := "myissuer"
 		audience := "myaudience"
@@ -77,7 +76,7 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 		}
 		userProfileStore.TimeNowfunc = timeNow
 		lh.UserProfileStore = userProfileStore
-		lh.TokenStore = mockTokenStore
+		lh.SessionProvider = session.NewMockProvider()
 		lh.AuditTrail = audit.NewMockTrail(t)
 		lh.WelcomeEmailEnabled = true
 		mockTaskQueue := async.NewMockQueue()
@@ -109,7 +108,6 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 			h.ServeHTTP(resp, req)
 
 			p, _ := lh.CustomTokenAuthProvider.GetPrincipalByTokenPrincipalID("otherid1")
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 
 			So(resp.Code, ShouldEqual, 200)
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`{
@@ -139,14 +137,15 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 							"email": "John@skygear.io"
 						}
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-%s-0"
 				}
 			}`,
 				p.UserID,
 				p.ID,
 				iat.Unix(),
 				exp.Unix(),
-				token.AccessToken))
+				p.UserID,
+				p.ID))
 
 			mockTrail, _ := lh.AuditTrail.(*audit.MockTrail)
 			So(mockTrail.Hook.LastEntry().Message, ShouldEqual, "audit_trail")
@@ -328,7 +327,6 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 	})
 
 	Convey("Test OnUserDuplicate", t, func() {
-		mockTokenStore := authtoken.NewMockStore()
 		lh := &CustomTokenLoginHandler{}
 		issuer := "myissuer"
 		audience := "myaudience"
@@ -382,7 +380,7 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 		})
 		userProfileStore.TimeNowfunc = timeNow
 		lh.UserProfileStore = userProfileStore
-		lh.TokenStore = mockTokenStore
+		lh.SessionProvider = session.NewMockProvider()
 		lh.AuditTrail = audit.NewMockTrail(t)
 		lh.WelcomeEmailEnabled = true
 		mockTaskQueue := async.NewMockQueue()
@@ -438,7 +436,6 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 			So(resp.Code, ShouldEqual, 200)
 
 			p, _ := lh.CustomTokenAuthProvider.GetPrincipalByTokenPrincipalID("otherid1")
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`
 			{
@@ -467,13 +464,13 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 						},
 						"type": "custom_token"
 					},
-					"access_token": "%s"
+					"access_token": "access-token-john.doe.id-%s-0"
 				}
 			}
 			`, p.ID,
 				iat.Unix(),
 				exp.Unix(),
-				token.AccessToken))
+				p.ID))
 		})
 
 		Convey("OnUserDuplicate == create", func() {
@@ -488,7 +485,6 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 			So(resp.Code, ShouldEqual, 200)
 
 			p, _ := lh.CustomTokenAuthProvider.GetPrincipalByTokenPrincipalID("otherid1")
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`
 			{
@@ -518,10 +514,10 @@ func TestCustomTokenLoginHandler(t *testing.T) {
 						},
 						"type": "custom_token"
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-%s-0"
 				}
 			}
-			`, p.UserID, p.ID, iat.Unix(), exp.Unix(), token.AccessToken))
+			`, p.UserID, p.ID, iat.Unix(), exp.Unix(), p.UserID, p.ID))
 		})
 	})
 }
diff --git a/pkg/auth/handler/sso/link_test.go b/pkg/auth/handler/sso/link_test.go
index b6273bab..dd0a2a54 100644
--- a/pkg/auth/handler/sso/link_test.go
+++ b/pkg/auth/handler/sso/link_test.go
@@ -14,8 +14,8 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	coreconfig "github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -61,7 +61,9 @@ func TestLinkHandler(t *testing.T) {
 
 		sh := &LinkHandler{}
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		sh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:                      "http://localhost:3000",
 			StateJWTSecret:                 stateJWTSecret,
diff --git a/pkg/auth/handler/sso/login_test.go b/pkg/auth/handler/sso/login_test.go
index 4eda0dd1..78dbe90e 100644
--- a/pkg/auth/handler/sso/login_test.go
+++ b/pkg/auth/handler/sso/login_test.go
@@ -15,9 +15,9 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	coreconfig "github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -61,7 +61,9 @@ func TestLoginHandler(t *testing.T) {
 
 		sh := &LoginHandler{}
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		sh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:                      "http://localhost:3000",
 			StateJWTSecret:                 stateJWTSecret,
@@ -93,8 +95,7 @@ func TestLoginHandler(t *testing.T) {
 			map[string]authinfo.AuthInfo{},
 		)
 		sh.AuthInfoStore = authInfoStore
-		mockTokenStore := authtoken.NewMockStore()
-		sh.TokenStore = mockTokenStore
+		sh.SessionProvider = session.NewMockProvider()
 		sh.UserProfileStore = userprofile.NewMockUserProfileStore()
 		sh.OAuthConfiguration = oauthConfig
 		hookProvider := hook.NewMockProvider()
@@ -112,7 +113,6 @@ func TestLoginHandler(t *testing.T) {
 				ProviderType:   "google",
 				ProviderUserID: providerUserID,
 			})
-			token := mockTokenStore.GetTokensByAuthInfoID(p.UserID)[0]
 			So(resp.Body.Bytes(), ShouldEqualJSON, fmt.Sprintf(`{
 				"result": {
 					"user": {
@@ -137,12 +137,9 @@ func TestLoginHandler(t *testing.T) {
 							"email": "mock@example.com"
 						}
 					},
-					"access_token": "%s"
+					"access_token": "access-token-%s-%s-0"
 				}
-			}`,
-				p.UserID,
-				p.ID,
-				token.AccessToken))
+			}`, p.UserID, p.ID, p.UserID, p.ID))
 
 			So(hookProvider.DispatchedEvents, ShouldResemble, []event.Payload{
 				event.UserCreateEvent{
diff --git a/pkg/auth/handler/sso/unlink_test.go b/pkg/auth/handler/sso/unlink_test.go
index 942ddb2b..4e40a8bc 100644
--- a/pkg/auth/handler/sso/unlink_test.go
+++ b/pkg/auth/handler/sso/unlink_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -31,7 +31,9 @@ func TestUnlinkHandler(t *testing.T) {
 		sh := &UnlinkHandler{}
 		sh.ProviderID = providerID
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = auth.NewMockContextGetterWithDefaultUser()
+		sh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified()
 		sh.ProviderFactory = sso.NewProviderFactory(config.TenantConfiguration{
 			UserConfig: config.UserConfiguration{
 				SSO: config.SSOConfiguration{
@@ -107,7 +109,9 @@ func TestUnlinkHandler(t *testing.T) {
 		})
 
 		Convey("should disallow remove current identity", func() {
-			sh.AuthContext = auth.NewMockContextGetterWithUser("faseng.cat.id", "oauth-principal-id", true, map[string]bool{})
+			sh.AuthContext = authtest.NewMockContext().
+				UseUser("faseng.cat.id", "oauth-principal-id").
+				MarkVerified()
 			h := handler.APIHandlerToHandler(sh, sh.TxContext)
 
 			req, _ := http.NewRequest("POST", "", strings.NewReader(`{
diff --git a/pkg/auth/handler/update_metadata_test.go b/pkg/auth/handler/update_metadata_test.go
index 87305373..c40cee04 100644
--- a/pkg/auth/handler/update_metadata_test.go
+++ b/pkg/auth/handler/update_metadata_test.go
@@ -14,8 +14,8 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -36,7 +36,9 @@ func TestUpdateMetadataHandler(t *testing.T) {
 		userID := "john.doe.id"
 
 		uh := &UpdateMetadataHandler{}
-		uh.AuthContext = auth.NewMockContextGetterWithUser(userID, "john.doe.principal.id0", true, map[string]bool{})
+		uh.AuthContext = authtest.NewMockContext().
+			UseUser(userID, "john.doe.principal.id0").
+			MarkVerified()
 		uh.AuthInfoStore = authinfo.NewMockStoreWithAuthInfoMap(
 			map[string]authinfo.AuthInfo{
 				userID: authinfo.AuthInfo{
@@ -227,7 +229,10 @@ func TestUpdateMetadataHandler(t *testing.T) {
 		userID := "john.doe.id"
 
 		uh := &UpdateMetadataHandler{}
-		uh.AuthContext = auth.NewMockContextGetterWithMasterkeyDefaultUser()
+		uh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id").
+			MarkVerified().
+			UseMasterKey()
 		uh.AuthInfoStore = authinfo.NewMockStoreWithAuthInfoMap(
 			map[string]authinfo.AuthInfo{
 				userID: authinfo.AuthInfo{
diff --git a/pkg/auth/handler/userverify/verify_code_test.go b/pkg/auth/handler/userverify/verify_code_test.go
index 1d421f53..ad440d3f 100644
--- a/pkg/auth/handler/userverify/verify_code_test.go
+++ b/pkg/auth/handler/userverify/verify_code_test.go
@@ -8,18 +8,18 @@ import (
 	gotime "time"
 
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/hook"
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/time"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/event"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
+	"github.com/skygeario/skygear-server/pkg/core/time"
 
 	"github.com/sirupsen/logrus"
 	"github.com/sirupsen/logrus/hooks/test"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal/password"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userverify"
 
-	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
+	authtest "github.com/skygeario/skygear-server/pkg/core/auth/testing"
 	"github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/handler"
@@ -27,7 +27,7 @@ import (
 	. "github.com/smartystreets/goconvey/convey"
 )
 
-func TestForgotPasswordResetHandler(t *testing.T) {
+func TestVerifyCodeHandler(t *testing.T) {
 	Convey("Test VerifyCodeHandler", t, func() {
 		time := time.MockProvider{TimeNowUTC: gotime.Date(2006, 1, 2, 15, 4, 5, 0, gotime.UTC)}
 
@@ -35,9 +35,8 @@ func TestForgotPasswordResetHandler(t *testing.T) {
 		logger, _ := test.NewNullLogger()
 		vh.Logger = logrus.NewEntry(logger)
 		vh.TxContext = db.NewMockTxContext()
-		vh.AuthContext = auth.NewMockContextGetterWithUnverifiedUser(map[string]bool{
-			"faseng.cat.id@example.com": false,
-		})
+		vh.AuthContext = authtest.NewMockContext().
+			UseUser("faseng.cat.id", "faseng.cat.principal.id")
 
 		zero := 0
 		one := 1
@@ -150,13 +149,11 @@ func TestForgotPasswordResetHandler(t *testing.T) {
 						"faseng.cat.id@example.com": true,
 					},
 					User: model.User{
-						ID:       "faseng.cat.id",
-						Verified: false,
-						Disabled: false,
-						VerifyInfo: map[string]bool{
-							"faseng.cat.id@example.com": false,
-						},
-						Metadata: userprofile.Data{},
+						ID:         "faseng.cat.id",
+						Verified:   false,
+						Disabled:   false,
+						VerifyInfo: map[string]bool{},
+						Metadata:   userprofile.Data{},
 					},
 				},
 			})
diff --git a/pkg/core/auth/authz/policy/authinfo_test.go b/pkg/core/auth/authz/policy/authinfo_test.go
index c5d2a70d..cf6f83ae 100644
--- a/pkg/core/auth/authz/policy/authinfo_test.go
+++ b/pkg/core/auth/authz/policy/authinfo_test.go
@@ -6,7 +6,7 @@ import (
 	"time"
 
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 
 	. "github.com/smartystreets/goconvey/convey"
 )
@@ -29,8 +29,8 @@ func TestRequireAuthenticated(t *testing.T) {
 					ID:              "ID",
 					TokenValidSince: &validSince,
 				},
-				mToken: &authtoken.Token{
-					IssuedAt: time.Date(2016, 10, 1, 0, 0, 0, 0, time.UTC),
+				mSession: &session.Session{
+					AccessTokenCreatedAt: time.Date(2016, 10, 1, 0, 0, 0, 0, time.UTC),
 				},
 			}
 
@@ -44,8 +44,8 @@ func TestRequireAuthenticated(t *testing.T) {
 				mAuthInfo: &authinfo.AuthInfo{
 					ID: "ID",
 				},
-				mToken: &authtoken.Token{
-					IssuedAt: time.Date(2016, 10, 1, 0, 0, 0, 0, time.UTC),
+				mSession: &session.Session{
+					AccessTokenCreatedAt: time.Date(2016, 10, 1, 0, 0, 0, 0, time.UTC),
 				},
 			}
 
diff --git a/pkg/core/auth/authz/policy/policy_test.go b/pkg/core/auth/authz/policy/policy_test.go
index b1d0d8cc..e9e263fd 100644
--- a/pkg/core/auth/authz/policy/policy_test.go
+++ b/pkg/core/auth/authz/policy/policy_test.go
@@ -2,20 +2,20 @@ package policy
 
 import (
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
-	"github.com/skygeario/skygear-server/pkg/core/auth/authtoken"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
 	"github.com/skygeario/skygear-server/pkg/core/model"
 )
 
 type ContextGetter interface {
 	AccessKeyType() model.KeyType
 	AuthInfo() *authinfo.AuthInfo
-	Token() *authtoken.Token
+	Session() *session.Session
 }
 
 type MemoryContextGetter struct {
 	mAccessKeyType model.KeyType
 	mAuthInfo      *authinfo.AuthInfo
-	mToken         *authtoken.Token
+	mSession       *session.Session
 }
 
 func (g MemoryContextGetter) AccessKeyType() model.KeyType {
@@ -26,6 +26,6 @@ func (g MemoryContextGetter) AuthInfo() *authinfo.AuthInfo {
 	return g.mAuthInfo
 }
 
-func (g MemoryContextGetter) Token() *authtoken.Token {
-	return g.mToken
+func (g MemoryContextGetter) Session() *session.Session {
+	return g.mSession
 }
diff --git a/pkg/core/auth/session/provider_impl_test.go b/pkg/core/auth/session/provider_impl_test.go
new file mode 100644
index 00000000..bfb9eb3f
--- /dev/null
+++ b/pkg/core/auth/session/provider_impl_test.go
@@ -0,0 +1,152 @@
+package session
+
+import (
+	"testing"
+	gotime "time"
+
+	corerand "github.com/skygeario/skygear-server/pkg/core/rand"
+	"github.com/skygeario/skygear-server/pkg/core/time"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestProvider(t *testing.T) {
+	Convey("Provider", t, func() {
+		store := NewMockStore()
+
+		timeProvider := &time.MockProvider{}
+		initialTime := gotime.Date(2006, 1, 2, 15, 4, 5, 0, gotime.UTC)
+		timeProvider.TimeNow = initialTime
+		timeProvider.TimeNowUTC = initialTime
+
+		var provider Provider = &providerImpl{
+			store: store,
+			time:  timeProvider,
+			rand:  corerand.InsecureRand,
+		}
+
+		Convey("creating session", func() {
+			Convey("should be successful", func() {
+				session, err := provider.Create("user-id", "principal-id")
+				So(err, ShouldBeNil)
+				So(session, ShouldResemble, &Session{
+					ID:                   session.ID,
+					UserID:               "user-id",
+					PrincipalID:          "principal-id",
+					CreatedAt:            initialTime,
+					AccessedAt:           initialTime,
+					AccessToken:          session.AccessToken,
+					AccessTokenCreatedAt: initialTime,
+				})
+				So(session.AccessToken, ShouldHaveLength, tokenLength+len(session.ID)+1)
+			})
+
+			Convey("should allow creating multiple sessions for same principal", func() {
+				session1, err := provider.Create("user-id", "principal-id")
+				So(err, ShouldBeNil)
+				So(session1, ShouldResemble, &Session{
+					ID:                   session1.ID,
+					UserID:               "user-id",
+					PrincipalID:          "principal-id",
+					CreatedAt:            initialTime,
+					AccessedAt:           initialTime,
+					AccessToken:          session1.AccessToken,
+					AccessTokenCreatedAt: initialTime,
+				})
+
+				session2, err := provider.Create("user-id", "principal-id")
+				So(err, ShouldBeNil)
+				So(session2, ShouldResemble, &Session{
+					ID:                   session2.ID,
+					UserID:               "user-id",
+					PrincipalID:          "principal-id",
+					CreatedAt:            initialTime,
+					AccessedAt:           initialTime,
+					AccessToken:          session2.AccessToken,
+					AccessTokenCreatedAt: initialTime,
+				})
+
+				So(session1.ID, ShouldNotEqual, session2.ID)
+			})
+		})
+
+		Convey("getting session", func() {
+			fixtureSession := Session{
+				ID:                   "session-id",
+				UserID:               "user-id",
+				PrincipalID:          "principal-id",
+				CreatedAt:            initialTime,
+				AccessedAt:           initialTime,
+				AccessToken:          "session-id.access-token",
+				AccessTokenCreatedAt: initialTime,
+			}
+			store.Sessions["session-id"] = fixtureSession
+
+			Convey("should be successful", func() {
+				session, err := provider.GetByToken("session-id.access-token", TokenKindAccessToken)
+				So(err, ShouldBeNil)
+				So(session, ShouldResemble, &fixtureSession)
+			})
+
+			Convey("should reject non-existant session", func() {
+				session, err := provider.GetByToken("session-id-unknown.access-token", TokenKindAccessToken)
+				So(err, ShouldBeError, ErrSessionNotFound)
+				So(session, ShouldBeNil)
+			})
+
+			Convey("should reject incorrect token", func() {
+				session, err := provider.GetByToken("session-id.incorrect-token", TokenKindAccessToken)
+				So(err, ShouldBeError, ErrSessionNotFound)
+				So(session, ShouldBeNil)
+
+				session, err = provider.GetByToken("invalid-token", TokenKindAccessToken)
+				So(err, ShouldBeError, ErrSessionNotFound)
+				So(session, ShouldBeNil)
+			})
+		})
+
+		Convey("accessing session", func() {
+			session := Session{
+				ID:                   "session-id",
+				UserID:               "user-id",
+				PrincipalID:          "principal-id",
+				CreatedAt:            initialTime,
+				AccessedAt:           initialTime,
+				AccessToken:          "access-token",
+				AccessTokenCreatedAt: initialTime,
+			}
+			timeProvider.AdvanceSeconds(100)
+			timeNow := timeProvider.TimeNowUTC
+			store.Sessions["session-id"] = session
+
+			Convey("should be update accessed at time", func() {
+				err := provider.Access(&session)
+				So(err, ShouldBeNil)
+				So(session.AccessedAt, ShouldEqual, timeNow)
+			})
+		})
+
+		Convey("invalidating session", func() {
+			store.Sessions["session-id"] = Session{
+				ID:                   "session-id",
+				UserID:               "user-id",
+				PrincipalID:          "principal-id",
+				CreatedAt:            initialTime,
+				AccessedAt:           initialTime,
+				AccessToken:          "access-token",
+				AccessTokenCreatedAt: initialTime,
+			}
+
+			Convey("should be successful", func() {
+				err := provider.Invalidate("session-id")
+				So(err, ShouldBeNil)
+				So(store.Sessions, ShouldBeEmpty)
+			})
+
+			Convey("should be successful for non-existant sessions", func() {
+				err := provider.Invalidate("session-id-unknown")
+				So(err, ShouldBeNil)
+				So(store.Sessions, ShouldNotBeEmpty)
+			})
+		})
+	})
+}
diff --git a/pkg/core/auth/testing/context.go b/pkg/core/auth/testing/context.go
new file mode 100644
index 00000000..375155f0
--- /dev/null
+++ b/pkg/core/auth/testing/context.go
@@ -0,0 +1,63 @@
+package testing
+
+import (
+	"fmt"
+
+	"github.com/skygeario/skygear-server/pkg/core/auth"
+	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
+	"github.com/skygeario/skygear-server/pkg/core/auth/session"
+	"github.com/skygeario/skygear-server/pkg/core/model"
+)
+
+type MockContext struct {
+	accessKeyType model.KeyType
+	authInfo      *authinfo.AuthInfo
+	session       *session.Session
+}
+
+var _ auth.ContextGetter = &MockContext{}
+
+func NewMockContext() *MockContext {
+	return &MockContext{accessKeyType: model.APIAccessKey}
+}
+
+func (m *MockContext) AccessKeyType() model.KeyType {
+	return m.accessKeyType
+}
+
+func (m *MockContext) AuthInfo() *authinfo.AuthInfo {
+	return m.authInfo
+}
+
+func (m *MockContext) Session() *session.Session {
+	return m.session
+}
+
+func (m *MockContext) UseNoAccessKey() *MockContext {
+	m.accessKeyType = model.NoAccessKey
+	return m
+}
+
+func (m *MockContext) UseMasterKey() *MockContext {
+	m.accessKeyType = model.MasterAccessKey
+	return m
+}
+
+func (m *MockContext) UseUser(userID string, principalID string) *MockContext {
+	m.authInfo = &authinfo.AuthInfo{
+		ID:         userID,
+		VerifyInfo: map[string]bool{},
+	}
+	m.session = &session.Session{
+		ID:          fmt.Sprintf("%s-%s", userID, principalID),
+		UserID:      userID,
+		PrincipalID: principalID,
+		AccessToken: fmt.Sprintf("access-token-%s-%s", userID, principalID),
+	}
+	return m
+}
+
+func (m *MockContext) MarkVerified() *MockContext {
+	m.authInfo.Verified = true
+	return m
+}
diff --git a/pkg/core/config/tenant_test.go b/pkg/core/config/tenant_test.go
index 7a04bb8e..de09a631 100644
--- a/pkg/core/config/tenant_test.go
+++ b/pkg/core/config/tenant_test.go
@@ -27,8 +27,6 @@ user_config:
         type: phone
       username:
         type: raw
-  token_store:
-    secret: tokensecret
   hook:
     secret: hooksecret
   sso:
@@ -64,9 +62,6 @@ const inputMinimalJSON = `
 			},
 			"allowed_realms": ["default"]
 		},
-		"token_store": {
-			"secret": "tokensecret"
-		},
 		"hook": {
 			"secret": "hooksecret"
 		},
@@ -144,10 +139,7 @@ func makeFullTenantConfig() TenantConfiguration {
 				AllowedRealms:              []string{"default"},
 				OnUserDuplicateAllowCreate: true,
 			},
-			TokenStore: TokenStoreConfiguration{
-				Secret: "mytokenstoresecret",
-				Expiry: 0,
-			},
+
 			UserAudit: UserAuditConfiguration{
 				Enabled:         true,
 				TrailHandlerURL: "http://localhost:3000/useraudit",
diff --git a/pkg/core/middleware/tenant_test.go b/pkg/core/middleware/tenant_test.go
index 1b27de82..37c3eb46 100644
--- a/pkg/core/middleware/tenant_test.go
+++ b/pkg/core/middleware/tenant_test.go
@@ -20,10 +20,6 @@ var (
 		UserConfig: config.UserConfiguration{
 			APIKey:    "APIKey",
 			MasterKey: "MasterKey",
-			TokenStore: config.TokenStoreConfiguration{
-				Secret: "Secret",
-				Expiry: 1000,
-			},
 		},
 	}
 )
diff --git a/pkg/core/validation/schema_test.go b/pkg/core/validation/schema_test.go
index f252b964..6d4ae72a 100644
--- a/pkg/core/validation/schema_test.go
+++ b/pkg/core/validation/schema_test.go
@@ -20,22 +20,19 @@ func TestValidateUserConfiguration(t *testing.T) {
 #: auth is required
 #: hook is required
 #: master_key is required
-#: token_store is required
 `,
 			},
-			// Empty auth and empty token_store
+			// Empty auth
 			{
 				`
 {
 	"api_key": "api_key",
 	"master_key": "master_key",
 	"auth": {},
-	"token_store": {},
 	"hook": {}
 }
 				`,
 				`#/hook: secret is required
-#/token_store: secret is required
 `,
 			},
 			// Empty auth.login_id_keys and auth.allowed_realms
@@ -48,14 +45,12 @@ func TestValidateUserConfiguration(t *testing.T) {
 		"login_id_keys": {},
 		"allowed_realms": []
 	},
-	"token_store": {},
 	"hook": {}
 }
 				`,
 				`#/auth/allowed_realms: Array must have at least 1 items
 #/auth/login_id_keys: Must have at least 1 properties
 #/hook: secret is required
-#/token_store: secret is required
 `,
 			},
 			// Invalid login id type
@@ -81,13 +76,11 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {},
 	"hook": {}
 }
 				`,
 				`#/auth/login_id_keys/type: auth.login_id_keys.type must be one of the following: "raw", "email", "phone"
 #/hook: secret is required
-#/token_store: secret is required
 `,
 			},
 			// Minimal valid example
@@ -110,9 +103,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	}
@@ -140,9 +130,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	},
@@ -172,9 +159,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	},
@@ -216,9 +200,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	},
@@ -250,9 +231,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	},
@@ -289,9 +267,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	},
@@ -342,9 +317,6 @@ func TestValidateUserConfiguration(t *testing.T) {
 		},
 		"allowed_realms": ["default"]
 	},
-	"token_store": {
-		"secret": "tokensecret"
-	},
 	"hook": {
 		"secret": "hooksecret"
 	},