Commit 1418640d authored by ale's avatar ale

Switch to a simpler session implementation

Drop gorilla/sessions in favor of using gorilla/securecookie
directly (we use a single cookie anyway). Since securecookie already
has its own expiration timestamp, we can drop some stuff from httputil
as well.
parent 655b0a9f
Pipeline #5379 passed with stages
in 3 minutes and 3 seconds
package httputil
import (
"encoding/gob"
"net/http"
"testing"
"time"
"github.com/gorilla/sessions"
)
type mySession struct {
Data string
}
func init() {
gob.Register(&mySession{})
}
func TestExpiringSession(t *testing.T) {
store := sessions.NewCookieStore()
req, _ := http.NewRequest("GET", "http://localhost/", nil)
httpsess, err := GetExpiringSession(req, store, "testkey", 60*time.Second)
if err != nil {
t.Errorf("store.Get error: %v", err)
}
if _, ok := httpsess.Values["mykey"].(*mySession); ok {
t.Fatal("got a session without any data")
}
}
......@@ -24,8 +24,8 @@ import (
authclient "git.autistici.org/id/auth/client"
ksclient "git.autistici.org/id/keystore/client"
"git.autistici.org/id/go-sso/httputil"
"git.autistici.org/id/go-sso/server/device"
"git.autistici.org/id/go-sso/server/httputil"
"git.autistici.org/id/go-sso/server/login"
)
......
......@@ -2,7 +2,6 @@ package login
import (
"context"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
......@@ -13,16 +12,13 @@ import (
"git.autistici.org/id/auth"
authclient "git.autistici.org/id/auth/client"
"github.com/gorilla/sessions"
"github.com/tstranex/u2f"
"go.opencensus.io/trace"
"git.autistici.org/id/go-sso/httputil"
"git.autistici.org/id/go-sso/server/device"
"git.autistici.org/id/go-sso/server/httputil"
)
const loginSessionKey = "_auth"
const maxFailures = 5
type Auth struct {
......@@ -56,6 +52,9 @@ type loginSession struct {
AuthResponse *auth.Response
Redir string
Failures int
// Implementation detail of the session layer.
deleted bool
}
func (l *loginSession) Reset() {
......@@ -90,29 +89,8 @@ func (l *loginSession) Can2FA(method auth.TFAMethod) error {
return nil
}
func init() {
gob.Register(&loginSession{})
}
type loginSessionInt struct {
*loginSession
httpSession *httputil.ExpiringSession
}
func (s *loginSessionInt) Delete(req *http.Request, w http.ResponseWriter) {
delete(s.httpSession.Values, "data")
s.httpSession.Options.MaxAge = -1
}
func newLoginSession(hs *httputil.ExpiringSession, s *loginSession) *loginSessionInt {
if s == nil {
s = new(loginSession)
hs.Values["data"] = s
}
return &loginSessionInt{
loginSession: s,
httpSession: hs,
}
func (l *loginSession) Delete() {
l.deleted = true
}
type ctxKey int
......@@ -138,7 +116,7 @@ type LoginCallback func(context.Context, string, string, *auth.UserInfo) error
// Login wraps an http.Handler with a login workflow.
type Login struct {
wrap http.Handler
sessionStore sessions.Store
sessionMgr *sessionManager
sessionTTL time.Duration
urlPrefix string
renderer *httputil.Renderer
......@@ -152,21 +130,14 @@ type Login struct {
// New returns a new Login wrapper.
func New(wrap http.Handler, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix, fallbackRedirect string, renderer *httputil.Renderer, callback LoginCallback, keyPairs [][]byte, sessionTTL time.Duration) *Login {
store := sessions.NewCookieStore(keyPairs...)
store.Options = &sessions.Options{
HttpOnly: true,
Secure: true,
MaxAge: 0,
Path: urlPrefix + "/",
}
if sessionTTL == 0 {
sessionTTL = 20 * time.Hour // default TTL.
}
smgr := newSessionManager(urlPrefix+"/", keyPairs[0], keyPairs[1], sessionTTL)
return &Login{
wrap: wrap,
sessionStore: store,
sessionMgr: smgr,
sessionTTL: sessionTTL,
urlPrefix: urlPrefix,
renderer: renderer,
......@@ -183,58 +154,51 @@ func (l *Login) urlFor(path string) string {
return l.urlPrefix + path
}
func (l *Login) fetchOrInitSession(req *http.Request) *loginSessionInt {
httpSession, err := httputil.GetExpiringSession(req, l.sessionStore, loginSessionKey, l.sessionTTL)
func (l *Login) fetchOrInitSession(req *http.Request) *loginSession {
session, err := l.sessionMgr.getSession(req)
if err != nil {
log.Printf("sessionStore.Get error: %v", err)
}
var session *loginSessionInt
if inner, ok := httpSession.Values["data"].(*loginSession); ok {
session = newLoginSession(httpSession, inner)
} else {
// Initialize a new session.
session = newLoginSession(httpSession, nil)
return new(loginSession)
}
return session
}
func (l *Login) ServeHTTP(w http.ResponseWriter, req *http.Request) {
sess := l.fetchOrInitSession(req)
session := l.fetchOrInitSession(req)
// This way we don't have to call sess.Save explicitly.
w = httputil.NewSessionResponseWriter(w, req)
// Ensure that the session is saved.
w = l.sessionMgr.newSessionResponseWriter(w, req, session)
// A very simple router.
switch req.URL.Path {
case l.urlFor("/login"):
l.handleLogin(w, req, sess)
l.handleLogin(w, req, session)
case l.urlFor("/login/u2f"):
l.handleLoginU2F(w, req, sess)
l.handleLoginU2F(w, req, session)
case l.urlFor("/login/otp"):
l.handleLoginOTP(w, req, sess)
l.handleLoginOTP(w, req, session)
default:
// Wipe the session on logout, before passing through to the
// wrapped handler. Note that the Auth object will still
// contain valid data, but Authenticated will be set to false.
if req.URL.Path == l.urlFor("/logout") {
log.Printf("logging out user %s", sess.Username)
sess.Authenticated = false
sess.Delete(req, w)
} else if !sess.Authenticated {
log.Printf("logging out user %s", session.Username)
session.Authenticated = false
session.Delete()
} else if !session.Authenticated {
// Save the current URL in the session for later redirect.
sess.Redir = req.URL.String()
session.Redir = req.URL.String()
http.Redirect(w, req, "/login", http.StatusFound)
return
}
// Pass the AuthContext to the wrapped Handler via the
// request context.
req = req.WithContext(withAuth(req.Context(), &sess.Auth))
req = req.WithContext(withAuth(req.Context(), &session.Auth))
l.wrap.ServeHTTP(w, req)
}
}
func (l *Login) loginOk(w http.ResponseWriter, req *http.Request, sess *loginSessionInt, password string) {
func (l *Login) loginOk(w http.ResponseWriter, req *http.Request, sess *loginSession, password string) {
if l.callback != nil {
if err := l.callback(req.Context(), sess.Username, password, sess.UserInfo); err != nil {
log.Printf("login callback error: %v", err)
......@@ -258,7 +222,7 @@ func (l *Login) loginOk(w http.ResponseWriter, req *http.Request, sess *loginSes
http.Redirect(w, req, target, http.StatusFound)
}
func (l *Login) handleLogin(w http.ResponseWriter, req *http.Request, sess *loginSessionInt) {
func (l *Login) handleLogin(w http.ResponseWriter, req *http.Request, sess *loginSession) {
if req.Method != "GET" && req.Method != "POST" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
......@@ -311,7 +275,7 @@ func (l *Login) handleLogin(w http.ResponseWriter, req *http.Request, sess *logi
l.renderer.Render(w, req, "login_password.html", env)
}
func (l *Login) handleLoginOTP(w http.ResponseWriter, req *http.Request, sess *loginSessionInt) {
func (l *Login) handleLoginOTP(w http.ResponseWriter, req *http.Request, sess *loginSession) {
if req.Method != "GET" && req.Method != "POST" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
......@@ -353,7 +317,7 @@ func (l *Login) handleLoginOTP(w http.ResponseWriter, req *http.Request, sess *l
l.renderer.Render(w, req, "login_otp.html", env)
}
func (l *Login) handleLoginU2F(w http.ResponseWriter, req *http.Request, sess *loginSessionInt) {
func (l *Login) handleLoginU2F(w http.ResponseWriter, req *http.Request, sess *loginSession) {
if req.Method != "GET" && req.Method != "POST" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
......
package httputil
package login
import (
"encoding/gob"
......@@ -6,51 +6,64 @@ import (
"net/http"
"time"
"github.com/gorilla/sessions"
"github.com/gorilla/securecookie"
)
// ExpiringSession is a session with server-side expiration check.
// Session data is saved in signed, encrypted cookies in the
// browser. We'd like these cookies to expire when a certain amount of
// time passes, or when the user closes the browser. We trust the
// browser for the latter, but we enforce time-based expiration on the
// server.
type ExpiringSession struct {
*sessions.Session
func init() {
gob.Register(&loginSession{})
}
// GetExpiringSession wraps a Session (obtained from 'store') with
// an ExpiringSession. If it's invalid or expired, a new empty Session
// will be created with an expiration time set using 'ttl'.
func GetExpiringSession(req *http.Request, store sessions.Store, key string, ttl time.Duration) (*ExpiringSession, error) {
now := time.Now()
const sessionCookieName = "_sso_auth"
// An error here just means that we failed to decode the
// existing session for some reason. A new session will always
// be returned, so we just pass along the error to the caller
// (so it can be logged).
s, err := store.Get(req, key)
type sessionManager struct {
sc *securecookie.SecureCookie
path string
}
// See if we have a valid session first.
if !s.IsNew {
if exp, ok := s.Values["_exp"].(time.Time); ok && now.Before(exp) {
return &ExpiringSession{Session: s}, err
}
// We can't call sessions.NewSession() because that
// won't register the session with the Registry, so it
// won't be sent with the response. Wipe the data
// instead.
for k := range s.Values {
delete(s.Values, k)
}
func newSessionManager(path string, authKey, encKey []byte, ttl time.Duration) *sessionManager {
sc := securecookie.New(authKey, encKey)
sc.MaxAge(int(ttl.Seconds()))
// The JSON encoder generates smaller data than gob in our case.
sc.SetSerializer(&securecookie.JSONEncoder{})
return &sessionManager{
sc: sc,
path: path,
}
}
// The session is either invalid or expired, create a new
// blank one containing no data.
expiry := now.Add(ttl)
s.Values["_exp"] = expiry
func (m *sessionManager) getSession(req *http.Request) (*loginSession, error) {
cookie, err := req.Cookie(sessionCookieName)
if err != nil {
return nil, err
}
var s loginSession
err = m.sc.Decode(sessionCookieName, cookie.Value, &s)
if err != nil {
return nil, err
}
return &s, nil
}
return &ExpiringSession{Session: s}, err
func (m *sessionManager) setSession(w http.ResponseWriter, session *loginSession) (err error) {
var encoded string
if !session.deleted {
encoded, err = m.sc.Encode(sessionCookieName, session)
if err != nil {
return
}
}
cookie := &http.Cookie{
Name: sessionCookieName,
Value: encoded,
Path: m.path,
Secure: true,
HttpOnly: true,
}
if session.deleted {
cookie.MaxAge = -1
}
http.SetCookie(w, cookie)
return
}
// Wrapper for an http.ResponseWriter that ensures all tracked
......@@ -63,11 +76,13 @@ type sessionResponseWriter struct {
http.ResponseWriter
headerWritten bool
req *http.Request
mgr *sessionManager
session *loginSession
}
func (w *sessionResponseWriter) WriteHeader(statusCode int) {
if statusCode >= 200 && statusCode < 400 {
if err := sessions.Save(w.req, w.ResponseWriter); err != nil {
if err := w.mgr.setSession(w, w.session); err != nil {
log.Printf("error saving sessions: %v", err)
}
}
......@@ -82,19 +97,13 @@ func (w *sessionResponseWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
// NewSessionResponseWriter returns a wrapped http.ResponseWriter that
// will always remember to save the Gorilla sessions before writing
// the response body.
func NewSessionResponseWriter(w http.ResponseWriter, req *http.Request) http.ResponseWriter {
// newSessionResponseWriter returns a wrapped http.ResponseWriter that
// will always save the session before writing the response body.
func (m *sessionManager) newSessionResponseWriter(w http.ResponseWriter, req *http.Request, session *loginSession) http.ResponseWriter {
return &sessionResponseWriter{
ResponseWriter: w,
req: req,
mgr: m,
session: session,
}
}
func init() {
// Register time.Time with encoding/gob, to ensure that the
// ExpiringSession timestamp can be serialized.
var t time.Time
gob.Register(t)
}
# This is the official list of gorilla/sessions authors for copyright purposes.
#
# Please keep the list sorted.
Ahmadreza Zibaei <ahmadrezazibaei@hotmail.com>
Anton Lindström <lindztr@gmail.com>
Brian Jones <mojobojo@gmail.com>
Collin Stedman <kronion@users.noreply.github.com>
Deniz Eren <dee.116@gmail.com>
Dmitry Chestnykh <dmitry@codingrobots.com>
Dustin Oprea <myselfasunder@gmail.com>
Egon Elbre <egonelbre@gmail.com>
enumappstore <appstore@enumapps.com>
Geofrey Ernest <geofreyernest@live.com>
Google LLC (https://opensource.google.com/)
Jerry Saravia <SaraviaJ@gmail.com>
Jonathan Gillham <jonathan.gillham@gamil.com>
Justin Clift <justin@postgresql.org>
Justin Hellings <justin.hellings@gmail.com>
Kamil Kisiel <kamil@kamilkisiel.net>
Keiji Yoshida <yoshida.keiji.84@gmail.com>
kliron <kliron@gmail.com>
Kshitij Saraogi <KshitijSaraogi@gmail.com>
Lauris BH <lauris@nix.lv>
Lukas Rist <glaslos@gmail.com>
Mark Dain <ancarda@users.noreply.github.com>
Matt Ho <matt.ho@gmail.com>
Matt Silverlock <matt@eatsleeprepeat.net>
Mattias Wadman <mattias.wadman@gmail.com>
Michael Schuett <michaeljs1990@gmail.com>
Michael Stapelberg <stapelberg@users.noreply.github.com>
Mirco Zeiss <mirco.zeiss@gmail.com>
moraes <rodrigo.moraes@gmail.com>
nvcnvn <nguyen@open-vn.org>
pappz <zoltan.pmail@gmail.com>
Pontus Leitzler <leitzler@users.noreply.github.com>
QuaSoft <info@quasoft.net>
rcadena <robert.cadena@gmail.com>
rodrigo moraes <rodrigo.moraes@gmail.com>
Shawn Smith <shawnpsmith@gmail.com>
Taylor Hurt <taylor.a.hurt@gmail.com>
Tortuoise <sanyasinp@gmail.com>
Vitor De Mario <vitordemario@gmail.com>
Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# sessions
[![GoDoc](https://godoc.org/github.com/gorilla/sessions?status.svg)](https://godoc.org/github.com/gorilla/sessions) [![Build Status](https://travis-ci.org/gorilla/sessions.svg?branch=master)](https://travis-ci.org/gorilla/sessions)
[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/sessions/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/sessions?badge)
gorilla/sessions provides cookie and filesystem sessions and infrastructure for
custom session backends.
The key features are:
- Simple API: use it as an easy way to set signed (and optionally
encrypted) cookies.
- Built-in backends to store sessions in cookies or the filesystem.
- Flash messages: session values that last until read.
- Convenient way to switch session persistency (aka "remember me") and set
other attributes.
- Mechanism to rotate authentication and encryption keys.
- Multiple sessions per request, even using different backends.
- Interfaces and infrastructure for custom session backends: sessions from
different stores can be retrieved and batch-saved using a common API.
Let's start with an example that shows the sessions API in a nutshell:
```go
import (
"net/http"
"github.com/gorilla/sessions"
)
// Note: Don't store your key in your source code. Pass it via an
// environmental variable, or flag (or both), and don't accidentally commit it
// alongside your code. Ensure your key is sufficiently random - i.e. use Go's
// crypto/rand or securecookie.GenerateRandomKey(32) and persist the result.
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func MyHandler(w http.ResponseWriter, r *http.Request) {
// Get a session. We're ignoring the error resulted from decoding an
// existing session: Get() always returns a session, even if empty.
session, _ := store.Get(r, "session-name")
// Set some session values.
session.Values["foo"] = "bar"
session.Values[42] = 43
// Save it before we write to the response/return from the handler.
err = session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
```
First we initialize a session store calling `NewCookieStore()` and passing a
secret key used to authenticate the session. Inside the handler, we call
`store.Get()` to retrieve an existing session or create a new one. Then we set
some session values in session.Values, which is a `map[interface{}]interface{}`.
And finally we call `session.Save()` to save the session in the response.
More examples are available [on the Gorilla
website](https://www.gorillatoolkit.org/pkg/sessions).
## Store Implementations
Other implementations of the `sessions.Store` interface:
- [github.com/starJammer/gorilla-sessions-arangodb](https://github.com/starJammer/gorilla-sessions-arangodb) - ArangoDB
- [github.com/yosssi/boltstore](https://github.com/yosssi/boltstore) - Bolt
- [github.com/srinathgs/couchbasestore](https://github.com/srinathgs/couchbasestore) - Couchbase
- [github.com/denizeren/dynamostore](https://github.com/denizeren/dynamostore) - Dynamodb on AWS
- [github.com/savaki/dynastore](https://github.com/savaki/dynastore) - DynamoDB on AWS (Official AWS library)
- [github.com/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache
- [github.com/dsoprea/go-appengine-sessioncascade](https://github.com/dsoprea/go-appengine-sessioncascade) - Memcache/Datastore/Context in AppEngine
- [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
- [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
- [github.com/EnumApps/clustersqlstore](https://github.com/EnumApps/clustersqlstore) - MySQL Cluster
- [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
- [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
- [github.com/rbcervilla/redisstore](https://github.com/rbcervilla/redisstore) - Redis (Single, Sentinel, Cluster)
- [github.com/boj/rethinkstore](https://github.com/boj/rethinkstore) - RethinkDB
- [github.com/boj/riakstore](https://github.com/boj/riakstore) - Riak
- [github.com/michaeljs1990/sqlitestore](https://github.com/michaeljs1990/sqlitestore) - SQLite
- [github.com/wader/gormstore](https://github.com/wader/gormstore) - GORM (MySQL, PostgreSQL, SQLite)
- [github.com/gernest/qlstore](https://github.com/gernest/qlstore) - ql
- [github.com/quasoft/memstore](https://github.com/quasoft/memstore) - In-memory implementation for use in unit tests
- [github.com/lafriks/xormstore](https://github.com/lafriks/xormstore) - XORM (MySQL, PostgreSQL, SQLite, Microsoft SQL Server, TiDB)
- [github.com/GoogleCloudPlatform/firestore-gorilla-sessions](https://github.com/GoogleCloudPlatform/firestore-gorilla-sessions) - Cloud Firestore
## License
BSD licensed. See the LICENSE file for details.
// +build !go1.11
package sessions
import "net/http"
// newCookieFromOptions returns an http.Cookie with the options set.
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: options.Path,
Domain: options.Domain,
MaxAge: options.MaxAge,
Secure: options.Secure,
HttpOnly: options.HttpOnly,
}
}
// +build go1.11
package sessions
import "net/http"
// newCookieFromOptions returns an http.Cookie with the options set.
func newCookieFromOptions(name, value string, options *Options) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: options.Path,
Domain: options.Domain,
MaxAge: options.MaxAge,
Secure: options.Secure,
HttpOnly: options.HttpOnly,
SameSite: options.SameSite,
}
}
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package sessions provides cookie and filesystem sessions and
infrastructure for custom session backends.
The key features are:
* Simple API: use it as an easy way to set signed (and optionally
encrypted) cookies.
* Built-in backends to store sessions in cookies or the filesystem.
* Flash messages: session values that last until read.
* Convenient way to switch session persistency (aka "remember me") and set
other attributes.
* Mechanism to rotate authentication and encryption keys.
* Multiple sessions per request, even using different backends.
* Interfaces and infrastructure for custom session backends: sessions from
different stores can be retrieved and batch-saved using a common API.
Let's start with an example that shows the sessions API in a nutshell:
import (
"net/http"
"github.com/gorilla/sessions"
)
// Note: Don't store your key in your source code. Pass it via an
// environmental variable, or flag (or both), and don't accidentally commit it
// alongside your code. Ensure your key is sufficiently random - i.e. use Go's
// crypto/rand or securecookie.GenerateRandomKey(32) and persist the result.
// Ensure SESSION_KEY exists in the environment, or sessions will fail.
var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))
func MyHandler(w http.ResponseWriter, r *http.Request) {
// Get a session. Get() always returns a session, even if empty.
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set some session values.
session.Values["foo"] = "bar"
session.Values[42] = 43
// Save it before we write to the response/return from the handler.
err = session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
First we initialize a session store calling NewCookieStore() and passing a
secret key used to authenticate the session. Inside the handler, we call
store.Get() to retrieve an existing session or a new one. Then we set some
session values in session.Values, which is a map[interface{}]interface{}.
And finally we call session.Save() to save the session in the response.
Note that in production code, we should check for errors when calling
session.Save(r, w), and either display an error message or otherwise handle it.
Save must be called before writing to the response, otherwise the session
cookie will not be sent to the client.
That's all you need to know for the basic usage. Let's take a look at other
options, starting with flash messages.
Flash messages are session values that last until read. The term appeared with
Ruby On Rails a few years back. When we request a flash message, it is removed
from the session. To add a flash, call session.AddFlash(), and to get all
flashes, call session.Flashes(). Here is an example:
func MyHandler(w http.ResponseWriter, r *http.Request) {
// Get a session.
session, err := store.Get(r, "session-name")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Get the previous flashes, if any.
if flashes := session.Flashes(); len(flashes) > 0 {
// Use the flash values.
} else {
// Set a new flash.
session.AddFlash("Hello, flash messages world!")
}
err = session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Flash messages are useful to set information to be read after a redirection,
like after form submissions.
There may also be cases where you want to store a complex datatype within a
session, such as a struct. Sessions are serialised using the encoding/gob package,
so it is easy to register new datatypes for storage in sessions:
import(
"encoding/gob"
"github.com/gorilla/sessions"
)
type Person struct {
FirstName string
LastName string
Email string
Age int
}
type M map[string]interface{}
func init() {
gob.Register(&Person{})
gob.Register(&M{})
}
As it's not possible to pass a raw type as a parameter to a function, gob.Register()
relies on us passing it a value of the desired type. In the example above we've passed
it a pointer to a struct and a pointer to a custom type representing a
map[string]interface. (We could have passed non-pointer values if we wished.) This will
then allow us to serialise/deserialise values of those types to and from our sessions.
Note that because session values are stored in a map[string]interface{}, there's
a need to type-assert data when retrieving it. We'll use the Person struct we registered above: