diff --git a/locale/translations.go b/locale/translations.go
index 64fb49052ae71f80173c9cfdfc8dcfc478e58dc9..8f5f4edab52a6f7ddaca05840bc68b23f2457f4e 100644
--- a/locale/translations.go
+++ b/locale/translations.go
@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 18:49:24.054372873 -0800 PST m=+0.026968745
+// 2017-12-16 17:48:32.323083386 -0800 PST m=+0.056720065
 
 package locale
 
@@ -169,12 +169,14 @@ var translations = map[string]string{
     "Fever Password": "Mot de passe pour l'API de Fever",
     "Fetch original content": "Récupérer le contenu original",
     "Scraper Rules": "Règles pour récupérer le contenu original",
-    "Rewrite Rules": "Règles de réécriture"
+    "Rewrite Rules": "Règles de réécriture",
+    "Preferences saved!": "Préférences sauvegardées !",
+    "Your external account is now linked !": "Votre compte externe est maintenant associé !"
 }
 `,
 }
 
 var translationsChecksums = map[string]string{
 	"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
-	"fr_FR": "0e14d65f38ca5c5e34f1d84f6837ce8a29a4ae5f8836b384bb098222b724cb5b",
+	"fr_FR": "f52a6503ee61d1103adb280c242d438a89936b34d147d29c2502cec8b2cc9ff9",
 }
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index a7c34f934f2c1b069f703ed27a84d6144a953b17..5015f2ae8a67c03a8f3ab9438db14876a2d248eb 100644
--- a/locale/translations/fr_FR.json
+++ b/locale/translations/fr_FR.json
@@ -153,5 +153,7 @@
     "Fever Password": "Mot de passe pour l'API de Fever",
     "Fetch original content": "Récupérer le contenu original",
     "Scraper Rules": "Règles pour récupérer le contenu original",
-    "Rewrite Rules": "Règles de réécriture"
+    "Rewrite Rules": "Règles de réécriture",
+    "Preferences saved!": "Préférences sauvegardées !",
+    "Your external account is now linked !": "Votre compte externe est maintenant associé !"
 }
diff --git a/model/session.go b/model/session.go
new file mode 100644
index 0000000000000000000000000000000000000000..2a7430d168d78758a96fd84fa28064daca13f5f9
--- /dev/null
+++ b/model/session.go
@@ -0,0 +1,56 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package model
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"errors"
+	"fmt"
+)
+
+// SessionData represents the data attached to the session.
+type SessionData struct {
+	CSRF              string `json:"csrf"`
+	OAuth2State       string `json:"oauth2_state"`
+	FlashMessage      string `json:"flash_message"`
+	FlashErrorMessage string `json:"flash_error_message"`
+}
+
+func (s SessionData) String() string {
+	return fmt.Sprintf(`CSRF="%s", "OAuth2State="%s", FlashMessage="%s", "FlashErrorMessage="%s"`,
+		s.CSRF, s.OAuth2State, s.FlashMessage, s.FlashErrorMessage)
+}
+
+// Value converts the session data to JSON.
+func (s SessionData) Value() (driver.Value, error) {
+	j, err := json.Marshal(s)
+	return j, err
+}
+
+// Scan converts raw JSON data.
+func (s *SessionData) Scan(src interface{}) error {
+	source, ok := src.([]byte)
+	if !ok {
+		return errors.New("session: unable to assert type of src")
+	}
+
+	err := json.Unmarshal(source, s)
+	if err != nil {
+		return fmt.Errorf("session: %v", err)
+	}
+
+	return err
+}
+
+// Session represents a session in the system.
+type Session struct {
+	ID   string
+	Data *SessionData
+}
+
+func (s *Session) String() string {
+	return fmt.Sprintf(`ID="%s", Data="%v"`, s.ID, s.Data)
+}
diff --git a/model/token.go b/model/token.go
deleted file mode 100644
index 3c5c323292624ff89eb63ee2e97316ba94f91356..0000000000000000000000000000000000000000
--- a/model/token.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package model
-
-import "fmt"
-
-// Token represents a CSRF token in the system.
-type Token struct {
-	ID    string
-	Value string
-}
-
-func (t Token) String() string {
-	return fmt.Sprintf(`ID="%s"`, t.ID)
-}
diff --git a/server/cookie/cookie.go b/server/cookie/cookie.go
new file mode 100644
index 0000000000000000000000000000000000000000..d028d877daf1818702d09c9c013b83c84e9e69f4
--- /dev/null
+++ b/server/cookie/cookie.go
@@ -0,0 +1,40 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package cookie
+
+import (
+	"net/http"
+	"time"
+)
+
+// Cookie names.
+const (
+	CookieSessionID     = "sessionID"
+	CookieUserSessionID = "userSessionID"
+)
+
+// New create a new cookie.
+func New(name, value string, isHTTPS bool) *http.Cookie {
+	return &http.Cookie{
+		Name:     name,
+		Value:    value,
+		Path:     "/",
+		Secure:   isHTTPS,
+		HttpOnly: true,
+	}
+}
+
+// Expired returns an expired cookie.
+func Expired(name string, isHTTPS bool) *http.Cookie {
+	return &http.Cookie{
+		Name:     name,
+		Value:    "",
+		Path:     "/",
+		Secure:   isHTTPS,
+		HttpOnly: true,
+		MaxAge:   -1,
+		Expires:  time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
+	}
+}
diff --git a/server/core/context.go b/server/core/context.go
index 50496b33adb8012b014db1485cb97732cafe44e9..dfd8e5b3ae05776788bf7b18b913a7debc785477 100644
--- a/server/core/context.go
+++ b/server/core/context.go
@@ -7,6 +7,8 @@ package core
 import (
 	"net/http"
 
+	"github.com/miniflux/miniflux/helper"
+	"github.com/miniflux/miniflux/locale"
 	"github.com/miniflux/miniflux/logger"
 	"github.com/miniflux/miniflux/model"
 	"github.com/miniflux/miniflux/server/middleware"
@@ -18,11 +20,12 @@ import (
 
 // Context contains helper functions related to the current request.
 type Context struct {
-	writer  http.ResponseWriter
-	request *http.Request
-	store   *storage.Storage
-	router  *mux.Router
-	user    *model.User
+	writer     http.ResponseWriter
+	request    *http.Request
+	store      *storage.Storage
+	router     *mux.Router
+	user       *model.User
+	translator *locale.Translator
 }
 
 // IsAdminUser checks if the logged user is administrator.
@@ -35,10 +38,11 @@ func (c *Context) IsAdminUser() bool {
 
 // UserTimezone returns the timezone used by the logged user.
 func (c *Context) UserTimezone() string {
-	if v := c.request.Context().Value(middleware.UserTimezoneContextKey); v != nil {
-		return v.(string)
+	value := c.getContextStringValue(middleware.UserTimezoneContextKey)
+	if value == "" {
+		value = "UTC"
 	}
-	return "UTC"
+	return value
 }
 
 // IsAuthenticated returns a boolean if the user is authenticated.
@@ -80,13 +84,68 @@ func (c *Context) UserLanguage() string {
 	return user.Language
 }
 
-// CsrfToken returns the current CSRF token.
-func (c *Context) CsrfToken() string {
-	if v := c.request.Context().Value(middleware.TokenContextKey); v != nil {
+// Translate translates a message in the current language.
+func (c *Context) Translate(message string, args ...interface{}) string {
+	return c.translator.GetLanguage(c.UserLanguage()).Get(message, args...)
+}
+
+// CSRF returns the current CSRF token.
+func (c *Context) CSRF() string {
+	return c.getContextStringValue(middleware.CSRFContextKey)
+}
+
+// SessionID returns the current session ID.
+func (c *Context) SessionID() string {
+	return c.getContextStringValue(middleware.SessionIDContextKey)
+}
+
+// UserSessionToken returns the current user session token.
+func (c *Context) UserSessionToken() string {
+	return c.getContextStringValue(middleware.UserSessionTokenContextKey)
+}
+
+// OAuth2State returns the current OAuth2 state.
+func (c *Context) OAuth2State() string {
+	return c.getContextStringValue(middleware.OAuth2StateContextKey)
+}
+
+// GenerateOAuth2State generate a new OAuth2 state.
+func (c *Context) GenerateOAuth2State() string {
+	state := helper.GenerateRandomString(32)
+	c.store.UpdateSessionField(c.SessionID(), "oauth2_state", state)
+	return state
+}
+
+// SetFlashMessage defines a new flash message.
+func (c *Context) SetFlashMessage(message string) {
+	c.store.UpdateSessionField(c.SessionID(), "flash_message", message)
+}
+
+// FlashMessage returns the flash message and remove it.
+func (c *Context) FlashMessage() string {
+	message := c.getContextStringValue(middleware.FlashMessageContextKey)
+	c.store.UpdateSessionField(c.SessionID(), "flash_message", "")
+	return message
+}
+
+// SetFlashErrorMessage defines a new flash error message.
+func (c *Context) SetFlashErrorMessage(message string) {
+	c.store.UpdateSessionField(c.SessionID(), "flash_error_message", message)
+}
+
+// FlashErrorMessage returns the error flash message and remove it.
+func (c *Context) FlashErrorMessage() string {
+	message := c.getContextStringValue(middleware.FlashMessageContextKey)
+	c.store.UpdateSessionField(c.SessionID(), "flash_error_message", "")
+	return message
+}
+
+func (c *Context) getContextStringValue(key *middleware.ContextKey) string {
+	if v := c.request.Context().Value(key); v != nil {
 		return v.(string)
 	}
 
-	logger.Error("No CSRF token in context!")
+	logger.Error("[Core:Context] Missing key: %s", key)
 	return ""
 }
 
@@ -96,6 +155,6 @@ func (c *Context) Route(name string, args ...interface{}) string {
 }
 
 // NewContext creates a new Context.
-func NewContext(w http.ResponseWriter, r *http.Request, store *storage.Storage, router *mux.Router) *Context {
-	return &Context{writer: w, request: r, store: store, router: router}
+func NewContext(w http.ResponseWriter, r *http.Request, store *storage.Storage, router *mux.Router, translator *locale.Translator) *Context {
+	return &Context{writer: w, request: r, store: store, router: router, translator: translator}
 }
diff --git a/server/core/handler.go b/server/core/handler.go
index c55cf792396245aa94f5d1e8d36f04f5a0c528b1..a5d84afa0a2e7e08ddab5820832f0700b6c12af5 100644
--- a/server/core/handler.go
+++ b/server/core/handler.go
@@ -36,7 +36,7 @@ func (h *Handler) Use(f HandlerFunc) http.Handler {
 		defer helper.ExecutionTime(time.Now(), r.URL.Path)
 		logger.Debug("[HTTP] %s %s", r.Method, r.URL.Path)
 
-		ctx := NewContext(w, r, h.store, h.router)
+		ctx := NewContext(w, r, h.store, h.router, h.translator)
 		request := NewRequest(w, r)
 		response := NewResponse(w, r, h.template)
 
diff --git a/server/middleware/context_keys.go b/server/middleware/context_keys.go
index 3099322ad67b0a108a34973799b2002a3c9a4d98..31ad286ea48ad75cda9bffc91fa2dc369809a6b5 100644
--- a/server/middleware/context_keys.go
+++ b/server/middleware/context_keys.go
@@ -4,23 +4,43 @@
 
 package middleware
 
-type contextKey struct {
+// ContextKey represents a context key.
+type ContextKey struct {
 	name string
 }
 
+func (c ContextKey) String() string {
+	return c.name
+}
+
 var (
 	// UserIDContextKey is the context key used to store the user ID.
-	UserIDContextKey = &contextKey{"UserID"}
+	UserIDContextKey = &ContextKey{"UserID"}
 
 	// UserTimezoneContextKey is the context key used to store the user timezone.
-	UserTimezoneContextKey = &contextKey{"UserTimezone"}
+	UserTimezoneContextKey = &ContextKey{"UserTimezone"}
 
 	// IsAdminUserContextKey is the context key used to store the user role.
-	IsAdminUserContextKey = &contextKey{"IsAdminUser"}
+	IsAdminUserContextKey = &ContextKey{"IsAdminUser"}
 
 	// IsAuthenticatedContextKey is the context key used to store the authentication flag.
-	IsAuthenticatedContextKey = &contextKey{"IsAuthenticated"}
+	IsAuthenticatedContextKey = &ContextKey{"IsAuthenticated"}
+
+	// UserSessionTokenContextKey is the context key used to store the user session ID.
+	UserSessionTokenContextKey = &ContextKey{"UserSessionToken"}
+
+	// SessionIDContextKey is the context key used to store the session ID.
+	SessionIDContextKey = &ContextKey{"SessionID"}
+
+	// CSRFContextKey is the context key used to store CSRF token.
+	CSRFContextKey = &ContextKey{"CSRF"}
+
+	// OAuth2StateContextKey is the context key used to store OAuth2 state.
+	OAuth2StateContextKey = &ContextKey{"OAuth2State"}
+
+	// FlashMessageContextKey is the context key used to store a flash message.
+	FlashMessageContextKey = &ContextKey{"FlashMessage"}
 
-	// TokenContextKey is the context key used to store CSRF token.
-	TokenContextKey = &contextKey{"CSRF"}
+	// FlashErrorMessageContextKey is the context key used to store a flash error message.
+	FlashErrorMessageContextKey = &ContextKey{"FlashErrorMessage"}
 )
diff --git a/server/middleware/session.go b/server/middleware/session.go
index 3759565f363c0d3ad6171d561cf6861283080b4e..2891b68a1f4b75647498ff1032528e519ab24371 100644
--- a/server/middleware/session.go
+++ b/server/middleware/session.go
@@ -10,60 +10,66 @@ import (
 
 	"github.com/miniflux/miniflux/logger"
 	"github.com/miniflux/miniflux/model"
-	"github.com/miniflux/miniflux/server/route"
+	"github.com/miniflux/miniflux/server/cookie"
 	"github.com/miniflux/miniflux/storage"
-
-	"github.com/gorilla/mux"
 )
 
 // SessionMiddleware represents a session middleware.
 type SessionMiddleware struct {
-	store  *storage.Storage
-	router *mux.Router
+	store *storage.Storage
 }
 
 // Handler execute the middleware.
-func (s *SessionMiddleware) Handler(next http.Handler) http.Handler {
+func (t *SessionMiddleware) Handler(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		session := s.getSessionFromCookie(r)
+		var err error
+		session := t.getSessionValueFromCookie(r)
 
 		if session == nil {
 			logger.Debug("[Middleware:Session] Session not found")
-			if s.isPublicRoute(r) {
-				next.ServeHTTP(w, r)
-			} else {
-				http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound)
+			session, err = t.store.CreateSession()
+			if err != nil {
+				logger.Error("[Middleware:Session] %v", err)
+				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+				return
 			}
+
+			http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, r.URL.Scheme == "https"))
 		} else {
 			logger.Debug("[Middleware:Session] %s", session)
-			ctx := r.Context()
-			ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
-			ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
+		}
 
-			next.ServeHTTP(w, r.WithContext(ctx))
+		if r.Method == "POST" {
+			formValue := r.FormValue("csrf")
+			headerValue := r.Header.Get("X-Csrf-Token")
+
+			if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
+				logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
+				w.WriteHeader(http.StatusBadRequest)
+				w.Write([]byte("Invalid or missing CSRF session!"))
+				return
+			}
 		}
-	})
-}
 
-func (s *SessionMiddleware) isPublicRoute(r *http.Request) bool {
-	route := mux.CurrentRoute(r)
-	switch route.GetName() {
-	case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback", "appIcon", "favicon":
-		return true
-	default:
-		return false
-	}
+		ctx := r.Context()
+		ctx = context.WithValue(ctx, SessionIDContextKey, session.ID)
+		ctx = context.WithValue(ctx, CSRFContextKey, session.Data.CSRF)
+		ctx = context.WithValue(ctx, OAuth2StateContextKey, session.Data.OAuth2State)
+		ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage)
+		ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
+		next.ServeHTTP(w, r.WithContext(ctx))
+	})
 }
 
-func (s *SessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession {
-	sessionCookie, err := r.Cookie("sessionID")
+func (t *SessionMiddleware) getSessionValueFromCookie(r *http.Request) *model.Session {
+	sessionCookie, err := r.Cookie(cookie.CookieSessionID)
 	if err == http.ErrNoCookie {
 		return nil
 	}
 
-	session, err := s.store.UserSessionByToken(sessionCookie.Value)
+	session, err := t.store.Session(sessionCookie.Value)
 	if err != nil {
-		logger.Error("[SessionMiddleware] %v", err)
+		logger.Error("[Middleware:Session] %v", err)
 		return nil
 	}
 
@@ -71,6 +77,6 @@ func (s *SessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSes
 }
 
 // NewSessionMiddleware returns a new SessionMiddleware.
-func NewSessionMiddleware(s *storage.Storage, r *mux.Router) *SessionMiddleware {
-	return &SessionMiddleware{store: s, router: r}
+func NewSessionMiddleware(s *storage.Storage) *SessionMiddleware {
+	return &SessionMiddleware{store: s}
 }
diff --git a/server/middleware/token.go b/server/middleware/token.go
deleted file mode 100644
index e1666f7fe43a90015e1be9bf414e3d07b255208f..0000000000000000000000000000000000000000
--- a/server/middleware/token.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
-	"context"
-	"net/http"
-
-	"github.com/miniflux/miniflux/logger"
-	"github.com/miniflux/miniflux/model"
-	"github.com/miniflux/miniflux/storage"
-)
-
-// TokenMiddleware represents a token middleware.
-type TokenMiddleware struct {
-	store *storage.Storage
-}
-
-// Handler execute the middleware.
-func (t *TokenMiddleware) Handler(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		var err error
-		token := t.getTokenValueFromCookie(r)
-
-		if token == nil {
-			logger.Debug("[Middleware:Token] Token not found")
-			token, err = t.store.CreateToken()
-			if err != nil {
-				logger.Error("[Middleware:Token] %v", err)
-				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
-				return
-			}
-
-			cookie := &http.Cookie{
-				Name:     "tokenID",
-				Value:    token.ID,
-				Path:     "/",
-				Secure:   r.URL.Scheme == "https",
-				HttpOnly: true,
-			}
-
-			http.SetCookie(w, cookie)
-		} else {
-			logger.Info("[Middleware:Token] %s", token)
-		}
-
-		isTokenValid := token.Value == r.FormValue("csrf") || token.Value == r.Header.Get("X-Csrf-Token")
-
-		if r.Method == "POST" && !isTokenValid {
-			logger.Error("[Middleware:CSRF] Invalid or missing CSRF token!")
-			w.WriteHeader(http.StatusBadRequest)
-			w.Write([]byte("Invalid or missing CSRF token!"))
-		} else {
-			ctx := r.Context()
-			ctx = context.WithValue(ctx, TokenContextKey, token.Value)
-			next.ServeHTTP(w, r.WithContext(ctx))
-		}
-	})
-}
-
-func (t *TokenMiddleware) getTokenValueFromCookie(r *http.Request) *model.Token {
-	tokenCookie, err := r.Cookie("tokenID")
-	if err == http.ErrNoCookie {
-		return nil
-	}
-
-	token, err := t.store.Token(tokenCookie.Value)
-	if err != nil {
-		logger.Error("[Middleware:Token] %v", err)
-		return nil
-	}
-
-	return token
-}
-
-// NewTokenMiddleware returns a new TokenMiddleware.
-func NewTokenMiddleware(s *storage.Storage) *TokenMiddleware {
-	return &TokenMiddleware{store: s}
-}
diff --git a/server/middleware/user_session.go b/server/middleware/user_session.go
new file mode 100644
index 0000000000000000000000000000000000000000..e9bad82dc7196873dfb34345d195e879355467eb
--- /dev/null
+++ b/server/middleware/user_session.go
@@ -0,0 +1,78 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package middleware
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/miniflux/miniflux/logger"
+	"github.com/miniflux/miniflux/model"
+	"github.com/miniflux/miniflux/server/cookie"
+	"github.com/miniflux/miniflux/server/route"
+	"github.com/miniflux/miniflux/storage"
+
+	"github.com/gorilla/mux"
+)
+
+// UserSessionMiddleware represents a user session middleware.
+type UserSessionMiddleware struct {
+	store  *storage.Storage
+	router *mux.Router
+}
+
+// Handler execute the middleware.
+func (s *UserSessionMiddleware) Handler(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session := s.getSessionFromCookie(r)
+
+		if session == nil {
+			logger.Debug("[Middleware:UserSession] Session not found")
+			if s.isPublicRoute(r) {
+				next.ServeHTTP(w, r)
+			} else {
+				http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound)
+			}
+		} else {
+			logger.Debug("[Middleware:UserSession] %s", session)
+			ctx := r.Context()
+			ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
+			ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
+			ctx = context.WithValue(ctx, UserSessionTokenContextKey, session.Token)
+
+			next.ServeHTTP(w, r.WithContext(ctx))
+		}
+	})
+}
+
+func (s *UserSessionMiddleware) isPublicRoute(r *http.Request) bool {
+	route := mux.CurrentRoute(r)
+	switch route.GetName() {
+	case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback", "appIcon", "favicon":
+		return true
+	default:
+		return false
+	}
+}
+
+func (s *UserSessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession {
+	sessionCookie, err := r.Cookie(cookie.CookieUserSessionID)
+	if err == http.ErrNoCookie {
+		return nil
+	}
+
+	session, err := s.store.UserSessionByToken(sessionCookie.Value)
+	if err != nil {
+		logger.Error("[Middleware:UserSession] %v", err)
+		return nil
+	}
+
+	return session
+}
+
+// NewUserSessionMiddleware returns a new UserSessionMiddleware.
+func NewUserSessionMiddleware(s *storage.Storage, r *mux.Router) *UserSessionMiddleware {
+	return &UserSessionMiddleware{store: s, router: r}
+}
diff --git a/server/routes.go b/server/routes.go
index d564925b7d0ea17490faf18b75f9a02218c39653..8aa849efd8805bfe120aa8dc68623d5a6e8e560e 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -42,8 +42,8 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
 	))
 
 	uiHandler := core.NewHandler(store, router, templateEngine, translator, middleware.NewChain(
-		middleware.NewSessionMiddleware(store, router).Handler,
-		middleware.NewTokenMiddleware(store).Handler,
+		middleware.NewUserSessionMiddleware(store, router).Handler,
+		middleware.NewSessionMiddleware(store).Handler,
 	))
 
 	router.Handle("/fever/", feverHandler.Use(feverController.Handler))
diff --git a/server/template/common.go b/server/template/common.go
index 555db1684374eebf88d6084b2b7b2b2d69e32a2f..6e7b8b9a6486deee4f360cdb25e03e95247695f0 100644
--- a/server/template/common.go
+++ b/server/template/common.go
@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 21:24:38.377969493 -0800 PST m=+0.007061903
+// 2017-12-16 17:48:32.321995978 -0800 PST m=+0.055632657
 
 package template
 
@@ -88,6 +88,12 @@ var templateCommonMap = map[string]string{
         </nav>
     </header>
     {{ end }}
+    {{ if .flashMessage }}
+        <div class="flash-message alert alert-success">{{ .flashMessage }}</div>
+    {{ end }}
+    {{ if .flashErrorMessage }}
+        <div class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
+    {{ end }}
     <main>
         {{template "content" .}}
     </main>
@@ -118,6 +124,6 @@ var templateCommonMap = map[string]string{
 
 var templateCommonMapChecksums = map[string]string{
 	"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
-	"layout":           "100d1ffff506b9cdd4c28233ff883c323452ea01fa224ff891d4ad69997b62b1",
+	"layout":           "ff5e3d87a48e4d3aeceda4aabe6c2c2f607006c6b6e83dfcab6c5eb255a1e6f2",
 	"pagination":       "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
 }
diff --git a/server/template/html/common/layout.html b/server/template/html/common/layout.html
index 1675c6c95f16a742acd138da99574827a82dfe06..bd7836d8d36be5c6c8901bb68c6a6340b5515b4d 100644
--- a/server/template/html/common/layout.html
+++ b/server/template/html/common/layout.html
@@ -63,6 +63,12 @@
         </nav>
     </header>
     {{ end }}
+    {{ if .flashMessage }}
+        <div class="flash-message alert alert-success">{{ .flashMessage }}</div>
+    {{ end }}
+    {{ if .flashErrorMessage }}
+        <div class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
+    {{ end }}
     <main>
         {{template "content" .}}
     </main>
diff --git a/server/ui/controller/controller.go b/server/ui/controller/controller.go
index cfecf42d6122fa42548f7e2b32743a2389e45191..8f1f4154a83e51044dfa05b10f813e1b58dd5017 100644
--- a/server/ui/controller/controller.go
+++ b/server/ui/controller/controller.go
@@ -44,10 +44,11 @@ func (c *Controller) getCommonTemplateArgs(ctx *core.Context) (tplParams, error)
 	}
 
 	params := tplParams{
-		"menu":        "",
-		"user":        user,
-		"countUnread": countUnread,
-		"csrf":        ctx.CsrfToken(),
+		"menu":         "",
+		"user":         user,
+		"countUnread":  countUnread,
+		"csrf":         ctx.CSRF(),
+		"flashMessage": ctx.FlashMessage(),
 	}
 	return params, nil
 }
diff --git a/server/ui/controller/login.go b/server/ui/controller/login.go
index d130f4c8d9f12e42824c7bf05ec4bea00b043520..87b8a4edd9a3202ef5ba84c66459c6cf7f551759 100644
--- a/server/ui/controller/login.go
+++ b/server/ui/controller/login.go
@@ -5,10 +5,8 @@
 package controller
 
 import (
-	"net/http"
-	"time"
-
 	"github.com/miniflux/miniflux/logger"
+	"github.com/miniflux/miniflux/server/cookie"
 	"github.com/miniflux/miniflux/server/core"
 	"github.com/miniflux/miniflux/server/ui/form"
 
@@ -23,7 +21,7 @@ func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, res
 	}
 
 	response.HTML().Render("login", tplParams{
-		"csrf": ctx.CsrfToken(),
+		"csrf": ctx.CSRF(),
 	})
 }
 
@@ -32,7 +30,7 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
 	authForm := form.NewAuthForm(request.Request())
 	tplParams := tplParams{
 		"errorMessage": "Invalid username or password.",
-		"csrf":         ctx.CsrfToken(),
+		"csrf":         ctx.CSRF(),
 	}
 
 	if err := authForm.Validate(); err != nil {
@@ -60,15 +58,7 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
 
 	logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
 
-	cookie := &http.Cookie{
-		Name:     "sessionID",
-		Value:    sessionToken,
-		Path:     "/",
-		Secure:   request.IsHTTPS(),
-		HttpOnly: true,
-	}
-
-	response.SetCookie(cookie)
+	response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, request.IsHTTPS()))
 	response.Redirect(ctx.Route("unread"))
 }
 
@@ -76,21 +66,10 @@ func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, respon
 func (c *Controller) Logout(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
 
-	sessionCookie := request.Cookie("sessionID")
-	if err := c.store.RemoveUserSessionByToken(user.ID, sessionCookie); err != nil {
+	if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
 		logger.Error("[Controller:Logout] %v", err)
 	}
 
-	cookie := &http.Cookie{
-		Name:     "sessionID",
-		Value:    "",
-		Path:     "/",
-		Secure:   request.IsHTTPS(),
-		HttpOnly: true,
-		MaxAge:   -1,
-		Expires:  time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
-	}
-
-	response.SetCookie(cookie)
+	response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, request.IsHTTPS()))
 	response.Redirect(ctx.Route("login"))
 }
diff --git a/server/ui/controller/oauth2.go b/server/ui/controller/oauth2.go
index 56ed53c5d8016aa41bb6d0e402094345f979cb7c..5011b1a69c74695ccf05d1e73720a430616c3017 100644
--- a/server/ui/controller/oauth2.go
+++ b/server/ui/controller/oauth2.go
@@ -5,11 +5,10 @@
 package controller
 
 import (
-	"net/http"
-
 	"github.com/miniflux/miniflux/config"
 	"github.com/miniflux/miniflux/logger"
 	"github.com/miniflux/miniflux/model"
+	"github.com/miniflux/miniflux/server/cookie"
 	"github.com/miniflux/miniflux/server/core"
 	"github.com/miniflux/miniflux/server/oauth2"
 	"github.com/tomasen/realip"
@@ -19,7 +18,7 @@ import (
 func (c *Controller) OAuth2Redirect(ctx *core.Context, request *core.Request, response *core.Response) {
 	provider := request.StringParam("provider", "")
 	if provider == "" {
-		logger.Error("[OAuth2] Invalid or missing provider")
+		logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
 		response.Redirect(ctx.Route("login"))
 		return
 	}
@@ -31,7 +30,7 @@ func (c *Controller) OAuth2Redirect(ctx *core.Context, request *core.Request, re
 		return
 	}
 
-	response.Redirect(authProvider.GetRedirectURL(ctx.CsrfToken()))
+	response.Redirect(authProvider.GetRedirectURL(ctx.GenerateOAuth2State()))
 }
 
 // OAuth2Callback receives the authorization code and create a new session.
@@ -51,8 +50,8 @@ func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, re
 	}
 
 	state := request.QueryStringParam("state", "")
-	if state != ctx.CsrfToken() {
-		logger.Error("[OAuth2] Invalid state value")
+	if state == "" || state != ctx.OAuth2State() {
+		logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
 		response.Redirect(ctx.Route("login"))
 		return
 	}
@@ -78,6 +77,7 @@ func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, re
 			return
 		}
 
+		ctx.SetFlashMessage(ctx.Translate("Your external account is now linked !"))
 		response.Redirect(ctx.Route("settings"))
 		return
 	}
@@ -118,15 +118,7 @@ func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, re
 
 	logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
 
-	cookie := &http.Cookie{
-		Name:     "sessionID",
-		Value:    sessionToken,
-		Path:     "/",
-		Secure:   request.IsHTTPS(),
-		HttpOnly: true,
-	}
-
-	response.SetCookie(cookie)
+	response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, request.IsHTTPS()))
 	response.Redirect(ctx.Route("unread"))
 }
 
diff --git a/server/ui/controller/session.go b/server/ui/controller/session.go
index a020b165427df23bc608bcd267cdbf6764d75c6b..05cb29edb66f90e612c03f08f845f6eafa63b512 100644
--- a/server/ui/controller/session.go
+++ b/server/ui/controller/session.go
@@ -9,7 +9,7 @@ import (
 	"github.com/miniflux/miniflux/server/core"
 )
 
-// ShowSessions shows the list of active sessions.
+// ShowSessions shows the list of active user sessions.
 func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
 	args, err := c.getCommonTemplateArgs(ctx)
@@ -24,15 +24,14 @@ func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, resp
 		return
 	}
 
-	sessionCookie := request.Cookie("sessionID")
 	response.HTML().Render("sessions", args.Merge(tplParams{
 		"sessions":            sessions,
-		"currentSessionToken": sessionCookie,
+		"currentSessionToken": ctx.UserSessionToken(),
 		"menu":                "settings",
 	}))
 }
 
-// RemoveSession remove a session.
+// RemoveSession remove a user session.
 func (c *Controller) RemoveSession(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
 
diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go
index af7558a2d86a1abcd1efb7d14c4cb99872816680..feba8936c5bb835fcf5580ef5ad86652d1df7afa 100644
--- a/server/ui/controller/settings.go
+++ b/server/ui/controller/settings.go
@@ -62,6 +62,7 @@ func (c *Controller) UpdateSettings(ctx *core.Context, request *core.Request, re
 		return
 	}
 
+	ctx.SetFlashMessage(ctx.Translate("Preferences saved!"))
 	response.Redirect(ctx.Route("settings"))
 }
 
diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go
index 87faafc1c41268b17461b7237fdfd8bac60ea158..8cf8a38d4c1e896f6a286033462bf25317955808 100644
--- a/server/ui/controller/unread.go
+++ b/server/ui/controller/unread.go
@@ -44,6 +44,6 @@ func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, re
 		"entries":     entries,
 		"pagination":  c.getPagination(ctx.Route("unread"), countUnread, offset),
 		"menu":        "unread",
-		"csrf":        ctx.CsrfToken(),
+		"csrf":        ctx.CSRF(),
 	})
 }
diff --git a/sql/schema_version_10.sql b/sql/schema_version_10.sql
new file mode 100644
index 0000000000000000000000000000000000000000..abd26d878a2e0061412eededba3a3dfcc5d7f89c
--- /dev/null
+++ b/sql/schema_version_10.sql
@@ -0,0 +1,8 @@
+drop table tokens;
+
+create table sessions (
+    id text not null,
+    data jsonb not null,
+    created_at timestamp with time zone not null default now(),
+    primary key(id)
+);
\ No newline at end of file
diff --git a/sql/sql.go b/sql/sql.go
index a7974e21e8e245a82c09beac78872c2c892a79cd..b0262438a491cb33270c894bacad8560026a5ad8 100644
--- a/sql/sql.go
+++ b/sql/sql.go
@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-16 12:08:03.005451004 -0800 PST m=+0.002264796
+// 2017-12-16 17:48:32.268871258 -0800 PST m=+0.002507937
 
 package sql
 
@@ -108,6 +108,14 @@ create table feed_icons (
     foreign key (icon_id) references icons(id) on delete cascade
 );
 `,
+	"schema_version_10": `drop table tokens;
+
+create table sessions (
+    id text not null,
+    data jsonb not null,
+    created_at timestamp with time zone not null default now(),
+    primary key(id)
+);`,
 	"schema_version_2": `create extension if not exists hstore;
 alter table users add column extra hstore;
 create index users_extra_idx on users using gin(extra);
@@ -147,13 +155,14 @@ alter table users add column entry_direction entry_sorting_direction default 'as
 }
 
 var SqlMapChecksums = map[string]string{
-	"schema_version_1": "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
-	"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
-	"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
-	"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
-	"schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
-	"schema_version_6": "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",
-	"schema_version_7": "33f298c9aa30d6de3ca28e1270df51c2884d7596f1283a75716e2aeb634cd05c",
-	"schema_version_8": "9922073fc4032d8922617ec6a6a07ae8d4817846c138760fb96cb5608ab83bfc",
-	"schema_version_9": "de5ba954752fe808a993feef5bf0c6f808e0a4ced5379de8bec8342678150892",
+	"schema_version_1":  "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
+	"schema_version_10": "8faf15ddeff7c8cc305e66218face11ed92b97df2bdc2d0d7944d61441656795",
+	"schema_version_2":  "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
+	"schema_version_3":  "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
+	"schema_version_4":  "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
+	"schema_version_5":  "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
+	"schema_version_6":  "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",
+	"schema_version_7":  "33f298c9aa30d6de3ca28e1270df51c2884d7596f1283a75716e2aeb634cd05c",
+	"schema_version_8":  "9922073fc4032d8922617ec6a6a07ae8d4817846c138760fb96cb5608ab83bfc",
+	"schema_version_9":  "de5ba954752fe808a993feef5bf0c6f808e0a4ced5379de8bec8342678150892",
 }
diff --git a/storage/migration.go b/storage/migration.go
index 368c567c2b915285223ee535a622b0e7c7d7e5a6..d29c76db9193394a5d697e9b7a7b099f7d211823 100644
--- a/storage/migration.go
+++ b/storage/migration.go
@@ -12,7 +12,7 @@ import (
 	"github.com/miniflux/miniflux/sql"
 )
 
-const schemaVersion = 9
+const schemaVersion = 10
 
 // Migrate run database migrations.
 func (s *Storage) Migrate() {
diff --git a/storage/session.go b/storage/session.go
new file mode 100644
index 0000000000000000000000000000000000000000..17a63e2bd7d21622127cd5041d18c9c0edc702e3
--- /dev/null
+++ b/storage/session.go
@@ -0,0 +1,77 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package storage
+
+import (
+	"database/sql"
+	"fmt"
+
+	"github.com/miniflux/miniflux/helper"
+	"github.com/miniflux/miniflux/model"
+)
+
+// CreateSession creates a new session.
+func (s *Storage) CreateSession() (*model.Session, error) {
+	session := model.Session{
+		ID:   helper.GenerateRandomString(32),
+		Data: &model.SessionData{CSRF: helper.GenerateRandomString(64)},
+	}
+
+	query := "INSERT INTO sessions (id, data) VALUES ($1, $2)"
+	_, err := s.db.Exec(query, session.ID, session.Data)
+	if err != nil {
+		return nil, fmt.Errorf("unable to create session: %v", err)
+	}
+
+	return &session, nil
+}
+
+// UpdateSessionField updates only one session field.
+func (s *Storage) UpdateSessionField(sessionID, field string, value interface{}) error {
+	query := `UPDATE sessions
+		SET data = jsonb_set(data, '{%s}', to_jsonb($1::text), true)
+		WHERE id=$2`
+
+	_, err := s.db.Exec(fmt.Sprintf(query, field), value, sessionID)
+	if err != nil {
+		return fmt.Errorf("unable to update session field: %v", err)
+	}
+
+	return nil
+}
+
+// Session returns the given session.
+func (s *Storage) Session(id string) (*model.Session, error) {
+	var session model.Session
+
+	query := "SELECT id, data FROM sessions WHERE id=$1"
+	err := s.db.QueryRow(query, id).Scan(
+		&session.ID,
+		&session.Data,
+	)
+
+	if err == sql.ErrNoRows {
+		return nil, fmt.Errorf("session not found: %s", id)
+	} else if err != nil {
+		return nil, fmt.Errorf("unable to fetch session: %v", err)
+	}
+
+	return &session, nil
+}
+
+// FlushAllSessions removes all sessions from the database.
+func (s *Storage) FlushAllSessions() (err error) {
+	_, err = s.db.Exec(`DELETE FROM user_sessions`)
+	if err != nil {
+		return err
+	}
+
+	_, err = s.db.Exec(`DELETE FROM sessions`)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/storage/token.go b/storage/token.go
deleted file mode 100644
index c5a614ab22c7920cf6e2b4f556b0083e9e71c157..0000000000000000000000000000000000000000
--- a/storage/token.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package storage
-
-import (
-	"database/sql"
-	"fmt"
-
-	"github.com/miniflux/miniflux/helper"
-	"github.com/miniflux/miniflux/model"
-)
-
-// CreateToken creates a new token.
-func (s *Storage) CreateToken() (*model.Token, error) {
-	token := model.Token{
-		ID:    helper.GenerateRandomString(32),
-		Value: helper.GenerateRandomString(64),
-	}
-
-	query := "INSERT INTO tokens (id, value) VALUES ($1, $2)"
-	_, err := s.db.Exec(query, token.ID, token.Value)
-	if err != nil {
-		return nil, fmt.Errorf("unable to create token: %v", err)
-	}
-
-	return &token, nil
-}
-
-// Token returns a Token.
-func (s *Storage) Token(id string) (*model.Token, error) {
-	var token model.Token
-
-	query := "SELECT id, value FROM tokens WHERE id=$1"
-	err := s.db.QueryRow(query, id).Scan(
-		&token.ID,
-		&token.Value,
-	)
-
-	if err == sql.ErrNoRows {
-		return nil, fmt.Errorf("token not found: %s", id)
-	} else if err != nil {
-		return nil, fmt.Errorf("unable to fetch token: %v", err)
-	}
-
-	return &token, nil
-}
diff --git a/storage/user_session.go b/storage/user_session.go
index da9cebad795da893e24d2ed5e3fa7e6ad9929373..ffb82fcd8c5b03f2a9f83c6cd261c6e3a36b5d7a 100644
--- a/storage/user_session.go
+++ b/storage/user_session.go
@@ -127,9 +127,3 @@ func (s *Storage) RemoveUserSessionByID(userID, sessionID int64) error {
 
 	return nil
 }
-
-// FlushAllSessions removes all user sessions from the database.
-func (s *Storage) FlushAllSessions() (err error) {
-	_, err = s.db.Exec(`DELETE FROM user_sessions`)
-	return
-}