diff --git a/httpsso/handler.go b/httpsso/handler.go
index 0bdde27c976f9e16d35f888f476ff88a0aa0e3ec..402e77862686150ac4e22f72636a3e6853286397 100644
--- a/httpsso/handler.go
+++ b/httpsso/handler.go
@@ -20,8 +20,6 @@ import (
 )
 
 type authSession struct {
-	*httputil.ExpiringSession
-
 	Auth     bool
 	Username string
 	Groups   []string
@@ -29,7 +27,7 @@ type authSession struct {
 
 type authSessionKeyType int
 
-var authSessionKey authSessionKeyType = 42
+const authSessionKey authSessionKeyType = 0
 
 func getCurrentAuthSession(req *http.Request) *authSession {
 	s, ok := req.Context().Value(authSessionKey).(*authSession)
@@ -64,7 +62,7 @@ func Groups(req *http.Request) []string {
 	return nil
 }
 
-var authSessionLifetime = 1 * time.Hour
+var defaultAuthSessionTTL = 1 * time.Hour
 
 func init() {
 	gob.Register(&authSession{})
@@ -77,6 +75,8 @@ type SSOWrapper struct {
 	sessionEncKey  []byte
 	serverURL      string
 	serverOrigin   string
+
+	TTL time.Duration
 }
 
 // NewSSOWrapper returns a new SSOWrapper that will authenticate users
@@ -93,6 +93,7 @@ func NewSSOWrapper(serverURL string, pkey []byte, domain string, sessionAuthKey,
 		serverOrigin:   originFromURL(serverURL),
 		sessionAuthKey: sessionAuthKey,
 		sessionEncKey:  sessionEncKey,
+		TTL:            defaultAuthSessionTTL,
 	}, nil
 }
 
@@ -109,7 +110,7 @@ func (s *SSOWrapper) Wrap(h http.Handler, service string, groups []string) http.
 	}
 
 	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-		session, _ := store.Get(req, "sso")
+		session, _ := httputil.GetExpiringSession(req, store, "sso", s.TTL)
 
 		switch strings.TrimPrefix(req.URL.Path, svcPath) {
 		case "sso_login":
@@ -119,11 +120,11 @@ func (s *SSOWrapper) Wrap(h http.Handler, service string, groups []string) http.
 			s.handleLogout(w, req, session)
 
 		default:
-			if auth, ok := session.Values["a"].(*authSession); ok && auth.Valid() && auth.Auth {
+			if auth, ok := session.Values["a"].(*authSession); ok && auth.Auth {
 				req.Header.Set("X-Authenticated-User", auth.Username)
 
-				ctx := context.WithValue(req.Context(), authSessionKey, auth)
-				h.ServeHTTP(w, req.WithContext(ctx))
+				req = req.WithContext(context.WithValue(req.Context(), authSessionKey, auth))
+				h.ServeHTTP(w, req)
 				return
 			}
 
@@ -132,7 +133,7 @@ func (s *SSOWrapper) Wrap(h http.Handler, service string, groups []string) http.
 	})
 }
 
-func (s *SSOWrapper) handleLogin(w http.ResponseWriter, req *http.Request, session *sessions.Session, service string, groups []string) {
+func (s *SSOWrapper) handleLogin(w http.ResponseWriter, req *http.Request, session *httputil.ExpiringSession, service string, groups []string) {
 	t := req.FormValue("t")
 	d := req.FormValue("d")
 
@@ -154,21 +155,25 @@ func (s *SSOWrapper) handleLogin(w http.ResponseWriter, req *http.Request, sessi
 
 	// Authenticate the user.
 	session.Values["a"] = &authSession{
-		ExpiringSession: httputil.NewExpiringSession(authSessionLifetime),
-		Auth:            true,
-		Username:        tkt.User,
-		Groups:          tkt.Groups,
+		Auth:     true,
+		Username: tkt.User,
+		Groups:   tkt.Groups,
 	}
 	if err := sessions.Save(req, w); err != nil {
+		log.Printf("error saving SSO session: %v", err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 	http.Redirect(w, req, d, http.StatusFound)
 }
 
-func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request, session *sessions.Session) {
+func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request, session *httputil.ExpiringSession) {
+	// Delete the auth session.
 	session.Options.MaxAge = -1
+	delete(session.Values, "sso")
+
 	if err := sessions.Save(req, w); err != nil {
+		log.Printf("error saving SSO session: %v", err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -182,11 +187,13 @@ func (s *SSOWrapper) handleLogout(w http.ResponseWriter, req *http.Request, sess
 }
 
 // Redirect to the SSO server.
-func (s *SSOWrapper) redirectToLogin(w http.ResponseWriter, req *http.Request, session *sessions.Session, service string, groups []string) {
+func (s *SSOWrapper) redirectToLogin(w http.ResponseWriter, req *http.Request, session *httputil.ExpiringSession, service string, groups []string) {
 	// Generate a random nonce and store it in the local session.
 	nonce := makeUniqueNonce()
 	session.Values["nonce"] = nonce
+
 	if err := sessions.Save(req, w); err != nil {
+		log.Printf("error saving SSO session: %v", err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
diff --git a/httputil/headers.go b/httputil/headers.go
new file mode 100644
index 0000000000000000000000000000000000000000..8594f43298f4e86e28749d50c1b50472617e2145
--- /dev/null
+++ b/httputil/headers.go
@@ -0,0 +1,23 @@
+package httputil
+
+import "net/http"
+
+// WithDynamicHeaders wraps an http.Handler with cache-busting and
+// security-related headers appropriate for a user-facing dynamic
+// application. The 'csp' argument sets a default
+// Content-Security-Policy.
+func WithDynamicHeaders(h http.Handler, csp string) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		hdr := w.Header()
+		hdr.Set("Pragma", "no-cache")
+		hdr.Set("Cache-Control", "no-store")
+		hdr.Set("Expires", "-1")
+		hdr.Set("X-Frame-Options", "NONE")
+		hdr.Set("X-XSS-Protection", "1; mode=block")
+		hdr.Set("X-Content-Type-Options", "nosniff")
+		if csp != "" && hdr.Get("Content-Security-Policy") == "" {
+			hdr.Set("Content-Security-Policy", csp)
+		}
+		h.ServeHTTP(w, r)
+	})
+}
diff --git a/httputil/renderer.go b/httputil/renderer.go
new file mode 100644
index 0000000000000000000000000000000000000000..e572eebdc801b6eb6a33fcb856e8a42a5a003183
--- /dev/null
+++ b/httputil/renderer.go
@@ -0,0 +1,57 @@
+package httputil
+
+import (
+	"bytes"
+	"html/template"
+	"io"
+	"log"
+	"net/http"
+	"strconv"
+
+	"github.com/gorilla/csrf"
+)
+
+// A Renderer just renders HTML templates with some common context
+// variables. Context is represented as a map[string]interface{}, to
+// allow the merge operation.
+type Renderer struct {
+	tpl  *template.Template
+	vars map[string]interface{}
+}
+
+// NewRenderer creates a new Renderer with the provided templates and
+// default variables.
+func NewRenderer(tpl *template.Template, vars map[string]interface{}) *Renderer {
+	return &Renderer{
+		tpl:  tpl,
+		vars: vars,
+	}
+}
+
+// Render the named HTML template to 'w'.
+func (r *Renderer) Render(w http.ResponseWriter, req *http.Request, templateName string, data map[string]interface{}) {
+	// Merge default variables with the ones passed in 'data',
+	// without modifying either. Always populate the CRSFField
+	// variable with the current CSRF token.
+	vars := make(map[string]interface{})
+	vars["CSRFField"] = csrf.TemplateField(req)
+	for k, v := range r.vars {
+		vars[k] = v
+	}
+	for k, v := range data {
+		vars[k] = v
+	}
+
+	// Render the template into a buffer, to prevent returning
+	// half-rendered templates when there is an error.
+	var buf bytes.Buffer
+	if err := r.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
+		log.Printf("template rendering error for %s: %v", req.URL.String(), err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	// Write our response to the client.
+	w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
+	io.Copy(w, &buf) // nolint
+}
diff --git a/httputil/session.go b/httputil/session.go
index f1847ba98e563371f0188afd191550a224ac98e5..5d2872eab361e583b2da660924e4930aa975178d 100644
--- a/httputil/session.go
+++ b/httputil/session.go
@@ -2,7 +2,11 @@ package httputil
 
 import (
 	"encoding/gob"
+	"log"
+	"net/http"
 	"time"
+
+	"github.com/gorilla/sessions"
 )
 
 // ExpiringSession is a session with server-side expiration check.
@@ -12,24 +16,85 @@ import (
 // browser for the latter, but we enforce time-based expiration on the
 // server.
 type ExpiringSession struct {
-	Expiry time.Time
+	*sessions.Session
 }
 
-// NewExpiringSession returns a session that is valid for the given
-// duration.
-func NewExpiringSession(ttl time.Duration) *ExpiringSession {
-	return &ExpiringSession{
-		Expiry: time.Now().Add(ttl),
+// 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()
+
+	// 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)
+
+	// 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)
+		}
 	}
+
+	// The session is either invalid or expired, create a new
+	// blank one containing no data.
+	expiry := now.Add(ttl)
+	s.Values["_exp"] = expiry
+
+	return &ExpiringSession{Session: s}, err
 }
 
-// Valid returns true if the session has not expired yet.
-// It can be called with a nil receiver.
-func (e *ExpiringSession) Valid() bool {
-	return e != nil && time.Now().Before(e.Expiry)
+// Wrapper for an http.ResponseWriter that ensures all tracked
+// sessions are saved before the request body is sent.
+//
+// We have to duplicate the logic to call WriteHeader on the first
+// Write, otherwise the underlying ResponseWriter won't call our
+// WriteHeader function but its own instead.
+type sessionResponseWriter struct {
+	http.ResponseWriter
+	headerWritten bool
+	req           *http.Request
 }
 
-func init() {
-	gob.Register(&ExpiringSession{})
+func (w *sessionResponseWriter) WriteHeader(statusCode int) {
+	if statusCode >= 200 && statusCode < 400 {
+		if err := sessions.Save(w.req, w.ResponseWriter); err != nil {
+			log.Printf("error saving sessions: %v", err)
+		}
+	}
+	w.ResponseWriter.WriteHeader(statusCode)
+	w.headerWritten = true
 }
 
+func (w *sessionResponseWriter) Write(b []byte) (int, error) {
+	if !w.headerWritten {
+		w.WriteHeader(http.StatusOK)
+	}
+	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 {
+	return &sessionResponseWriter{
+		ResponseWriter: w,
+		req:            req,
+	}
+}
+
+func init() {
+	// Register time.Time with encoding/gob, to ensure that the
+	// ExpiringSession timestamp can be serialized.
+	var t time.Time
+	gob.Register(t)
+}
diff --git a/httputil/session_test.go b/httputil/session_test.go
index 84086f136fd6148d0907f3ac21b7ab399ebf26c8..b46817117da6bab86633f1234336161eb813eae6 100644
--- a/httputil/session_test.go
+++ b/httputil/session_test.go
@@ -1,32 +1,32 @@
 package httputil
 
 import (
-	"bytes"
 	"encoding/gob"
-	"reflect"
+	"net/http"
 	"testing"
 	"time"
+
+	"github.com/gorilla/sessions"
 )
 
+type mySession struct {
+	Data string
+}
+
+func init() {
+	gob.Register(&mySession{})
+}
+
 func TestExpiringSession(t *testing.T) {
-	type mySession struct {
-		*ExpiringSession
-		Data string
-	}
-	s := &mySession{
-		ExpiringSession: NewExpiringSession(60 * time.Second),
-		Data:            "data",
-	}
+	store := sessions.NewCookieStore()
+	req, _ := http.NewRequest("GET", "http://localhost/", nil)
 
-	var buf bytes.Buffer
-	if err := gob.NewEncoder(&buf).Encode(s); err != nil {
-		t.Fatal("encode:", err)
+	httpsess, err := GetExpiringSession(req, store, "testkey", 60*time.Second)
+	if err != nil {
+		t.Errorf("store.Get error: %v", err)
 	}
-	var s2 mySession
-	if err := gob.NewDecoder(&buf).Decode(&s2); err != nil {
-		t.Fatal("decode:", err)
-	}
-	if !reflect.DeepEqual(s.Data, s2.Data) {
-		t.Fatalf("sessions differ: %+v vs %+v", s, &s2)
+
+	if _, ok := httpsess.Values["mykey"].(*mySession); ok {
+		t.Fatal("got a session without any data")
 	}
 }
diff --git a/httputil/static.go b/httputil/static.go
new file mode 100644
index 0000000000000000000000000000000000000000..ae965744d7a3107680fa676683cdab55c96d0a5c
--- /dev/null
+++ b/httputil/static.go
@@ -0,0 +1,38 @@
+package httputil
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"time"
+)
+
+// StaticContent is an http.Handler that serves in-memory data as if
+// it were a static file.
+type StaticContent struct {
+	modtime time.Time
+	name    string
+	data    []byte
+}
+
+// LoadStaticContent creates a StaticContent by loading data from a file.
+func LoadStaticContent(path string) (*StaticContent, error) {
+	stat, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	data, err := ioutil.ReadFile(path) // #nosec
+	if err != nil {
+		return nil, err
+	}
+	return &StaticContent{
+		name:    path,
+		modtime: stat.ModTime(),
+		data:    data,
+	}, nil
+}
+
+func (c *StaticContent) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	http.ServeContent(w, req, c.name, c.modtime, bytes.NewReader(c.data))
+}
diff --git a/server/device/manager.go b/server/device/manager.go
index a9dfe231c1330997a2dad62e4910b7a0cd04d214..d6d6fa741e36a48f9601576a80459b794bde59d7 100644
--- a/server/device/manager.go
+++ b/server/device/manager.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 
 	"git.autistici.org/id/auth"
+	"github.com/gorilla/securecookie"
 	"github.com/gorilla/sessions"
 	"github.com/mssola/user_agent"
 )
@@ -35,7 +36,7 @@ type Config struct {
 }
 
 // New returns a new Manager with the given configuration.
-func New(config *Config) (*Manager, error) {
+func New(config *Config, urlPrefix string) (*Manager, error) {
 	if config == nil {
 		config = &Config{}
 	}
@@ -45,9 +46,15 @@ func New(config *Config) (*Manager, error) {
 		log.Printf("Warning: GeoIP disabled: %v", err)
 	}
 
+	// This should only happen in tests.
+	if config.AuthKey == "" {
+		log.Printf("Warning: device_manager.auth_key unset, generating temporary random secrets")
+		config.AuthKey = string(securecookie.GenerateRandomKey(64))
+	}
+
 	return &Manager{
 		geodb: geodb,
-		store: newStore([]byte(config.AuthKey)),
+		store: newStore([]byte(config.AuthKey), urlPrefix),
 	}, nil
 }
 
diff --git a/server/device/store.go b/server/device/store.go
index 9a55ce990fe0b72fc520c87afc88bc376301ef27..1a00725a0ede2be7eca7d0c1b8c7482f57744c19 100644
--- a/server/device/store.go
+++ b/server/device/store.go
@@ -4,11 +4,11 @@ import "github.com/gorilla/sessions"
 
 const aVeryLongTimeInSeconds = 10 * 365 * 86400
 
-func newStore(authKey []byte) sessions.Store {
+func newStore(authKey []byte, urlPrefix string) sessions.Store {
 	// No encryption, long-term lifetime cookie.
 	store := sessions.NewCookieStore(authKey, nil)
 	store.Options = &sessions.Options{
-		Path:     "/",
+		Path:     urlPrefix + "/",
 		HttpOnly: true,
 		Secure:   true,
 		MaxAge:   aVeryLongTimeInSeconds,
diff --git a/server/http.go b/server/http.go
index 0a7646ed10498d4e59284d474884b0fbd7bbdd6d..f4412f1a9bb762ab1b90cb4cccb1b9cd1bef9587 100644
--- a/server/http.go
+++ b/server/http.go
@@ -4,25 +4,20 @@ package server
 //go:generate go-bindata --nocompress --pkg server static/... templates/...
 
 import (
-	"bytes"
 	"context"
-	"encoding/gob"
 	"encoding/json"
 	"fmt"
 	"html/template"
 	"io"
-	"io/ioutil"
 	"log"
 	"net/http"
 	"net/url"
-	"os"
 	"strings"
 	"time"
 
 	assetfs "github.com/elazarl/go-bindata-assetfs"
 	"github.com/gorilla/csrf"
 	"github.com/gorilla/mux"
-	"github.com/gorilla/sessions"
 	"github.com/rs/cors"
 
 	"git.autistici.org/id/auth"
@@ -31,46 +26,15 @@ import (
 
 	"git.autistici.org/id/go-sso/httputil"
 	"git.autistici.org/id/go-sso/server/device"
+	"git.autistici.org/id/go-sso/server/login"
 )
 
-const authSessionKey = "_auth"
-
-type authSession struct {
-	*httputil.ExpiringSession
-
-	// User name and other information (like group membership).
-	Username string
-	UserInfo *auth.UserInfo
-
-	// Services the user has logged in to from this session.
-	Services []string
-}
-
-// AddService adds a service to the current session (if it's not
-// already there).
-func (s *authSession) AddService(service string) {
-	for _, svc := range s.Services {
-		if svc == service {
-			return
-		}
-	}
-	s.Services = append(s.Services, service)
-}
-
-// By default, make users log in again after (almost) one day.
-var defaultAuthSessionLifetime = 20 * time.Hour
-
-func newAuthSession(ttl time.Duration, username string, userinfo *auth.UserInfo) *authSession {
-	return &authSession{
-		ExpiringSession: httputil.NewExpiringSession(ttl),
-		Username:        username,
-		UserInfo:        userinfo,
-	}
-}
+// A relatively strict CSP.
+const contentSecurityPolicy = "default-src 'none'; img-src 'self' data:; script-src 'self'; style-src 'self'; connect-src 'self';"
 
-func init() {
-	gob.Register(&authSession{})
-}
+// Slightly looser CSP for the logout page: it needs to load remote
+// images.
+const logoutContentSecurityPolicy = "default-src 'none'; img-src *; script-src 'self'; style-src 'self'; connect-src *;"
 
 // Returns the URL of the login handler on the target service.
 func serviceLoginCallback(service, destination, token string) string {
@@ -88,105 +52,122 @@ func serviceLogoutCallback(service string) string {
 // Server for the SSO protocol. Provides the HTTP interface to a
 // LoginService.
 type Server struct {
-	authSessionStore    sessions.Store
-	authSessionLifetime time.Duration
-	loginHandler        *loginHandler
+	authSessionLifetime int
 	loginService        *LoginService
 	keystore            ksclient.Client
 	keystoreGroups      []string
-	csrfSecret          []byte
-	renderer            *renderer
+	renderer            *httputil.Renderer
 	urlPrefix           string
 	homepageRedirectURL string
-	allowedOrigins      []string
-
-	// User-configurable static data that we serve from memory.
-	siteLogo    *staticContent
-	siteFavicon *staticContent
-}
-
-func sl2bl(sl []string) [][]byte {
-	var out [][]byte
-	for _, s := range sl {
-		out = append(out, []byte(s))
-	}
-	return out
+	handler             http.Handler
 }
 
 // New returns a new Server.
 func New(loginService *LoginService, authClient authclient.Client, config *Config) (*Server, error) {
 	urlPrefix := strings.TrimRight(config.URLPrefix, "/")
-	sessionSecrets := sl2bl(config.SessionSecrets)
-	store := sessions.NewCookieStore(sessionSecrets...)
-	store.Options = &sessions.Options{
-		HttpOnly: true,
-		Secure:   true,
-		MaxAge:   0,
-		Path:     urlPrefix + "/",
-	}
-
-	renderer := newRenderer(config)
-	s := &Server{
-		authSessionLifetime: defaultAuthSessionLifetime,
-		authSessionStore:    store,
+	renderer := httputil.NewRenderer(
+		parseEmbeddedTemplates(),
+		map[string]interface{}{
+			"URLPrefix":          urlPrefix,
+			"AccountRecoveryURL": config.AccountRecoveryURL,
+			"SiteName":           config.SiteName,
+			"SiteLogo":           config.SiteLogo,
+			"SiteFavicon":        config.SiteFavicon,
+		},
+	)
+
+	h := &Server{
 		loginService:        loginService,
 		urlPrefix:           urlPrefix,
 		homepageRedirectURL: config.HomepageRedirectURL,
-		allowedOrigins:      config.AllowedCORSOrigins,
+		authSessionLifetime: config.AuthSessionLifetimeSeconds,
 		renderer:            renderer,
 	}
-	if config.CSRFSecret != "" {
-		s.csrfSecret = []byte(config.CSRFSecret)
+
+	if config.KeyStore != nil {
+		ks, err := ksclient.New(config.KeyStore)
+		if err != nil {
+			return nil, err
+		}
+		log.Printf("keystore client enabled")
+		h.keystore = ks
+		h.keystoreGroups = config.KeyStoreEnableGroups
 	}
-	if config.AuthSessionLifetimeSeconds > 0 {
-		s.authSessionLifetime = time.Duration(config.AuthSessionLifetimeSeconds) * time.Second
+
+	devMgr, err := device.New(config.DeviceManager, urlPrefix)
+	if err != nil {
+		return nil, err
 	}
 
+	// The root HTTP handler. This must be a gorilla/mux.Router since
+	// session handling depends on it.
+	//
+	// If a URL prefix is set, we can't just add a StripPrefix in
+	// front of everything, as the handlers need access to the
+	// actual full request URL, so we just inject the prefix
+	// everywhere.
+	root := mux.NewRouter()
+
+	// If we have customized content, serve it from well-known URLs.
 	if config.SiteLogo != "" {
-		siteLogo, err := loadStaticContent(config.SiteLogo)
+		siteLogo, err := httputil.LoadStaticContent(config.SiteLogo)
 		if err != nil {
 			return nil, err
 		}
-		s.siteLogo = siteLogo
+		root.Handle(h.urlFor("/img/site_logo"), siteLogo)
 	}
 	if config.SiteFavicon != "" {
-		siteFavicon, err := loadStaticContent(config.SiteFavicon)
+		siteFavicon, err := httputil.LoadStaticContent(config.SiteFavicon)
 		if err != nil {
 			return nil, err
 		}
-		s.siteFavicon = siteFavicon
+		root.Handle(h.urlFor("/favicon.ico"), siteFavicon)
 	}
 
-	if config.KeyStore != nil {
-		ks, err := ksclient.New(config.KeyStore)
-		if err != nil {
-			return nil, err
-		}
-		log.Printf("keystore client enabled")
-		s.keystore = ks
-		s.keystoreGroups = config.KeyStoreEnableGroups
-	}
+	// Serve static content to anyone.
+	staticPath := h.urlFor("/static/")
+	root.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath, http.FileServer(&assetfs.AssetFS{
+		Asset:     Asset,
+		AssetDir:  AssetDir,
+		AssetInfo: AssetInfo,
+		Prefix:    "static",
+	})))
 
-	devMgr, err := device.New(config.DeviceManager)
-	if err != nil {
-		return nil, err
-	}
-	s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient,
-		config.AuthService, config.U2FAppID, config.URLPrefix,
-		renderer, sessionSecrets...)
+	// Add the /exchange endpoint (which does not use the normal
+	// HTTP-based login workflow).
+	root.HandleFunc(h.urlFor("/exchange"), h.handleExchange)
 
-	return s, nil
-}
+	// Build the main IDP application router, wrap it with a login
+	// handler, optional CSRF protection, custom HTTP headers,
+	// etc.
+	mainh := http.NewServeMux()
+	mainh.HandleFunc("/logout", h.handleLogout)
+	mainh.HandleFunc("/", h.handleGrantTicket)
 
-func inAnyGroups(groups, ref []string) bool {
-	for _, rr := range ref {
-		for _, gg := range groups {
-			if gg == rr {
-				return true
-			}
-		}
+	loginh := login.New(mainh, devMgr, authClient,
+		config.AuthService, config.U2FAppID, urlPrefix,
+		config.HomepageRedirectURL, renderer, h.loginCallback,
+		sl2bl(config.SessionSecrets),
+		time.Duration(config.AuthSessionLifetimeSeconds)*time.Second)
+
+	apph := httputil.WithDynamicHeaders(loginh, contentSecurityPolicy)
+	if config.CSRFSecret != "" {
+		apph = csrf.Protect([]byte(config.CSRFSecret))(apph)
 	}
-	return false
+
+	// Add CORS headers on the main IDP endpoints.
+	corsp := cors.New(cors.Options{
+		AllowedOrigins:   config.AllowedCORSOrigins,
+		AllowedHeaders:   []string{"*"},
+		AllowCredentials: true,
+		MaxAge:           86400,
+	})
+	apph = corsp.Handler(apph)
+
+	root.PathPrefix(h.urlFor("/")).Handler(apph)
+	h.handler = root
+
+	return h, nil
 }
 
 // We unlock the keystore if the following conditions are met:
@@ -206,17 +187,17 @@ func (h *Server) maybeUnlockKeystore(ctx context.Context, username, password str
 		}
 		shard = userinfo.Shard
 	}
-	return true, h.keystore.Open(ctx, shard, username, password, int(h.authSessionLifetime.Seconds()))
+	return true, h.keystore.Open(ctx, shard, username, password, h.authSessionLifetime)
 }
 
-func (h *Server) loginCallback(w http.ResponseWriter, req *http.Request, username, password string, userinfo *auth.UserInfo) error {
-	// Open the keystore for this user with the password used to
-	// authenticate. Set the TTL to the duration of the
-	// authenticated session.
-	decrypted, err := h.maybeUnlockKeystore(req.Context(), username, password, userinfo)
+// Callback called by the login handler whenever a user successfully
+// logs in. We use it to unlock the keystore with the user's password.
+func (h *Server) loginCallback(ctx context.Context, username, password string, userinfo *auth.UserInfo) error {
+	// Open the keystore for this user, with the same password
+	// used to authenticate.
+	decrypted, err := h.maybeUnlockKeystore(ctx, username, password, userinfo)
 	if err != nil {
-		log.Printf("failed to unlock keystore for user %s: %v", username, err)
-		return err
+		return fmt.Errorf("failed to unlock keystore for user %s: %v", username, err)
 	}
 
 	var kmsg string
@@ -224,43 +205,13 @@ func (h *Server) loginCallback(w http.ResponseWriter, req *http.Request, usernam
 		kmsg = " (key unlocked)"
 	}
 	log.Printf("successful login for user %s%s", username, kmsg)
-
-	// Create cookie-based session for the authenticated user.
-	session := newAuthSession(h.authSessionLifetime, username, userinfo)
-	httpSession, _ := h.authSessionStore.Get(req, authSessionKey) // nolint
-	httpSession.Values["data"] = session
-	return httpSession.Save(req, w)
-}
-
-func (h *Server) redirectToLogin(w http.ResponseWriter, req *http.Request) {
-	http.Redirect(w, req, h.loginHandler.makeLoginURL(req), http.StatusFound)
-}
-
-func (h *Server) withAuth(f func(http.ResponseWriter, *http.Request, *authSession), authFail func(http.ResponseWriter, *http.Request)) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-		httpSession, err := h.authSessionStore.Get(req, authSessionKey)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		session, ok := httpSession.Values["data"].(*authSession)
-		if ok && session.Valid() {
-			f(w, req, session)
-			return
-		}
-		httpSession.Options.MaxAge = -1
-		delete(httpSession.Values, "data")
-		if err := httpSession.Save(req, w); err != nil {
-			log.Printf("error saving session: %v", err)
-		}
-		authFail(w, req)
-	})
+	return nil
 }
 
 // Token signing handler. Authorizes an authenticated user to a service by
 // signing a token with the user's identity. The client is redirected back to
 // the original service, with the signed token.
-func (h *Server) handleHomepage(w http.ResponseWriter, req *http.Request, session *authSession) {
+func (h *Server) handleGrantTicket(w http.ResponseWriter, req *http.Request) {
 	// Extract the authorization request parameters from the HTTP
 	// request query args.
 	//
@@ -268,7 +219,13 @@ func (h *Server) handleHomepage(w http.ResponseWriter, req *http.Request, sessio
 	// it is a POST request redirected from a 307, so we do not
 	// call req.FormValue() but look directly into request.URL
 	// instead.
-	username := session.Username
+	auth, ok := login.GetAuth(req.Context())
+	if !ok {
+		http.Error(w, "No valid session", http.StatusBadRequest)
+		return
+	}
+
+	username := auth.Username
 	service := req.URL.Query().Get("s")
 	destination := req.URL.Query().Get("d")
 	nonce := req.URL.Query().Get("n")
@@ -292,8 +249,8 @@ func (h *Server) handleHomepage(w http.ResponseWriter, req *http.Request, sessio
 	var groups []string
 	if groupsStr != "" {
 		reqGroups := strings.Split(groupsStr, ",")
-		if len(reqGroups) > 0 && session.UserInfo != nil {
-			groups = intersectGroups(reqGroups, session.UserInfo.Groups)
+		if len(reqGroups) > 0 && auth.UserInfo != nil {
+			groups = intersectGroups(reqGroups, auth.UserInfo.Groups)
 			// We only make this check here as a convenience to
 			// the user (we may be able to show a nicer UI): the
 			// actual group ACL must be applied on the destination
@@ -314,28 +271,32 @@ func (h *Server) handleHomepage(w http.ResponseWriter, req *http.Request, sessio
 		return
 	}
 
-	session.AddService(service)
-	if err := sessions.Save(req, w); err != nil {
-		log.Printf("error saving session: %v", err)
-	}
+	// Record the service in the session.
+	auth.AddService(service)
 
 	// Redirect to service callback.
 	callbackURL := serviceLoginCallback(service, destination, token)
 	http.Redirect(w, req, callbackURL, http.StatusFound)
 }
 
-func (h *Server) alreadyLoggedOut(w http.ResponseWriter, req *http.Request) {
-	http.Error(w, "You do not seem to be logged in", http.StatusBadRequest)
-}
-
 type logoutServiceInfo struct {
 	URL  string `json:"url"`
 	Name string `json:"name"`
 }
 
-func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session *authSession) {
+// Logout handler. We generate a page that triggers child logout
+// requests to all the services the user is logged in to.
+func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request) {
+	auth, ok := login.GetAuth(req.Context())
+	if !ok {
+		http.Error(w, "No valid session", http.StatusBadRequest)
+		return
+	}
+
+	//
+
 	var svcs []logoutServiceInfo
-	for _, svc := range session.Services {
+	for _, svc := range auth.Services {
 		svcs = append(svcs, logoutServiceInfo{
 			Name: svc,
 			URL:  serviceLogoutCallback(svc),
@@ -349,32 +310,20 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
 		"IncludeLogoutScripts": true,
 	}
 
-	// Clear the local session. Ignore errors.
-	httpSession, _ := h.authSessionStore.Get(req, authSessionKey) // nolint
-	delete(httpSession.Values, "data")
-	httpSession.Options.MaxAge = -1
-	httpSession.Save(req, w) // nolint
-
 	// Close the keystore.
 	if h.keystore != nil {
 		var shard string
-		if session.UserInfo != nil {
-			shard = session.UserInfo.Shard
+		if auth.UserInfo != nil {
+			shard = auth.UserInfo.Shard
 		}
-		if err := h.keystore.Close(req.Context(), shard, session.Username); err != nil {
-			log.Printf("failed to wipe keystore for user %s: %v", session.Username, err)
+		if err := h.keystore.Close(req.Context(), shard, auth.Username); err != nil {
+			// This is not a fatal error.
+			log.Printf("warning: failed to wipe keystore for user %s: %v", auth.Username, err)
 		}
 	}
 
 	w.Header().Set("Content-Security-Policy", logoutContentSecurityPolicy)
-
-	body, err := h.renderer.Render(req, "logout.html", data)
-	if err != nil {
-		log.Printf("template error in logout(): %v", err)
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	w.Write(body) // nolint
+	h.renderer.Render(w, req, "logout.html", data)
 }
 
 func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) {
@@ -406,91 +355,7 @@ func (h *Server) urlFor(path string) string {
 
 // Handler returns the http.Handler for the SSO server application.
 func (h *Server) Handler() http.Handler {
-	// The root HTTP handler. This must be a gorilla/mux.Router since
-	// sessions depend on it.
-	//
-	// If a URL prefix is set, we can't just add a StripPrefix in
-	// front of everything, as the handlers need access to the
-	// actual full request URL, so we just inject the prefix
-	// everywhere.
-	root := mux.NewRouter()
-
-	// If we have customized content, serve it from well-known URLs.
-	if h.siteLogo != nil {
-		root.Handle(h.urlFor("/img/site_logo"), h.siteLogo)
-	}
-	if h.siteFavicon != nil {
-		root.Handle(h.urlFor("/favicon.ico"), h.siteFavicon)
-	}
-
-	// Serve static content to anyone.
-	staticPath := h.urlFor("/static/")
-	root.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath, http.FileServer(&assetfs.AssetFS{
-		Asset:     Asset,
-		AssetDir:  AssetDir,
-		AssetInfo: AssetInfo,
-		Prefix:    "static",
-	})))
-
-	// Build the main IDP application router, with optional CSRF
-	// protection.
-	m := http.NewServeMux()
-	m.Handle(h.urlFor("/login"), h.loginHandler)
-	m.Handle(h.urlFor("/logout"), h.withAuth(h.handleLogout, h.alreadyLoggedOut))
-	idph := http.Handler(m)
-	if h.csrfSecret != nil {
-		idph = csrf.Protect(h.csrfSecret)(idph)
-	}
-
-	// Add CORS headers on the main SSO API endpoint.
-	c := cors.New(cors.Options{
-		AllowedOrigins:   h.allowedOrigins,
-		AllowedHeaders:   []string{"*"},
-		AllowCredentials: true,
-		MaxAge:           86400,
-	})
-
-	// Add the SSO provider endpoints (root path and /exchange),
-	// which do not need CSRF. We use a HandlerFunc to bypass the
-	// '/' dispatch semantics of the standard http.ServeMux.
-	ssoh := c.Handler(h.withAuth(h.handleHomepage, h.redirectToLogin))
-	userh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		switch {
-		case r.URL.Path == h.urlFor("/"):
-			ssoh.ServeHTTP(w, r)
-		case r.URL.Path == h.urlFor("/exchange"):
-			h.handleExchange(w, r)
-		default:
-			idph.ServeHTTP(w, r)
-		}
-	})
-
-	// User-facing routes require cache-busting and CSP headers.
-	root.PathPrefix(h.urlFor("/")).Handler(withDynamicHeaders(c.Handler(userh)))
-
-	return root
-}
-
-// A relatively strict CSP.
-const contentSecurityPolicy = "default-src 'none'; img-src 'self' data:; script-src 'self'; style-src 'self'; connect-src 'self';"
-
-// Slightly looser CSP for the logout page: it needs to load remote
-// images.
-const logoutContentSecurityPolicy = "default-src 'none'; img-src *; script-src 'self'; style-src 'self'; connect-src *;"
-
-func withDynamicHeaders(h http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("Pragma", "no-cache")
-		w.Header().Set("Cache-Control", "no-store")
-		w.Header().Set("Expires", "-1")
-		w.Header().Set("X-Frame-Options", "NONE")
-		w.Header().Set("X-XSS-Protection", "1; mode=block")
-		w.Header().Set("X-Content-Type-Options", "nosniff")
-		if w.Header().Get("Content-Security-Policy") == "" {
-			w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
-		}
-		h.ServeHTTP(w, r)
-	})
+	return h.handler
 }
 
 // Parse the templates that are embedded with the binary (in bindata.go).
@@ -514,64 +379,46 @@ func parseEmbeddedTemplates() *template.Template {
 	return root
 }
 
-type renderer struct {
-	tpl *template.Template
-
-	urlPrefix          string
-	siteName           string
-	siteLogo           string
-	siteFavicon        string
-	accountRecoveryURL string
-}
-
-func newRenderer(config *Config) *renderer {
-	return &renderer{
-		tpl:                parseEmbeddedTemplates(),
-		urlPrefix:          strings.TrimRight(config.URLPrefix, "/"),
-		accountRecoveryURL: config.AccountRecoveryURL,
-		siteName:           config.SiteName,
-		siteLogo:           config.SiteLogo,
-		siteFavicon:        config.SiteFavicon,
+// Template helper function that encodes its input as JSON.
+func toJSON(obj interface{}) string {
+	data, err := json.Marshal(obj)
+	if err != nil {
+		return ""
 	}
+	return string(data)
 }
 
-func (r *renderer) Render(req *http.Request, templateName string, data map[string]interface{}) ([]byte, error) {
-	data["CSRFField"] = csrf.TemplateField(req)
-	data["URLPrefix"] = r.urlPrefix
-	data["AccountRecoveryURL"] = r.accountRecoveryURL
-	data["SiteName"] = r.siteName
-	data["SiteLogo"] = r.siteLogo
-	data["SiteFavicon"] = r.siteFavicon
-
-	var buf bytes.Buffer
-	if err := r.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
-		return nil, err
+func sl2bl(sl []string) [][]byte {
+	var out [][]byte
+	for _, s := range sl {
+		out = append(out, []byte(s))
 	}
-	return buf.Bytes(), nil
-}
-
-type staticContent struct {
-	modtime time.Time
-	name    string
-	data    []byte
+	return out
 }
 
-func loadStaticContent(path string) (*staticContent, error) {
-	stat, err := os.Stat(path)
-	if err != nil {
-		return nil, err
-	}
-	data, err := ioutil.ReadFile(path) // #nosec
-	if err != nil {
-		return nil, err
+// Returns true if the intersection of the sets isn't empty (in O(N^2)
+// time).
+func inAnyGroups(groups, ref []string) bool {
+	for _, rr := range ref {
+		for _, gg := range groups {
+			if gg == rr {
+				return true
+			}
+		}
 	}
-	return &staticContent{
-		name:    path,
-		modtime: stat.ModTime(),
-		data:    data,
-	}, nil
+	return false
 }
 
-func (c *staticContent) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	http.ServeContent(w, req, c.name, c.modtime, bytes.NewReader(c.data))
+// Returns the intersection of two string lists (in O(N^2) time).
+func intersectGroups(a, b []string) []string {
+	var out []string
+	for _, aa := range a {
+		for _, bb := range b {
+			if aa == bb {
+				out = append(out, aa)
+				break
+			}
+		}
+	}
+	return out
 }
diff --git a/server/http_test.go b/server/http_test.go
index 8f2fd0b43df403add756f86771776c8181002219..a9cdf13cd086bf85bdc291b39c2e1687b2768a66 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -161,7 +161,7 @@ func checkLoginPasswordPage(t testing.TB, resp *http.Response) {
 var otpFieldRx = regexp.MustCompile(`<input[^>]*name="otp"`)
 
 func checkLoginOTPPage(t testing.TB, resp *http.Response) {
-	if resp.Request.URL.Path != "/login" {
+	if resp.Request.URL.Path != "/login/otp" {
 		t.Errorf("request path is not /login (%s)", resp.Request.URL.String())
 	}
 	data, err := ioutil.ReadAll(resp.Body)
@@ -283,7 +283,7 @@ func TestHTTP_LoginOTP(t *testing.T) {
 	// 302 redirect to the target service.
 	v = make(url.Values)
 	v.Set("otp", "123456")
-	doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService)
+	doPostForm(t, httpSrv, c, "/login/otp", v, checkRedirectToTargetService)
 }
 
 func createFakeKeyStore(t testing.TB, username, password string) *httptest.Server {
@@ -304,7 +304,7 @@ func createFakeKeyStore(t testing.TB, username, password string) *httptest.Serve
 			t.Errorf("bad password in keystore Open request: expected %s, got %s", password, openReq.Password)
 		}
 		w.Header().Set("Content-Type", "application/json")
-		io.WriteString(w, "{}")
+		io.WriteString(w, "{}") // nolint
 	})
 	return httptest.NewServer(h)
 }
diff --git a/server/httplog_test.go b/server/httplog_test.go
index 1176a70aa1e87bf5fca37592c6aab42e037cbc2e..aafd78919bb9ee123c200c8a8aecf5cd74b663d3 100644
--- a/server/httplog_test.go
+++ b/server/httplog_test.go
@@ -67,4 +67,4 @@ func (dl DefaultLogger) LogResponse(req *http.Request, res *http.Response, err e
 }
 
 // DefaultLoggedTransport wraps http.DefaultTransport to log using DefaultLogger
-var DefaultLoggedTransport = NewLoggedTransport(http.DefaultTransport, DefaultLogger{})
+//var DefaultLoggedTransport = NewLoggedTransport(http.DefaultTransport, DefaultLogger{})
diff --git a/server/login.go b/server/login.go
deleted file mode 100644
index d840a0b1b47515c607cac193761e7baf61f6ad24..0000000000000000000000000000000000000000
--- a/server/login.go
+++ /dev/null
@@ -1,386 +0,0 @@
-package server
-
-import (
-	"encoding/gob"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"log"
-	"net/http"
-	"net/url"
-	"strings"
-	"time"
-
-	"github.com/gorilla/sessions"
-	"github.com/tstranex/u2f"
-	"go.opencensus.io/trace"
-
-	"git.autistici.org/id/auth"
-	authclient "git.autistici.org/id/auth/client"
-	"git.autistici.org/id/go-sso/httputil"
-	"git.autistici.org/id/go-sso/server/device"
-)
-
-const loginSessionKey = "_login"
-
-type loginSession struct {
-	*httputil.ExpiringSession
-
-	State loginState
-
-	// Post-login redirection URL.
-	Redir string
-
-	// Cached from the first form.
-	Username string
-	Password string
-
-	// The auth.Response is cached for 2FA.
-	AuthResponse *auth.Response
-}
-
-// The login session is short-lived, it only needs to last for the duration of
-// the login process itself.
-var defaultLoginSessionLifetime = 10 * time.Minute
-
-func newLoginSession() *loginSession {
-	return &loginSession{
-		ExpiringSession: httputil.NewExpiringSession(defaultLoginSessionLifetime),
-		State:           loginStatePassword,
-	}
-}
-
-type loginState int
-
-const (
-	loginStateNone = iota
-	loginStatePassword
-	loginStateOTP
-	loginStateU2F
-	loginStateSuccess
-)
-
-func init() {
-	gob.Register(&loginSession{})
-}
-
-type loginCallbackFunc func(http.ResponseWriter, *http.Request, string, string, *auth.UserInfo) error
-
-type loginHandler struct {
-	authClient        authclient.Client
-	authService       string
-	u2fAppID          string
-	urlPrefix         string
-	devMgr            *device.Manager
-	loginCallback     loginCallbackFunc
-	loginSessionStore sessions.Store
-	renderer          *renderer
-}
-
-// NewLoginHandler will wrap an http.Handler with the login workflow,
-// invoking it only on successful login.
-func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix string, rndr *renderer, keyPairs ...[]byte) *loginHandler {
-	store := sessions.NewCookieStore(keyPairs...)
-	store.Options = &sessions.Options{
-		HttpOnly: true,
-		Secure:   true,
-		MaxAge:   0,
-	}
-	return &loginHandler{
-		authClient:        authClient,
-		authService:       authService,
-		u2fAppID:          u2fAppID,
-		urlPrefix:         strings.TrimRight(urlPrefix, "/"),
-		devMgr:            devMgr,
-		loginCallback:     okHandler,
-		loginSessionStore: store,
-		renderer:          rndr,
-	}
-}
-
-func (l *loginHandler) fetchOrInitSession(req *http.Request) (*sessions.Session, *loginSession, error) {
-	// Either fetch the current session or create a new blank one.
-	httpSession, err := l.loginSessionStore.Get(req, loginSessionKey)
-	if err != nil {
-		return nil, nil, err
-	}
-	session, ok := httpSession.Values["data"].(*loginSession)
-	if !ok || !session.Valid() {
-		session = newLoginSession()
-
-		// Initialize session. The only parameter is 'r', the target
-		// redirect location. Enforce relative redirect URL (no host
-		// should be specified).
-		session.Redir = req.FormValue("r")
-		if session.Redir == "" {
-			return nil, nil, errors.New("empty login redirect target")
-		}
-		if !strings.HasPrefix(session.Redir, "/") || strings.HasPrefix(session.Redir, "//") {
-			return nil, nil, errors.New("bad login redirect target")
-		}
-
-		httpSession.Values["data"] = session
-	}
-
-	return httpSession, session, nil
-}
-
-// The login session controls the flow of the client - it's just a way
-// to ensure that every step is authorized as part of the login
-// sequence.
-func (l *loginHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	httpSession, session, err := l.fetchOrInitSession(req)
-	if err != nil {
-		log.Printf("login session init error: %v", err)
-		http.Error(w, err.Error(), http.StatusBadRequest)
-		return
-	}
-
-	// Dispatch the current state to its handler. Handlers will
-	// handle the current request and either 1) validate the
-	// request successfully and move to the next state, or 2)
-	// return a response to the user. Handlers fall through to the
-	// next state on success.
-	for {
-		newState, body, err := l.dispatch(w, req, session)
-
-		// Uncaught errors result in 500s.
-		if err != nil {
-			log.Printf("login error: %v", err)
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-
-		switch newState {
-		case loginStateSuccess:
-			// Successful login. Delete the login session and invoke
-			// the login callback, before redirecting to the
-			// original URL.
-			httpSession.Options.MaxAge = -1
-			delete(httpSession.Values, "data")
-			if err := httpSession.Save(req, w); err != nil {
-				log.Printf("login error saving session: %v", err)
-				http.Error(w, err.Error(), http.StatusInternalServerError)
-				return
-			}
-			if err := l.loginCallback(w, req, session.Username, session.Password, session.AuthResponse.UserInfo); err != nil {
-				log.Printf("login callback error: %v: user=%s", err, session.Username)
-				http.Error(w, err.Error(), http.StatusInternalServerError)
-				return
-			}
-			http.Redirect(w, req, session.Redir, http.StatusFound)
-			return
-
-		case loginStateNone:
-			if err := httpSession.Save(req, w); err != nil {
-				log.Printf("error saving login session: %v", err)
-				http.Error(w, err.Error(), http.StatusInternalServerError)
-				return
-			}
-			w.Write(body) // nolint
-			return
-
-		default:
-			// Fall through to the next handler.
-			session.State = newState
-		}
-	}
-}
-
-func (l *loginHandler) dispatch(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
-	switch session.State {
-	case loginStatePassword:
-		return l.handlePassword(w, req, session)
-	case loginStateOTP, loginStateU2F:
-		return l.handle2FA(w, req, session)
-	}
-	return loginStateNone, nil, errors.New("unreachable")
-}
-
-// Handle password-based login.
-func (l *loginHandler) handlePassword(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
-	// Case-fold usernames to lowercase.
-	username := strings.ToLower(req.FormValue("username"))
-	password := req.FormValue("password")
-
-	// If the request is a POST, attempt login with username/password.
-	env := map[string]interface{}{
-		"Error":    false,
-		"Username": username,
-	}
-	if req.Method == "POST" && username != "" && password != "" {
-		resp, err := l.makeAuthRequest(w, req, username, password, "", nil)
-		if err != nil {
-			return loginStateNone, nil, err
-		}
-		// Save username / password for later in case of
-		// successful (or partially succesful) result.
-		switch resp.Status {
-		case auth.StatusOK:
-			session.Username = username
-			session.Password = password
-			session.AuthResponse = resp
-			return loginStateSuccess, nil, nil
-		case auth.StatusInsufficientCredentials:
-			session.Username = username
-			session.Password = password
-			session.AuthResponse = resp
-
-			// Always prefer U2F if supported, default to OTP.  We
-			// are assuming that the auth.Response is well formed,
-			// and TFAMethods is not nil.
-			var nextState loginState = loginStateOTP
-			if resp.Has2FAMethod(auth.TFAMethodU2F) {
-				nextState = loginStateU2F
-			}
-			return nextState, nil, nil
-		}
-		env["Error"] = true
-	}
-
-	body, err := l.renderer.Render(req, "login_password.html", env)
-	return loginStateNone, body, err
-}
-
-func (l *loginHandler) handle2FA(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
-	// The '2fa' request parameter can be used to manually switch between
-	// 2fa mechanisms. There is no need to pass the parameter through POSTs
-	// though, as the login session state is sticky.
-	if switch2fa := auth.TFAMethod(req.FormValue("2fa")); switch2fa != "" {
-		if !session.AuthResponse.Has2FAMethod(switch2fa) {
-			return loginStateNone, nil, errors.New("unsupported 2FA method")
-		}
-		switch switch2fa {
-		case auth.TFAMethodOTP:
-			session.State = loginStateOTP
-		case auth.TFAMethodU2F:
-			session.State = loginStateU2F
-		}
-	}
-
-	switch session.State {
-	case loginStateOTP:
-		return l.handleOTP(w, req, session)
-	case loginStateU2F:
-		return l.handleU2F(w, req, session)
-	}
-	return loginStateNone, nil, errors.New("unreachable")
-}
-
-// Handle login with password and TOTP.
-func (l *loginHandler) handleOTP(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
-	otp := req.FormValue("otp")
-
-	env := map[string]interface{}{
-		"AuthResponse": session.AuthResponse,
-		"Error":        false,
-	}
-	if req.Method == "POST" && otp != "" {
-		resp, err := l.makeAuthRequest(w, req, session.Username, session.Password, otp, nil)
-		if err != nil {
-			return loginStateNone, nil, err
-		}
-		if resp.Status == auth.StatusOK {
-			session.AuthResponse = resp
-			return loginStateSuccess, nil, nil
-		}
-		env["Error"] = true
-	}
-
-	body, err := l.renderer.Render(req, "login_otp.html", env)
-	return loginStateNone, body, err
-}
-
-// Handle login with password and hardware token.
-func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, session *loginSession) (loginState, []byte, error) {
-	u2fresponse := req.FormValue("u2f_response")
-
-	env := map[string]interface{}{
-		"AuthResponse":   session.AuthResponse,
-		"U2FSignRequest": session.AuthResponse.U2FSignRequest,
-		"Error":          false,
-	}
-	if req.Method == "POST" && u2fresponse != "" {
-		var usr u2f.SignResponse
-		if err := json.Unmarshal([]byte(u2fresponse), &usr); err != nil {
-			return loginStateNone, nil, err
-		}
-
-		resp, err := l.makeAuthRequest(w, req, session.Username, session.Password, "", &usr)
-		if err != nil {
-			return loginStateNone, nil, err
-		}
-		if resp.Status == auth.StatusOK {
-			session.AuthResponse = resp
-			return loginStateSuccess, nil, nil
-		}
-		env["Error"] = true
-	}
-
-	body, err := l.renderer.Render(req, "login_u2f.html", env)
-	return loginStateNone, body, err
-}
-
-// Make the auth request to the authentication server.
-func (l *loginHandler) makeAuthRequest(w http.ResponseWriter, req *http.Request, username, password, otp string, u2fResponse *u2f.SignResponse) (*auth.Response, error) {
-	appID := l.u2fAppID
-	if appID == "" {
-		appID = u2fAppIDFromRequest(req)
-	}
-	ar := auth.Request{
-		Service:     l.authService,
-		Username:    username,
-		Password:    []byte(password),
-		OTP:         otp,
-		DeviceInfo:  l.devMgr.GetDeviceInfoFromRequest(w, req),
-		U2FResponse: u2fResponse,
-		U2FAppID:    appID,
-	}
-
-	// Trace the authentication request.
-	ctx, span := trace.StartSpan(req.Context(), "auth",
-		trace.WithSpanKind(trace.SpanKindClient))
-	span.AddAttributes(
-		trace.StringAttribute("auth.user", username),
-		trace.StringAttribute("auth.service", l.authService),
-		trace.BoolAttribute("auth.with_password", len(password) > 0),
-		trace.BoolAttribute("auth.with_otp", otp != ""),
-		trace.BoolAttribute("auth.with_u2f", u2fResponse != nil),
-	)
-	defer span.End()
-
-	resp, err := l.authClient.Authenticate(ctx, &ar)
-
-	// Record the authentication response status in the trace.
-	if err != nil {
-		span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()})
-	} else if resp.Status == auth.StatusOK {
-		span.SetStatus(trace.Status{Code: trace.StatusCodeOK, Message: "OK"})
-	} else {
-		span.SetStatus(trace.Status{Code: trace.StatusCodePermissionDenied, Message: resp.Status.String()})
-	}
-
-	return resp, err
-}
-
-// Return a (relative) URL that will redirect the user to the login
-// page and set the continue token to the original requested URL.
-func (l *loginHandler) makeLoginURL(req *http.Request) string {
-	v := make(url.Values)
-	v.Set("r", req.URL.Path+"?"+req.URL.RawQuery)
-	return fmt.Sprintf("%s/login?%s", l.urlPrefix, v.Encode())
-}
-
-// Template helper function that encodes its input as JSON.
-func toJSON(obj interface{}) string {
-	data, err := json.Marshal(obj)
-	if err != nil {
-		return ""
-	}
-	return string(data)
-}
-
-// Guess the correct U2F AppID from the HTTP request.
-func u2fAppIDFromRequest(r *http.Request) string {
-	return fmt.Sprintf("https://%s", r.Host)
-}
diff --git a/server/login/login.go b/server/login/login.go
new file mode 100644
index 0000000000000000000000000000000000000000..17872c3a83d067d217e51db80d27fc9d242796b9
--- /dev/null
+++ b/server/login/login.go
@@ -0,0 +1,434 @@
+package login
+
+import (
+	"context"
+	"encoding/gob"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+	"time"
+
+	"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"
+)
+
+const loginSessionKey = "_auth"
+
+const maxFailures = 5
+
+type Auth struct {
+	// True if the user is authenticated.
+	Authenticated bool
+
+	// User name and other information (like group membership).
+	Username string
+	UserInfo *auth.UserInfo
+
+	// Services the user has logged in to from this session.
+	Services []string
+}
+
+// AddService adds a service to the current session (if it's not
+// already there).
+func (s *Auth) AddService(service string) {
+	for _, svc := range s.Services {
+		if svc == service {
+			return
+		}
+	}
+	s.Services = append(s.Services, service)
+}
+
+type loginSession struct {
+	Auth
+
+	// Temporary fields required by the login process.
+	Password     string
+	AuthResponse *auth.Response
+	Redir        string
+	Failures     int
+}
+
+func (l *loginSession) Reset() {
+	l.Username = ""
+	l.UserInfo = nil
+	l.Services = nil
+	l.Authenticated = false
+	l.Password = ""
+	l.AuthResponse = nil
+	l.Failures = 0
+	// Keep Redir.
+}
+
+func (l *loginSession) Can2FA(method auth.TFAMethod) bool {
+	return (l.Username != "" && l.Password != "" && l.AuthResponse != nil &&
+		l.AuthResponse.Has2FAMethod(method))
+}
+
+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,
+	}
+}
+
+type ctxKey int
+
+const authCtxKey ctxKey = 0
+
+func withAuth(ctx context.Context, s *Auth) context.Context {
+	return context.WithValue(ctx, authCtxKey, s)
+}
+
+// GetAuth returns the current user information, if any. Presence of an Auth
+// object implies that the authentication succeeded.
+func GetAuth(ctx context.Context) (*Auth, bool) {
+	s, ok := ctx.Value(authCtxKey).(*Auth)
+	return s, ok
+}
+
+// A LoginCallback will be invoked on every successful login, with username,
+// password, and additional UserInfo. If it returns an error, the login
+// workflow will fail.
+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
+	sessionTTL       time.Duration
+	urlPrefix        string
+	renderer         *httputil.Renderer
+	authClient       authclient.Client
+	authService      string
+	u2fAppID         string
+	devMgr           *device.Manager
+	fallbackRedirect string
+	callback         LoginCallback
+}
+
+// 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.
+	}
+
+	return &Login{
+		wrap:             wrap,
+		sessionStore:     store,
+		sessionTTL:       sessionTTL,
+		urlPrefix:        urlPrefix,
+		renderer:         renderer,
+		authClient:       authClient,
+		authService:      authService,
+		u2fAppID:         u2fAppID,
+		devMgr:           devMgr,
+		callback:         callback,
+		fallbackRedirect: fallbackRedirect,
+	}
+}
+
+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)
+	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 session
+}
+
+func (l *Login) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	sess := l.fetchOrInitSession(req)
+
+	// This way we don't have to call sess.Save explicitly.
+	w = httputil.NewSessionResponseWriter(w, req)
+
+	// A very simple router.
+	switch req.URL.Path {
+	case l.urlFor("/login"):
+		l.handleLogin(w, req, sess)
+	case l.urlFor("/login/u2f"):
+		l.handleLoginU2F(w, req, sess)
+	case l.urlFor("/login/otp"):
+		l.handleLoginOTP(w, req, sess)
+	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 {
+			// Save the current URL in the session for later redirect.
+			sess.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))
+		l.wrap.ServeHTTP(w, req)
+	}
+}
+
+func (l *Login) loginOk(w http.ResponseWriter, req *http.Request, sess *loginSessionInt, 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)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+
+	target := sess.Redir
+	if target == "" {
+		// This is so if you navigate directly to the login URL, we
+		// try to send you somewhere meaningful anyway.
+		target = l.fallbackRedirect
+	}
+
+	sess.Authenticated = true
+	sess.Redir = ""
+	sess.Password = ""
+	sess.AuthResponse = nil
+
+	http.Redirect(w, req, target, http.StatusFound)
+}
+
+func (l *Login) handleLogin(w http.ResponseWriter, req *http.Request, sess *loginSessionInt) {
+	if req.Method != "GET" && req.Method != "POST" {
+		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	// Reset the session to a known state.
+	sess.Reset()
+
+	// Case-fold usernames to lowercase.
+	username := strings.ToLower(req.FormValue("username"))
+	password := req.FormValue("password")
+
+	// If the request is a POST, attempt login with username/password.
+	env := map[string]interface{}{
+		"Error":    false,
+		"Username": username,
+	}
+	if req.Method == "POST" && username != "" && password != "" {
+		resp, err := l.makeAuthRequest(w, req, username, password, "", nil)
+		if err != nil {
+			log.Printf("error in makeAuthRequest: %v", err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		// Save username / password and the AuthResponse for later in
+		// case of successful (or partially succesful) result.
+		sess.Username = username
+
+		switch resp.Status {
+		case auth.StatusOK:
+			l.loginOk(w, req, sess, password)
+			return
+		case auth.StatusInsufficientCredentials:
+			sess.Password = password
+			sess.AuthResponse = resp
+
+			// Always prefer U2F if supported, default to OTP.  We
+			// are assuming that the auth.Response is well formed,
+			// and TFAMethods is not nil.
+			method := "otp"
+			if resp.Has2FAMethod(auth.TFAMethodU2F) {
+				method = "u2f"
+			}
+			http.Redirect(w, req, l.urlFor("/login/"+method), http.StatusFound)
+			return
+		}
+		env["Error"] = true
+	}
+
+	l.renderer.Render(w, req, "login_password.html", env)
+}
+
+func (l *Login) handleLoginOTP(w http.ResponseWriter, req *http.Request, sess *loginSessionInt) {
+	if req.Method != "GET" && req.Method != "POST" {
+		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	// First verify that we are ready to do 2FA.
+	if !sess.Can2FA(auth.TFAMethodOTP) {
+		log.Printf("got invalid 2FA request")
+		http.Redirect(w, req, l.urlFor("/login"), http.StatusFound)
+		return
+	}
+
+	otp := req.FormValue("otp")
+
+	env := map[string]interface{}{
+		"AuthResponse": sess.AuthResponse,
+		"Error":        false,
+	}
+	if req.Method == "POST" && otp != "" {
+		resp, err := l.makeAuthRequest(w, req, sess.Username, sess.Password, otp, nil)
+		if err != nil {
+			log.Printf("error in makeAuthRequest: %v", err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		if resp.Status == auth.StatusOK {
+			l.loginOk(w, req, sess, sess.Password)
+			return
+		}
+		env["Error"] = true
+		sess.Failures++
+		if sess.Failures > maxFailures {
+			log.Printf("too many login failures for %s, starting over", sess.Username)
+			http.Redirect(w, req, l.urlFor("/login"), http.StatusFound)
+			return
+		}
+	}
+
+	l.renderer.Render(w, req, "login_otp.html", env)
+}
+
+func (l *Login) handleLoginU2F(w http.ResponseWriter, req *http.Request, sess *loginSessionInt) {
+	if req.Method != "GET" && req.Method != "POST" {
+		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	// First verify that we are ready to do 2FA.
+	if !sess.Can2FA(auth.TFAMethodU2F) {
+		log.Printf("got invalid 2FA request")
+		http.Redirect(w, req, l.urlFor("/login"), http.StatusFound)
+		return
+	}
+
+	u2fresponse := req.FormValue("u2f_response")
+
+	env := map[string]interface{}{
+		"AuthResponse":   sess.AuthResponse,
+		"U2FSignRequest": sess.AuthResponse.U2FSignRequest,
+		"Error":          false,
+	}
+	if req.Method == "POST" && u2fresponse != "" {
+		var usr u2f.SignResponse
+		if err := json.Unmarshal([]byte(u2fresponse), &usr); err != nil {
+			log.Printf("error deserializing U2F SignResponse: %v", err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		resp, err := l.makeAuthRequest(w, req, sess.Username, sess.Password, "", &usr)
+		if err != nil {
+			log.Printf("error in makeAuthRequest: %v", err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		if resp.Status == auth.StatusOK {
+			l.loginOk(w, req, sess, sess.Password)
+			return
+		}
+		env["Error"] = true
+		sess.Failures++
+		if sess.Failures > maxFailures {
+			log.Printf("too many login failures for %s, starting over", sess.Username)
+			http.Redirect(w, req, l.urlFor("/login"), http.StatusFound)
+			return
+		}
+	}
+
+	l.renderer.Render(w, req, "login_u2f.html", env)
+}
+
+// Make the auth request to the authentication server.
+func (l *Login) makeAuthRequest(w http.ResponseWriter, req *http.Request, username, password, otp string, u2fResponse *u2f.SignResponse) (*auth.Response, error) {
+	appID := l.u2fAppID
+	if appID == "" {
+		appID = u2fAppIDFromRequest(req)
+	}
+	ar := auth.Request{
+		Service:     l.authService,
+		Username:    username,
+		Password:    []byte(password),
+		OTP:         otp,
+		DeviceInfo:  l.devMgr.GetDeviceInfoFromRequest(w, req),
+		U2FResponse: u2fResponse,
+		U2FAppID:    appID,
+	}
+
+	// Trace the authentication request.
+	ctx, span := trace.StartSpan(req.Context(), "auth",
+		trace.WithSpanKind(trace.SpanKindClient))
+	span.AddAttributes(
+		trace.StringAttribute("auth.user", username),
+		trace.StringAttribute("auth.service", l.authService),
+		trace.BoolAttribute("auth.with_password", len(password) > 0),
+		trace.BoolAttribute("auth.with_otp", otp != ""),
+		trace.BoolAttribute("auth.with_u2f", u2fResponse != nil),
+	)
+	defer span.End()
+
+	resp, err := l.authClient.Authenticate(ctx, &ar)
+
+	// Record the authentication response status in the trace.
+	if err != nil {
+		span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()})
+	} else if resp.Status == auth.StatusOK {
+		span.SetStatus(trace.Status{Code: trace.StatusCodeOK, Message: "OK"})
+	} else {
+		span.SetStatus(trace.Status{Code: trace.StatusCodePermissionDenied, Message: resp.Status.String()})
+	}
+
+	return resp, err
+}
+
+// Guess the correct U2F AppID from the HTTP request.
+func u2fAppIDFromRequest(r *http.Request) string {
+	return fmt.Sprintf("https://%s", r.Host)
+}
diff --git a/server/service_test.go b/server/service_test.go
index 4d9d6a12f8442137cfeebebc931f1a9d394cdc1c..c7471f2bf83fea27907685a5a656e90b53b93970 100644
--- a/server/service_test.go
+++ b/server/service_test.go
@@ -29,8 +29,8 @@ func testConfig(t testing.TB, tmpdir, keystoreURL string) *Config {
 	if err != nil {
 		t.Fatal(err)
 	}
-	ioutil.WriteFile(filepath.Join(tmpdir, "secret"), priv, 0600)
-	ioutil.WriteFile(filepath.Join(tmpdir, "public"), pub, 0600)
+	ioutil.WriteFile(filepath.Join(tmpdir, "secret"), priv, 0600) // nolint
+	ioutil.WriteFile(filepath.Join(tmpdir, "public"), pub, 0600)  // nolint
 
 	cfgstr := fmt.Sprintf(`---
 secret_key_file: %s
@@ -51,7 +51,7 @@ keystore:
   url: "%s"
 `, keystoreURL)
 	}
-	ioutil.WriteFile(filepath.Join(tmpdir, "config"), []byte(cfgstr), 0600)
+	ioutil.WriteFile(filepath.Join(tmpdir, "config"), []byte(cfgstr), 0600) // nolint
 
 	config, err := loadConfig(filepath.Join(tmpdir, "config"))
 	if err != nil {
diff --git a/server/util.go b/server/util.go
deleted file mode 100644
index 63439fd819cf81487954bc0d5cd574226f665471..0000000000000000000000000000000000000000
--- a/server/util.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package server
-
-// Returns the intersection of two string lists (in O(N^2) time).
-func intersectGroups(a, b []string) []string {
-	var out []string
-	for _, aa := range a {
-		for _, bb := range b {
-			if aa == bb {
-				out = append(out, aa)
-				break
-			}
-		}
-	}
-	return out
-}
diff --git a/vendor/github.com/gorilla/context/README.md b/vendor/github.com/gorilla/context/README.md
index 08f86693bcd88a09bacde0bfffbd2bbf32bad9f8..d31f2bad49f3315b123a9e3e2c8e4b60912b0254 100644
--- a/vendor/github.com/gorilla/context/README.md
+++ b/vendor/github.com/gorilla/context/README.md
@@ -7,4 +7,4 @@ gorilla/context is a general purpose registry for global request variables.
 > Note: gorilla/context, having been born well before `context.Context` existed, does not play well
 > with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`.
 
-Read the full documentation here: http://www.gorillatoolkit.org/pkg/context
+Read the full documentation here: https://www.gorillatoolkit.org/pkg/context
diff --git a/vendor/github.com/gorilla/handlers/README.md b/vendor/github.com/gorilla/handlers/README.md
index 296a956b88af28d51e46fc735d53f2f8b008f085..4a6895dcfb1a5d4b13c2af5545247d8ebe48f796 100644
--- a/vendor/github.com/gorilla/handlers/README.md
+++ b/vendor/github.com/gorilla/handlers/README.md
@@ -25,7 +25,7 @@ with Go's `net/http` package (or any framework supporting `http.Handler`), inclu
 * [**RecoveryHandler**](https://godoc.org/github.com/gorilla/handlers#RecoveryHandler) for recovering from unexpected panics.
 
 Other handlers are documented [on the Gorilla
-website](https://www.gorillatoolkit.org/pkg/handlers).
+website](http://www.gorillatoolkit.org/pkg/handlers).
 
 ## Example
 
diff --git a/vendor/github.com/gorilla/handlers/cors.go b/vendor/github.com/gorilla/handlers/cors.go
index 0dcdffb3d32e2dffdc6c2dd89c377506ffcdc343..1acf80d1bb2c4fba3617992d976633dc8390c5c4 100644
--- a/vendor/github.com/gorilla/handlers/cors.go
+++ b/vendor/github.com/gorilla/handlers/cors.go
@@ -19,16 +19,14 @@ type cors struct {
 	maxAge                 int
 	ignoreOptions          bool
 	allowCredentials       bool
-	optionStatusCode       int
 }
 
 // OriginValidator takes an origin string and returns whether or not that origin is allowed.
 type OriginValidator func(string) bool
 
 var (
-	defaultCorsOptionStatusCode = 200
-	defaultCorsMethods          = []string{"GET", "HEAD", "POST"}
-	defaultCorsHeaders          = []string{"Accept", "Accept-Language", "Content-Language", "Origin"}
+	defaultCorsMethods = []string{"GET", "HEAD", "POST"}
+	defaultCorsHeaders = []string{"Accept", "Accept-Language", "Content-Language", "Origin"}
 	// (WebKit/Safari v9 sends the Origin header by default in AJAX requests)
 )
 
@@ -132,7 +130,6 @@ func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set(corsAllowOriginHeader, returnOrigin)
 
 	if r.Method == corsOptionMethod {
-		w.WriteHeader(ch.optionStatusCode)
 		return
 	}
 	ch.h.ServeHTTP(w, r)
@@ -167,10 +164,9 @@ func CORS(opts ...CORSOption) func(http.Handler) http.Handler {
 
 func parseCORSOptions(opts ...CORSOption) *cors {
 	ch := &cors{
-		allowedMethods:   defaultCorsMethods,
-		allowedHeaders:   defaultCorsHeaders,
-		allowedOrigins:   []string{},
-		optionStatusCode: defaultCorsOptionStatusCode,
+		allowedMethods: defaultCorsMethods,
+		allowedHeaders: defaultCorsHeaders,
+		allowedOrigins: []string{},
 	}
 
 	for _, option := range opts {
@@ -255,20 +251,7 @@ func AllowedOriginValidator(fn OriginValidator) CORSOption {
 	}
 }
 
-// OptionStatusCode sets a custom status code on the OPTIONS requests.
-// Default behaviour sets it to 200 to reflect best practices. This is option is not mandatory
-// and can be used if you need a custom status code (i.e 204).
-//
-// More informations on the spec:
-// https://fetch.spec.whatwg.org/#cors-preflight-fetch
-func OptionStatusCode(code int) CORSOption {
-	return func(ch *cors) error {
-		ch.optionStatusCode = code
-		return nil
-	}
-}
-
-// ExposedHeaders can be used to specify headers that are available
+// ExposeHeaders can be used to specify headers that are available
 // and will not be stripped out by the user-agent.
 func ExposedHeaders(headers []string) CORSOption {
 	return func(ch *cors) error {
diff --git a/vendor/github.com/gorilla/mux/AUTHORS b/vendor/github.com/gorilla/mux/AUTHORS
new file mode 100644
index 0000000000000000000000000000000000000000..b722392ee59266773c107a8e871d6b2a3085cb7c
--- /dev/null
+++ b/vendor/github.com/gorilla/mux/AUTHORS
@@ -0,0 +1,8 @@
+# This is the official list of gorilla/mux authors for copyright purposes.
+#
+# Please keep the list sorted.
+
+Google LLC (https://opensource.google.com/)
+Kamil Kisielk <kamil@kamilkisiel.net>
+Matt Silverlock <matt@eatsleeprepeat.net>
+Rodrigo Moraes (https://github.com/moraes)
diff --git a/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md b/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000000000000000000000000000000000000..232be82e47a6abe7fbf9e73ccadde10b3022264c
--- /dev/null
+++ b/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md
@@ -0,0 +1,11 @@
+**What version of Go are you running?** (Paste the output of `go version`)
+
+
+**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`)
+
+
+**Describe your problem** (and what you have tried so far)
+
+
+**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it)
+
diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE
index 0e5fb872800da9557f75a5650bb9d80c1c2cf715..6903df6386e98928a3236b87c84b71260c2541a6 100644
--- a/vendor/github.com/gorilla/mux/LICENSE
+++ b/vendor/github.com/gorilla/mux/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
+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
diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md
index 8dcd7188700893494ec123076a7b65a3c8412907..0425bb8018e7f1464a17e78576886bda636e342a 100644
--- a/vendor/github.com/gorilla/mux/README.md
+++ b/vendor/github.com/gorilla/mux/README.md
@@ -1,12 +1,12 @@
-gorilla/mux
-===
+# gorilla/mux
+
 [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
 [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
 [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge)
 
 ![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png)
 
-http://www.gorillatoolkit.org/pkg/mux
+https://www.gorillatoolkit.org/pkg/mux
 
 Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to
 their respective handler.
@@ -27,6 +27,9 @@ The name mux stands for "HTTP request multiplexer". Like the standard `http.Serv
 * [Static Files](#static-files)
 * [Registered URLs](#registered-urls)
 * [Walking Routes](#walking-routes)
+* [Graceful Shutdown](#graceful-shutdown)
+* [Middleware](#middleware)
+* [Testing Handlers](#testing-handlers)
 * [Full Example](#full-example)
 
 ---
@@ -45,11 +48,11 @@ Let's start registering a couple of URL paths and handlers:
 
 ```go
 func main() {
-	r := mux.NewRouter()
-	r.HandleFunc("/", HomeHandler)
-	r.HandleFunc("/products", ProductsHandler)
-	r.HandleFunc("/articles", ArticlesHandler)
-	http.Handle("/", r)
+    r := mux.NewRouter()
+    r.HandleFunc("/", HomeHandler)
+    r.HandleFunc("/products", ProductsHandler)
+    r.HandleFunc("/articles", ArticlesHandler)
+    http.Handle("/", r)
 }
 ```
 
@@ -68,9 +71,9 @@ The names are used to create a map of route variables which can be retrieved cal
 
 ```go
 func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
-	vars := mux.Vars(r)
-	w.WriteHeader(http.StatusOK)
-	fmt.Fprintf(w, "Category: %v\n", vars["category"])
+    vars := mux.Vars(r)
+    w.WriteHeader(http.StatusOK)
+    fmt.Fprintf(w, "Category: %v\n", vars["category"])
 }
 ```
 
@@ -122,7 +125,7 @@ r.Queries("key", "value")
 
 ```go
 r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
-	return r.ProtoMajor == 0
+    return r.ProtoMajor == 0
 })
 ```
 
@@ -135,6 +138,14 @@ r.HandleFunc("/products", ProductsHandler).
   Schemes("http")
 ```
 
+Routes are tested in the order they were added to the router. If two routes match, the first one wins:
+
+```go
+r := mux.NewRouter()
+r.HandleFunc("/specific", specificHandler)
+r.PathPrefix("/").Handler(catchAllHandler)
+```
+
 Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting".
 
 For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it:
@@ -168,79 +179,34 @@ s.HandleFunc("/{key}/", ProductHandler)
 // "/products/{key}/details"
 s.HandleFunc("/{key}/details", ProductDetailsHandler)
 ```
-### Listing Routes
-
-Routes on a mux can be listed using the Router.Walk method—useful for generating documentation:
 
-```go
-package main
-
-import (
-    "fmt"
-    "net/http"
-    "strings"
-
-    "github.com/gorilla/mux"
-)
-
-func handler(w http.ResponseWriter, r *http.Request) {
-    return
-}
-
-func main() {
-    r := mux.NewRouter()
-    r.HandleFunc("/", handler)
-    r.HandleFunc("/products", handler).Methods("POST")
-    r.HandleFunc("/articles", handler).Methods("GET")
-    r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
-    r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
-        t, err := route.GetPathTemplate()
-        if err != nil {
-            return err
-        }
-        // p will contain regular expression is compatible with regular expression in Perl, Python, and other languages.
-        // for instance the regular expression for path '/articles/{id}' will be '^/articles/(?P<v0>[^/]+)$'
-        p, err := route.GetPathRegexp()
-        if err != nil {
-            return err
-        }
-        m, err := route.GetMethods()
-        if err != nil {
-            return err
-        }
-        fmt.Println(strings.Join(m, ","), t, p)
-        return nil
-    })
-    http.Handle("/", r)
-}
-```
 
 ### Static Files
 
 Note that the path provided to `PathPrefix()` represents a "wildcard": calling
 `PathPrefix("/static/").Handler(...)` means that the handler will be passed any
-request that matches "/static/*". This makes it easy to serve static files with mux:
+request that matches "/static/\*". This makes it easy to serve static files with mux:
 
 ```go
 func main() {
-	var dir string
+    var dir string
 
-	flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
-	flag.Parse()
-	r := mux.NewRouter()
+    flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
+    flag.Parse()
+    r := mux.NewRouter()
 
-	// This will serve files under http://localhost:8000/static/<filename>
-	r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
+    // This will serve files under http://localhost:8000/static/<filename>
+    r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
 
-	srv := &http.Server{
-		Handler:      r,
-		Addr:         "127.0.0.1:8000",
-		// Good practice: enforce timeouts for servers you create!
-		WriteTimeout: 15 * time.Second,
-		ReadTimeout:  15 * time.Second,
-	}
+    srv := &http.Server{
+        Handler:      r,
+        Addr:         "127.0.0.1:8000",
+        // Good practice: enforce timeouts for servers you create!
+        WriteTimeout: 15 * time.Second,
+        ReadTimeout:  15 * time.Second,
+    }
 
-	log.Fatal(srv.ListenAndServe())
+    log.Fatal(srv.ListenAndServe())
 }
 ```
 
@@ -325,30 +291,330 @@ url, err := r.Get("article").URL("subdomain", "news",
 The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example,
 the following prints all of the registered routes:
 
+```go
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/gorilla/mux"
+)
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	return
+}
+
+func main() {
+	r := mux.NewRouter()
+	r.HandleFunc("/", handler)
+	r.HandleFunc("/products", handler).Methods("POST")
+	r.HandleFunc("/articles", handler).Methods("GET")
+	r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
+	r.HandleFunc("/authors", handler).Queries("surname", "{surname}")
+	err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
+		pathTemplate, err := route.GetPathTemplate()
+		if err == nil {
+			fmt.Println("ROUTE:", pathTemplate)
+		}
+		pathRegexp, err := route.GetPathRegexp()
+		if err == nil {
+			fmt.Println("Path regexp:", pathRegexp)
+		}
+		queriesTemplates, err := route.GetQueriesTemplates()
+		if err == nil {
+			fmt.Println("Queries templates:", strings.Join(queriesTemplates, ","))
+		}
+		queriesRegexps, err := route.GetQueriesRegexp()
+		if err == nil {
+			fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ","))
+		}
+		methods, err := route.GetMethods()
+		if err == nil {
+			fmt.Println("Methods:", strings.Join(methods, ","))
+		}
+		fmt.Println()
+		return nil
+	})
+
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	http.Handle("/", r)
+}
+```
+
+### Graceful Shutdown
+
+Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`:
+
+```go
+package main
+
+import (
+    "context"
+    "flag"
+    "log"
+    "net/http"
+    "os"
+    "os/signal"
+    "time"
+
+    "github.com/gorilla/mux"
+)
+
+func main() {
+    var wait time.Duration
+    flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m")
+    flag.Parse()
+
+    r := mux.NewRouter()
+    // Add your routes as needed
+
+    srv := &http.Server{
+        Addr:         "0.0.0.0:8080",
+        // Good practice to set timeouts to avoid Slowloris attacks.
+        WriteTimeout: time.Second * 15,
+        ReadTimeout:  time.Second * 15,
+        IdleTimeout:  time.Second * 60,
+        Handler: r, // Pass our instance of gorilla/mux in.
+    }
+
+    // Run our server in a goroutine so that it doesn't block.
+    go func() {
+        if err := srv.ListenAndServe(); err != nil {
+            log.Println(err)
+        }
+    }()
+
+    c := make(chan os.Signal, 1)
+    // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
+    // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
+    signal.Notify(c, os.Interrupt)
+
+    // Block until we receive our signal.
+    <-c
+
+    // Create a deadline to wait for.
+    ctx, cancel := context.WithTimeout(context.Background(), wait)
+    defer cancel()
+    // Doesn't block if no connections, but will otherwise wait
+    // until the timeout deadline.
+    srv.Shutdown(ctx)
+    // Optionally, you could run srv.Shutdown in a goroutine and block on
+    // <-ctx.Done() if your application should wait for other services
+    // to finalize based on context cancellation.
+    log.Println("shutting down")
+    os.Exit(0)
+}
+```
+
+### Middleware
+
+Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters.
+Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking.
+
+Mux middlewares are defined using the de facto standard type:
+
+```go
+type MiddlewareFunc func(http.Handler) http.Handler
+```
+
+Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers.
+
+A very basic middleware which logs the URI of the request being handled could be written as:
+
+```go
+func loggingMiddleware(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        // Do stuff here
+        log.Println(r.RequestURI)
+        // Call the next handler, which can be another middleware in the chain, or the final handler.
+        next.ServeHTTP(w, r)
+    })
+}
+```
+
+Middlewares can be added to a router using `Router.Use()`:
+
 ```go
 r := mux.NewRouter()
 r.HandleFunc("/", handler)
-r.HandleFunc("/products", handler).Methods("POST")
-r.HandleFunc("/articles", handler).Methods("GET")
-r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
-r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
-    t, err := route.GetPathTemplate()
+r.Use(loggingMiddleware)
+```
+
+A more complex authentication middleware, which maps session token to users, could be written as:
+
+```go
+// Define our struct
+type authenticationMiddleware struct {
+	tokenUsers map[string]string
+}
+
+// Initialize it somewhere
+func (amw *authenticationMiddleware) Populate() {
+	amw.tokenUsers["00000000"] = "user0"
+	amw.tokenUsers["aaaaaaaa"] = "userA"
+	amw.tokenUsers["05f717e5"] = "randomUser"
+	amw.tokenUsers["deadbeef"] = "user0"
+}
+
+// Middleware function, which will be called for each request
+func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+        token := r.Header.Get("X-Session-Token")
+
+        if user, found := amw.tokenUsers[token]; found {
+        	// We found the token in our map
+        	log.Printf("Authenticated user %s\n", user)
+        	// Pass down the request to the next middleware (or final handler)
+        	next.ServeHTTP(w, r)
+        } else {
+        	// Write an error and stop the handler chain
+        	http.Error(w, "Forbidden", http.StatusForbidden)
+        }
+    })
+}
+```
+
+```go
+r := mux.NewRouter()
+r.HandleFunc("/", handler)
+
+amw := authenticationMiddleware{}
+amw.Populate()
+
+r.Use(amw.Middleware)
+```
+
+Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it.
+
+### Testing Handlers
+
+Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_.
+
+First, our simple HTTP handler:
+
+```go
+// endpoints.go
+package main
+
+func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
+    // A very simple health check.
+    w.WriteHeader(http.StatusOK)
+    w.Header().Set("Content-Type", "application/json")
+
+    // In the future we could report back on the status of our DB, or our cache
+    // (e.g. Redis) by performing a simple PING, and include them in the response.
+    io.WriteString(w, `{"alive": true}`)
+}
+
+func main() {
+    r := mux.NewRouter()
+    r.HandleFunc("/health", HealthCheckHandler)
+
+    log.Fatal(http.ListenAndServe("localhost:8080", r))
+}
+```
+
+Our test code:
+
+```go
+// endpoints_test.go
+package main
+
+import (
+    "net/http"
+    "net/http/httptest"
+    "testing"
+)
+
+func TestHealthCheckHandler(t *testing.T) {
+    // Create a request to pass to our handler. We don't have any query parameters for now, so we'll
+    // pass 'nil' as the third parameter.
+    req, err := http.NewRequest("GET", "/health", nil)
     if err != nil {
-        return err
+        t.Fatal(err)
     }
-    // p will contain a regular expression that is compatible with regular expressions in Perl, Python, and other languages.
-    // For example, the regular expression for path '/articles/{id}' will be '^/articles/(?P<v0>[^/]+)$'.
-    p, err := route.GetPathRegexp()
-    if err != nil {
-        return err
+
+    // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
+    rr := httptest.NewRecorder()
+    handler := http.HandlerFunc(HealthCheckHandler)
+
+    // Our handlers satisfy http.Handler, so we can call their ServeHTTP method
+    // directly and pass in our Request and ResponseRecorder.
+    handler.ServeHTTP(rr, req)
+
+    // Check the status code is what we expect.
+    if status := rr.Code; status != http.StatusOK {
+        t.Errorf("handler returned wrong status code: got %v want %v",
+            status, http.StatusOK)
     }
-    m, err := route.GetMethods()
-    if err != nil {
-        return err
+
+    // Check the response body is what we expect.
+    expected := `{"alive": true}`
+    if rr.Body.String() != expected {
+        t.Errorf("handler returned unexpected body: got %v want %v",
+            rr.Body.String(), expected)
     }
-    fmt.Println(strings.Join(m, ","), t, p)
-    return nil
-})
+}
+```
+
+In the case that our routes have [variables](#examples), we can pass those in the request. We could write
+[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple
+possible route variables as needed.
+
+```go
+// endpoints.go
+func main() {
+    r := mux.NewRouter()
+    // A route with a route variable:
+    r.HandleFunc("/metrics/{type}", MetricsHandler)
+
+    log.Fatal(http.ListenAndServe("localhost:8080", r))
+}
+```
+
+Our test file, with a table-driven test of `routeVariables`:
+
+```go
+// endpoints_test.go
+func TestMetricsHandler(t *testing.T) {
+    tt := []struct{
+        routeVariable string
+        shouldPass bool
+    }{
+        {"goroutines", true},
+        {"heap", true},
+        {"counters", true},
+        {"queries", true},
+        {"adhadaeqm3k", false},
+    }
+
+    for _, tc := range tt {
+        path := fmt.Sprintf("/metrics/%s", tc.routeVariable)
+        req, err := http.NewRequest("GET", path, nil)
+        if err != nil {
+            t.Fatal(err)
+        }
+
+        rr := httptest.NewRecorder()
+	
+	// Need to create a router that we can pass the request through so that the vars will be added to the context
+	router := mux.NewRouter()
+        router.HandleFunc("/metrics/{type}", MetricsHandler)
+        router.ServeHTTP(rr, req)
+
+        // In this case, our MetricsHandler returns a non-200 response
+        // for a route variable it doesn't know about.
+        if rr.Code == http.StatusOK && !tc.shouldPass {
+            t.Errorf("handler should have failed on routeVariable %s: got %v want %v",
+                tc.routeVariable, rr.Code, http.StatusOK)
+        }
+    }
+}
 ```
 
 ## Full Example
@@ -359,22 +625,22 @@ Here's a complete, runnable example of a small `mux` based server:
 package main
 
 import (
-	"net/http"
-	"log"
-	"github.com/gorilla/mux"
+    "net/http"
+    "log"
+    "github.com/gorilla/mux"
 )
 
 func YourHandler(w http.ResponseWriter, r *http.Request) {
-	w.Write([]byte("Gorilla!\n"))
+    w.Write([]byte("Gorilla!\n"))
 }
 
 func main() {
-	r := mux.NewRouter()
-	// Routes consist of a path and a handler function.
-	r.HandleFunc("/", YourHandler)
+    r := mux.NewRouter()
+    // Routes consist of a path and a handler function.
+    r.HandleFunc("/", YourHandler)
 
-	// Bind to a port and pass our router in
-	log.Fatal(http.ListenAndServe(":8000", r))
+    // Bind to a port and pass our router in
+    log.Fatal(http.ListenAndServe(":8000", r))
 }
 ```
 
diff --git a/vendor/github.com/gorilla/mux/context_native.go b/vendor/github.com/gorilla/mux/context.go
similarity index 95%
rename from vendor/github.com/gorilla/mux/context_native.go
rename to vendor/github.com/gorilla/mux/context.go
index 209cbea7d66170098a4c743f6e8746595e59f60f..13a4601ae7c876e7401ac24f37948774f8aceb6d 100644
--- a/vendor/github.com/gorilla/mux/context_native.go
+++ b/vendor/github.com/gorilla/mux/context.go
@@ -1,5 +1,3 @@
-// +build go1.7
-
 package mux
 
 import (
diff --git a/vendor/github.com/gorilla/mux/context_gorilla.go b/vendor/github.com/gorilla/mux/context_gorilla.go
deleted file mode 100644
index d7adaa8fad4fa8ce62d18a7058d10723ff2288af..0000000000000000000000000000000000000000
--- a/vendor/github.com/gorilla/mux/context_gorilla.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// +build !go1.7
-
-package mux
-
-import (
-	"net/http"
-
-	"github.com/gorilla/context"
-)
-
-func contextGet(r *http.Request, key interface{}) interface{} {
-	return context.Get(r, key)
-}
-
-func contextSet(r *http.Request, key, val interface{}) *http.Request {
-	if val == nil {
-		return r
-	}
-
-	context.Set(r, key, val)
-	return r
-}
-
-func contextClear(r *http.Request) {
-	context.Clear(r)
-}
diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go
index cce30b2f04c7b062c768a1c35dc3f7ff83e70778..38957deead3d73b4cbf24fca337bbf8a6dfa03cc 100644
--- a/vendor/github.com/gorilla/mux/doc.go
+++ b/vendor/github.com/gorilla/mux/doc.go
@@ -238,5 +238,69 @@ as well:
 	url, err := r.Get("article").URL("subdomain", "news",
 	                                 "category", "technology",
 	                                 "id", "42")
+
+Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking.
+
+	type MiddlewareFunc func(http.Handler) http.Handler
+
+Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created).
+
+A very basic middleware which logs the URI of the request being handled could be written as:
+
+	func simpleMw(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			// Do stuff here
+			log.Println(r.RequestURI)
+			// Call the next handler, which can be another middleware in the chain, or the final handler.
+			next.ServeHTTP(w, r)
+		})
+	}
+
+Middlewares can be added to a router using `Router.Use()`:
+
+	r := mux.NewRouter()
+	r.HandleFunc("/", handler)
+	r.Use(simpleMw)
+
+A more complex authentication middleware, which maps session token to users, could be written as:
+
+	// Define our struct
+	type authenticationMiddleware struct {
+		tokenUsers map[string]string
+	}
+
+	// Initialize it somewhere
+	func (amw *authenticationMiddleware) Populate() {
+		amw.tokenUsers["00000000"] = "user0"
+		amw.tokenUsers["aaaaaaaa"] = "userA"
+		amw.tokenUsers["05f717e5"] = "randomUser"
+		amw.tokenUsers["deadbeef"] = "user0"
+	}
+
+	// Middleware function, which will be called for each request
+	func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			token := r.Header.Get("X-Session-Token")
+
+			if user, found := amw.tokenUsers[token]; found {
+				// We found the token in our map
+				log.Printf("Authenticated user %s\n", user)
+				next.ServeHTTP(w, r)
+			} else {
+				http.Error(w, "Forbidden", http.StatusForbidden)
+			}
+		})
+	}
+
+	r := mux.NewRouter()
+	r.HandleFunc("/", handler)
+
+	amw := authenticationMiddleware{}
+	amw.Populate()
+
+	r.Use(amw.Middleware)
+
+Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to.
+
 */
 package mux
diff --git a/vendor/github.com/gorilla/mux/go.mod b/vendor/github.com/gorilla/mux/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..cfc8ede5818b9b09b510b5a34729054060b28656
--- /dev/null
+++ b/vendor/github.com/gorilla/mux/go.mod
@@ -0,0 +1 @@
+module github.com/gorilla/mux
diff --git a/vendor/github.com/gorilla/mux/middleware.go b/vendor/github.com/gorilla/mux/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..ceb812cee284493bde4d614b933a49d3f5cd425f
--- /dev/null
+++ b/vendor/github.com/gorilla/mux/middleware.go
@@ -0,0 +1,72 @@
+package mux
+
+import (
+	"net/http"
+	"strings"
+)
+
+// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler.
+// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed
+// to it, and then calls the handler passed as parameter to the MiddlewareFunc.
+type MiddlewareFunc func(http.Handler) http.Handler
+
+// middleware interface is anything which implements a MiddlewareFunc named Middleware.
+type middleware interface {
+	Middleware(handler http.Handler) http.Handler
+}
+
+// Middleware allows MiddlewareFunc to implement the middleware interface.
+func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler {
+	return mw(handler)
+}
+
+// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.
+func (r *Router) Use(mwf ...MiddlewareFunc) {
+	for _, fn := range mwf {
+		r.middlewares = append(r.middlewares, fn)
+	}
+}
+
+// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.
+func (r *Router) useInterface(mw middleware) {
+	r.middlewares = append(r.middlewares, mw)
+}
+
+// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header
+// on a request, by matching routes based only on paths. It also handles
+// OPTIONS requests, by settings Access-Control-Allow-Methods, and then
+// returning without calling the next http handler.
+func CORSMethodMiddleware(r *Router) MiddlewareFunc {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			var allMethods []string
+
+			err := r.Walk(func(route *Route, _ *Router, _ []*Route) error {
+				for _, m := range route.matchers {
+					if _, ok := m.(*routeRegexp); ok {
+						if m.Match(req, &RouteMatch{}) {
+							methods, err := route.GetMethods()
+							if err != nil {
+								return err
+							}
+
+							allMethods = append(allMethods, methods...)
+						}
+						break
+					}
+				}
+				return nil
+			})
+
+			if err == nil {
+				w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ","))
+
+				if req.Method == "OPTIONS" {
+					return
+				}
+			}
+
+			next.ServeHTTP(w, req)
+		})
+	}
+}
diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go
index fb69196dbd2094009ad0b9d5de99f9e813b29960..4bbafa51da301690f53a011964769d4eac6d7c84 100644
--- a/vendor/github.com/gorilla/mux/mux.go
+++ b/vendor/github.com/gorilla/mux/mux.go
@@ -10,11 +10,14 @@ import (
 	"net/http"
 	"path"
 	"regexp"
-	"strings"
 )
 
 var (
+	// ErrMethodMismatch is returned when the method in the request does not match
+	// the method defined against the route.
 	ErrMethodMismatch = errors.New("method is not allowed")
+	// ErrNotFound is returned when no route match is found.
+	ErrNotFound = errors.New("no matching route was found")
 )
 
 // NewRouter returns a new router instance.
@@ -63,26 +66,51 @@ type Router struct {
 	KeepContext bool
 	// see Router.UseEncodedPath(). This defines a flag for all routes.
 	useEncodedPath bool
+	// Slice of middlewares to be called after a match is found
+	middlewares []middleware
 }
 
-// Match matches registered routes against the request.
+// Match attempts to match the given request against the router's registered routes.
+//
+// If the request matches a route of this router or one of its subrouters the Route,
+// Handler, and Vars fields of the the match argument are filled and this function
+// returns true.
+//
+// If the request does not match any of this router's or its subrouters' routes
+// then this function returns false. If available, a reason for the match failure
+// will be filled in the match argument's MatchErr field. If the match failure type
+// (eg: not found) has a registered handler, the handler is assigned to the Handler
+// field of the match argument.
 func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
 	for _, route := range r.routes {
 		if route.Match(req, match) {
+			// Build middleware chain if no error was found
+			if match.MatchErr == nil {
+				for i := len(r.middlewares) - 1; i >= 0; i-- {
+					match.Handler = r.middlewares[i].Middleware(match.Handler)
+				}
+			}
 			return true
 		}
 	}
 
-	if match.MatchErr == ErrMethodMismatch && r.MethodNotAllowedHandler != nil {
-		match.Handler = r.MethodNotAllowedHandler
-		return true
+	if match.MatchErr == ErrMethodMismatch {
+		if r.MethodNotAllowedHandler != nil {
+			match.Handler = r.MethodNotAllowedHandler
+			return true
+		}
+
+		return false
 	}
 
 	// Closest match for a router (includes sub-routers)
 	if r.NotFoundHandler != nil {
 		match.Handler = r.NotFoundHandler
+		match.MatchErr = ErrNotFound
 		return true
 	}
+
+	match.MatchErr = ErrNotFound
 	return false
 }
 
@@ -94,7 +122,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	if !r.skipClean {
 		path := req.URL.Path
 		if r.useEncodedPath {
-			path = getPath(req)
+			path = req.URL.EscapedPath()
 		}
 		// Clean path to canonical form and redirect.
 		if p := cleanPath(path); p != path {
@@ -130,6 +158,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	if !r.KeepContext {
 		defer contextClear(req)
 	}
+
 	handler.ServeHTTP(w, req)
 }
 
@@ -147,13 +176,18 @@ func (r *Router) GetRoute(name string) *Route {
 // StrictSlash defines the trailing slash behavior for new routes. The initial
 // value is false.
 //
-// When true, if the route path is "/path/", accessing "/path" will redirect
+// When true, if the route path is "/path/", accessing "/path" will perform a redirect
 // to the former and vice versa. In other words, your application will always
 // see the path as specified in the route.
 //
 // When false, if the route path is "/path", accessing "/path/" will not match
 // this route and vice versa.
 //
+// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for
+// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed
+// request will be made as a GET by most clients. Use middleware or client settings
+// to modify this behaviour as needed.
+//
 // Special case: when a route sets a path prefix using the PathPrefix() method,
 // strict slash is ignored for that route because the redirect behavior can't
 // be determined from a prefix alone. However, any subrouters created from that
@@ -179,10 +213,6 @@ func (r *Router) SkipClean(value bool) *Router {
 // UseEncodedPath tells the router to match the encoded original path
 // to the routes.
 // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
-// This behavior has the drawback of needing to match routes against
-// r.RequestURI instead of r.URL.Path. Any modifications (such as http.StripPrefix)
-// to r.URL.Path will not affect routing when this flag is on and thus may
-// induce unintended behavior.
 //
 // If not called, the router will match the unencoded path to the routes.
 // For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to"
@@ -409,28 +439,6 @@ func setCurrentRoute(r *http.Request, val interface{}) *http.Request {
 // Helpers
 // ----------------------------------------------------------------------------
 
-// getPath returns the escaped path if possible; doing what URL.EscapedPath()
-// which was added in go1.5 does
-func getPath(req *http.Request) string {
-	if req.RequestURI != "" {
-		// Extract the path from RequestURI (which is escaped unlike URL.Path)
-		// as detailed here as detailed in https://golang.org/pkg/net/url/#URL
-		// for < 1.5 server side workaround
-		// http://localhost/path/here?v=1 -> /path/here
-		path := req.RequestURI
-		path = strings.TrimPrefix(path, req.URL.Scheme+`://`)
-		path = strings.TrimPrefix(path, req.URL.Host)
-		if i := strings.LastIndex(path, "?"); i > -1 {
-			path = path[:i]
-		}
-		if i := strings.LastIndex(path, "#"); i > -1 {
-			path = path[:i]
-		}
-		return path
-	}
-	return req.URL.Path
-}
-
 // cleanPath returns the canonical path for p, eliminating . and .. elements.
 // Borrowed from the net/http package.
 func cleanPath(p string) string {
diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go
index 80d1f785804e996282c703c26f80c641119acd08..b92d59f208eff34a4835e899939e22acc255950e 100644
--- a/vendor/github.com/gorilla/mux/regexp.go
+++ b/vendor/github.com/gorilla/mux/regexp.go
@@ -14,6 +14,20 @@ import (
 	"strings"
 )
 
+type routeRegexpOptions struct {
+	strictSlash    bool
+	useEncodedPath bool
+}
+
+type regexpType int
+
+const (
+	regexpTypePath   regexpType = 0
+	regexpTypeHost   regexpType = 1
+	regexpTypePrefix regexpType = 2
+	regexpTypeQuery  regexpType = 3
+)
+
 // newRouteRegexp parses a route template and returns a routeRegexp,
 // used to match a host, a path or a query string.
 //
@@ -24,7 +38,7 @@ import (
 // Previously we accepted only Python-like identifiers for variable
 // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
 // name and pattern can't be empty, and names can't contain a colon.
-func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash, useEncodedPath bool) (*routeRegexp, error) {
+func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) {
 	// Check if it is well-formed.
 	idxs, errBraces := braceIndices(tpl)
 	if errBraces != nil {
@@ -34,19 +48,18 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
 	template := tpl
 	// Now let's parse it.
 	defaultPattern := "[^/]+"
-	if matchQuery {
+	if typ == regexpTypeQuery {
 		defaultPattern = ".*"
-	} else if matchHost {
+	} else if typ == regexpTypeHost {
 		defaultPattern = "[^.]+"
-		matchPrefix = false
 	}
 	// Only match strict slash if not matching
-	if matchPrefix || matchHost || matchQuery {
-		strictSlash = false
+	if typ != regexpTypePath {
+		options.strictSlash = false
 	}
 	// Set a flag for strictSlash.
 	endSlash := false
-	if strictSlash && strings.HasSuffix(tpl, "/") {
+	if options.strictSlash && strings.HasSuffix(tpl, "/") {
 		tpl = tpl[:len(tpl)-1]
 		endSlash = true
 	}
@@ -88,16 +101,16 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
 	// Add the remaining.
 	raw := tpl[end:]
 	pattern.WriteString(regexp.QuoteMeta(raw))
-	if strictSlash {
+	if options.strictSlash {
 		pattern.WriteString("[/]?")
 	}
-	if matchQuery {
+	if typ == regexpTypeQuery {
 		// Add the default pattern if the query value is empty
 		if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
 			pattern.WriteString(defaultPattern)
 		}
 	}
-	if !matchPrefix {
+	if typ != regexpTypePrefix {
 		pattern.WriteByte('$')
 	}
 	reverse.WriteString(raw)
@@ -118,15 +131,13 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
 
 	// Done!
 	return &routeRegexp{
-		template:       template,
-		matchHost:      matchHost,
-		matchQuery:     matchQuery,
-		strictSlash:    strictSlash,
-		useEncodedPath: useEncodedPath,
-		regexp:         reg,
-		reverse:        reverse.String(),
-		varsN:          varsN,
-		varsR:          varsR,
+		template:   template,
+		regexpType: typ,
+		options:    options,
+		regexp:     reg,
+		reverse:    reverse.String(),
+		varsN:      varsN,
+		varsR:      varsR,
 	}, nil
 }
 
@@ -135,15 +146,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash,
 type routeRegexp struct {
 	// The unmodified template.
 	template string
-	// True for host match, false for path or query string match.
-	matchHost bool
-	// True for query string match, false for path and host match.
-	matchQuery bool
-	// The strictSlash value defined on the route, but disabled if PathPrefix was used.
-	strictSlash bool
-	// Determines whether to use encoded path from getPath function or unencoded
-	// req.URL.Path for path matching
-	useEncodedPath bool
+	// The type of match
+	regexpType regexpType
+	// Options for matching
+	options routeRegexpOptions
 	// Expanded regexp.
 	regexp *regexp.Regexp
 	// Reverse template.
@@ -156,13 +162,13 @@ type routeRegexp struct {
 
 // Match matches the regexp against the URL host or path.
 func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
-	if !r.matchHost {
-		if r.matchQuery {
+	if r.regexpType != regexpTypeHost {
+		if r.regexpType == regexpTypeQuery {
 			return r.matchQueryString(req)
 		}
 		path := req.URL.Path
-		if r.useEncodedPath {
-			path = getPath(req)
+		if r.options.useEncodedPath {
+			path = req.URL.EscapedPath()
 		}
 		return r.regexp.MatchString(path)
 	}
@@ -178,7 +184,7 @@ func (r *routeRegexp) url(values map[string]string) (string, error) {
 		if !ok {
 			return "", fmt.Errorf("mux: missing route variable %q", v)
 		}
-		if r.matchQuery {
+		if r.regexpType == regexpTypeQuery {
 			value = url.QueryEscape(value)
 		}
 		urlValues[k] = value
@@ -203,7 +209,7 @@ func (r *routeRegexp) url(values map[string]string) (string, error) {
 // For a URL with foo=bar&baz=ding, we return only the relevant key
 // value pair for the routeRegexp.
 func (r *routeRegexp) getURLQuery(req *http.Request) string {
-	if !r.matchQuery {
+	if r.regexpType != regexpTypeQuery {
 		return ""
 	}
 	templateKey := strings.SplitN(r.template, "=", 2)[0]
@@ -272,7 +278,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
 	}
 	path := req.URL.Path
 	if r.useEncodedPath {
-		path = getPath(req)
+		path = req.URL.EscapedPath()
 	}
 	// Store path variables.
 	if v.path != nil {
@@ -280,7 +286,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
 		if len(matches) > 0 {
 			extractVars(path, matches, v.path.varsN, m.Vars)
 			// Check if we should redirect.
-			if v.path.strictSlash {
+			if v.path.options.strictSlash {
 				p1 := strings.HasSuffix(path, "/")
 				p2 := strings.HasSuffix(v.path.template, "/")
 				if p1 != p2 {
@@ -290,7 +296,7 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
 					} else {
 						u.Path += "/"
 					}
-					m.Handler = http.RedirectHandler(u.String(), 301)
+					m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently)
 				}
 			}
 		}
diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go
index 6863adba50e60f895a32f949c6225892f9e62673..c8bb5c7e3a0d6331871879ed2c46ca952e34fb14 100644
--- a/vendor/github.com/gorilla/mux/route.go
+++ b/vendor/github.com/gorilla/mux/route.go
@@ -43,6 +43,8 @@ type Route struct {
 	buildVarsFunc BuildVarsFunc
 }
 
+// SkipClean reports whether path cleaning is enabled for this route via
+// Router.SkipClean.
 func (r *Route) SkipClean() bool {
 	return r.skipClean
 }
@@ -72,7 +74,13 @@ func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
 		return false
 	}
 
-	match.MatchErr = nil
+	if match.MatchErr == ErrMethodMismatch {
+		// We found a route which matches request method, clear MatchErr
+		match.MatchErr = nil
+		// Then override the mis-matched handler
+		match.Handler = r.handler
+	}
+
 	// Yay, we have a match. Let's collect some info about it.
 	if match.Route == nil {
 		match.Route = r
@@ -129,7 +137,7 @@ func (r *Route) GetHandler() http.Handler {
 // Name -----------------------------------------------------------------------
 
 // Name sets the name for the route, used to build URLs.
-// If the name was registered already it will be overwritten.
+// It is an error to call Name more than once on a route.
 func (r *Route) Name(name string) *Route {
 	if r.name != "" {
 		r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
@@ -165,12 +173,12 @@ func (r *Route) addMatcher(m matcher) *Route {
 }
 
 // addRegexpMatcher adds a host or path matcher and builder to a route.
-func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error {
+func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
 	if r.err != nil {
 		return r.err
 	}
 	r.regexp = r.getRegexpGroup()
-	if !matchHost && !matchQuery {
+	if typ == regexpTypePath || typ == regexpTypePrefix {
 		if len(tpl) > 0 && tpl[0] != '/' {
 			return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
 		}
@@ -178,7 +186,10 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery
 			tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
 		}
 	}
-	rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash, r.useEncodedPath)
+	rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
+		strictSlash:    r.strictSlash,
+		useEncodedPath: r.useEncodedPath,
+	})
 	if err != nil {
 		return err
 	}
@@ -187,7 +198,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery
 			return err
 		}
 	}
-	if matchHost {
+	if typ == regexpTypeHost {
 		if r.regexp.path != nil {
 			if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
 				return err
@@ -200,7 +211,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery
 				return err
 			}
 		}
-		if matchQuery {
+		if typ == regexpTypeQuery {
 			r.regexp.queries = append(r.regexp.queries, rr)
 		} else {
 			r.regexp.path = rr
@@ -252,7 +263,8 @@ func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
 //               "X-Requested-With", "XMLHttpRequest")
 //
 // The above route will only match if both the request header matches both regular expressions.
-// It the value is an empty string, it will match any value if the key is set.
+// If the value is an empty string, it will match any value if the key is set.
+// Use the start and end of string anchors (^ and $) to match an exact value.
 func (r *Route) HeadersRegexp(pairs ...string) *Route {
 	if r.err == nil {
 		var headers map[string]*regexp.Regexp
@@ -282,7 +294,7 @@ func (r *Route) HeadersRegexp(pairs ...string) *Route {
 // Variable names must be unique in a given route. They can be retrieved
 // calling mux.Vars(request).
 func (r *Route) Host(tpl string) *Route {
-	r.err = r.addRegexpMatcher(tpl, true, false, false)
+	r.err = r.addRegexpMatcher(tpl, regexpTypeHost)
 	return r
 }
 
@@ -342,7 +354,7 @@ func (r *Route) Methods(methods ...string) *Route {
 // Variable names must be unique in a given route. They can be retrieved
 // calling mux.Vars(request).
 func (r *Route) Path(tpl string) *Route {
-	r.err = r.addRegexpMatcher(tpl, false, false, false)
+	r.err = r.addRegexpMatcher(tpl, regexpTypePath)
 	return r
 }
 
@@ -358,7 +370,7 @@ func (r *Route) Path(tpl string) *Route {
 // Also note that the setting of Router.StrictSlash() has no effect on routes
 // with a PathPrefix matcher.
 func (r *Route) PathPrefix(tpl string) *Route {
-	r.err = r.addRegexpMatcher(tpl, false, true, false)
+	r.err = r.addRegexpMatcher(tpl, regexpTypePrefix)
 	return r
 }
 
@@ -389,7 +401,7 @@ func (r *Route) Queries(pairs ...string) *Route {
 		return nil
 	}
 	for i := 0; i < length; i += 2 {
-		if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil {
+		if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil {
 			return r
 		}
 	}
@@ -608,10 +620,48 @@ func (r *Route) GetPathRegexp() (string, error) {
 	return r.regexp.path.regexp.String(), nil
 }
 
+// GetQueriesRegexp returns the expanded regular expressions used to match the
+// route queries.
+// This is useful for building simple REST API documentation and for instrumentation
+// against third-party services.
+// An error will be returned if the route does not have queries.
+func (r *Route) GetQueriesRegexp() ([]string, error) {
+	if r.err != nil {
+		return nil, r.err
+	}
+	if r.regexp == nil || r.regexp.queries == nil {
+		return nil, errors.New("mux: route doesn't have queries")
+	}
+	var queries []string
+	for _, query := range r.regexp.queries {
+		queries = append(queries, query.regexp.String())
+	}
+	return queries, nil
+}
+
+// GetQueriesTemplates returns the templates used to build the
+// query matching.
+// This is useful for building simple REST API documentation and for instrumentation
+// against third-party services.
+// An error will be returned if the route does not define queries.
+func (r *Route) GetQueriesTemplates() ([]string, error) {
+	if r.err != nil {
+		return nil, r.err
+	}
+	if r.regexp == nil || r.regexp.queries == nil {
+		return nil, errors.New("mux: route doesn't have queries")
+	}
+	var queries []string
+	for _, query := range r.regexp.queries {
+		queries = append(queries, query.template)
+	}
+	return queries, nil
+}
+
 // GetMethods returns the methods the route matches against
 // This is useful for building simple REST API documentation and for instrumentation
 // against third-party services.
-// An empty list will be returned if route does not have methods.
+// An error will be returned if route does not have methods.
 func (r *Route) GetMethods() ([]string, error) {
 	if r.err != nil {
 		return nil, r.err
@@ -621,7 +671,7 @@ func (r *Route) GetMethods() ([]string, error) {
 			return []string(methods), nil
 		}
 	}
-	return nil, nil
+	return nil, errors.New("mux: route doesn't have methods")
 }
 
 // GetHostTemplate returns the template used to build the
diff --git a/vendor/github.com/gorilla/mux/test_helpers.go b/vendor/github.com/gorilla/mux/test_helpers.go
new file mode 100644
index 0000000000000000000000000000000000000000..32ecffde489f43aa0a73ff86e151cca3373daae6
--- /dev/null
+++ b/vendor/github.com/gorilla/mux/test_helpers.go
@@ -0,0 +1,19 @@
+// 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 mux
+
+import "net/http"
+
+// SetURLVars sets the URL variables for the given request, to be accessed via
+// mux.Vars for testing route behaviour. Arguments are not modified, a shallow
+// copy is returned.
+//
+// This API should only be used for testing purposes; it provides a way to
+// inject variables into the request context. Alternatively, URL variables
+// can be set by making a route that captures the required variables,
+// starting a server and sending the request to that server.
+func SetURLVars(r *http.Request, val map[string]string) *http.Request {
+	return setVars(r, val)
+}
diff --git a/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md
index a914d4ab3c46418990dc60bcaa7de296be961ae2..f416daafe99bd283c0ba47d29b522a2ef6f925cf 100644
--- a/vendor/github.com/gorilla/securecookie/README.md
+++ b/vendor/github.com/gorilla/securecookie/README.md
@@ -77,6 +77,64 @@ registered first using gob.Register(). For basic types this is not needed;
 it works out of the box. An optional JSON encoder that uses `encoding/json` is
 available for types compatible with JSON.
 
+### Key Rotation
+Rotating keys is an important part of any security strategy. The `EncodeMulti` and
+`DecodeMulti` functions allow for multiple keys to be rotated in and out.
+For example, let's take a system that stores keys in a map:
+
+```go
+// keys stored in a map will not be persisted between restarts
+// a more persistent storage should be considered for production applications.
+var cookies = map[string]*securecookie.SecureCookie{
+	"previous": securecookie.New(
+		securecookie.GenerateRandomKey(64),
+		securecookie.GenerateRandomKey(32),
+	),
+	"current": securecookie.New(
+		securecookie.GenerateRandomKey(64),
+		securecookie.GenerateRandomKey(32),
+	),
+}
+```
+
+Using the current key to encode new cookies:
+```go
+func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
+	value := map[string]string{
+		"foo": "bar",
+	}
+	if encoded, err := securecookie.EncodeMulti("cookie-name", value, cookies["current"]); err == nil {
+		cookie := &http.Cookie{
+			Name:  "cookie-name",
+			Value: encoded,
+			Path:  "/",
+		}
+		http.SetCookie(w, cookie)
+	}
+}
+```
+
+Later, decode cookies. Check against all valid keys:
+```go
+func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
+	if cookie, err := r.Cookie("cookie-name"); err == nil {
+		value := make(map[string]string)
+		err = securecookie.DecodeMulti("cookie-name", cookie.Value, &value, cookies["current"], cookies["previous"])
+		if err == nil {
+			fmt.Fprintf(w, "The value of foo is %q", value["foo"])
+		}
+	}
+}
+```
+
+Rotate the keys. This strategy allows previously issued cookies to be valid until the next rotation:
+```go
+func Rotate(newCookie *securecookie.SecureCookie) {
+	cookies["previous"] = cookies["current"]
+	cookies["current"] = newCookie
+}
+```
+
 ## License
 
 BSD licensed. See the LICENSE file for details.
diff --git a/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go
index a34f851284840dce8f09e34d038f3a05597dbfd0..b718ce98f88216c223360d52cf71f9d3f55d1fdb 100644
--- a/vendor/github.com/gorilla/securecookie/securecookie.go
+++ b/vendor/github.com/gorilla/securecookie/securecookie.go
@@ -124,7 +124,7 @@ type Codec interface {
 // GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes.
 //
 // blockKey is optional, used to encrypt values. Create it using
-// GenerateRandomKey(). The key length must correspond to the block size
+// GenerateRandomKey(). The key length must correspond to the key size
 // of the encryption algorithm. For AES, used by default, valid lengths are
 // 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
 // The default encoder used for cookie serialization is encoding/gob.
diff --git a/vendor/github.com/gorilla/sessions/AUTHORS b/vendor/github.com/gorilla/sessions/AUTHORS
new file mode 100644
index 0000000000000000000000000000000000000000..1e3e7acb629636728fb6f6f23bc6e5d16497db2a
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/AUTHORS
@@ -0,0 +1,43 @@
+# 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>
diff --git a/vendor/github.com/gorilla/sessions/LICENSE b/vendor/github.com/gorilla/sessions/LICENSE
index 0e5fb872800da9557f75a5650bb9d80c1c2cf715..6903df6386e98928a3236b87c84b71260c2541a6 100644
--- a/vendor/github.com/gorilla/sessions/LICENSE
+++ b/vendor/github.com/gorilla/sessions/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012 Rodrigo Moraes. All rights reserved.
+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
diff --git a/vendor/github.com/gorilla/sessions/README.md b/vendor/github.com/gorilla/sessions/README.md
index 10eb7f04471aa89047c6a9bd1b3a0dbeafa9c732..d4d70e9d1cd7231a000fdf8893ef5930b2929889 100644
--- a/vendor/github.com/gorilla/sessions/README.md
+++ b/vendor/github.com/gorilla/sessions/README.md
@@ -1,23 +1,22 @@
-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.png?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)
+# 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
+- 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
+- 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
+- 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:
@@ -28,7 +27,11 @@ Let's start with an example that shows the sessions API in a nutshell:
 		"github.com/gorilla/sessions"
 	)
 
-	var store = sessions.NewCookieStore([]byte("something-very-secret"))
+	// 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
@@ -38,7 +41,11 @@ Let's start with an example that shows the sessions API in a nutshell:
 		session.Values["foo"] = "bar"
 		session.Values[42] = 43
 		// Save it before we write to the response/return from the handler.
-		session.Save(r, w)
+		err = session.Save(r, w)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
 	}
 ```
 
@@ -48,42 +55,34 @@ secret key used to authenticate the session. Inside the handler, we call
 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.
 
-Important Note: If you aren't using gorilla/mux, you need to wrap your handlers
-with
-[`context.ClearHandler`](http://www.gorillatoolkit.org/pkg/context#ClearHandler)
-or else you will leak memory! An easy way to do this is to wrap the top-level
-mux when calling http.ListenAndServe:
-
-```go
-	http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux))
-```
-
-The ClearHandler function is provided by the gorilla/context package.
-
 More examples are available [on the Gorilla
-website](http://www.gorillatoolkit.org/pkg/sessions).
+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/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/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
 
diff --git a/vendor/github.com/gorilla/sessions/cookie.go b/vendor/github.com/gorilla/sessions/cookie.go
new file mode 100644
index 0000000000000000000000000000000000000000..1928b0471d9d259e6f18acf0ce96a8a42d78d306
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/cookie.go
@@ -0,0 +1,19 @@
+// +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,
+	}
+
+}
diff --git a/vendor/github.com/gorilla/sessions/cookie_go111.go b/vendor/github.com/gorilla/sessions/cookie_go111.go
new file mode 100644
index 0000000000000000000000000000000000000000..173d1a3ed12bb983164d7f3705ec927885a06e83
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/cookie_go111.go
@@ -0,0 +1,20 @@
+// +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,
+	}
+
+}
diff --git a/vendor/github.com/gorilla/sessions/doc.go b/vendor/github.com/gorilla/sessions/doc.go
index 591d932662b6b85d8b23c6c45ae7fadcaf8fb9a3..f4673cccf559e6c34a4b13ae6eb0fe16737c7c41 100644
--- a/vendor/github.com/gorilla/sessions/doc.go
+++ b/vendor/github.com/gorilla/sessions/doc.go
@@ -26,7 +26,12 @@ Let's start with an example that shows the sessions API in a nutshell:
 		"github.com/gorilla/sessions"
 	)
 
-	var store = sessions.NewCookieStore([]byte("something-very-secret"))
+	// 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.
@@ -40,7 +45,11 @@ Let's start with an example that shows the sessions API in a nutshell:
 		session.Values["foo"] = "bar"
 		session.Values[42] = 43
 		// Save it before we write to the response/return from the handler.
-		session.Save(r, w)
+		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
@@ -55,14 +64,6 @@ 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.
 
-Important Note: If you aren't using gorilla/mux, you need to wrap your handlers
-with context.ClearHandler as or else you will leak memory! An easy way to do this
-is to wrap the top-level mux when calling http.ListenAndServe:
-
-    http.ListenAndServe(":8080", context.ClearHandler(http.DefaultServeMux))
-
-The ClearHandler function is provided by the gorilla/context package.
-
 That's all you need to know for the basic usage. Let's take a look at other
 options, starting with flash messages.
 
@@ -79,14 +80,18 @@ flashes, call session.Flashes(). Here is an example:
 			return
 		}
 
-		// Get the previously flashes, if any.
+		// 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!")
 		}
-		session.Save(r, w)
+		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,
@@ -189,7 +194,11 @@ at once: it's sessions.Save(). Here's an example:
 		session2, _ := store.Get(r, "session-two")
 		session2.Values[42] = 43
 		// Save all sessions.
-		sessions.Save(r, w)
+		err = sessions.Save(r, w)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
 	}
 
 This is possible because when we call Get() from a session store, it adds the
diff --git a/vendor/github.com/gorilla/sessions/go.mod b/vendor/github.com/gorilla/sessions/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..9028bcf1c85e2483484fa781374774da85eeecc8
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/go.mod
@@ -0,0 +1,3 @@
+module github.com/gorilla/sessions
+
+require github.com/gorilla/securecookie v1.1.1
diff --git a/vendor/github.com/gorilla/sessions/go.sum b/vendor/github.com/gorilla/sessions/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..e6a7ed5f359b1341228321bd21eceea74152697d
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/go.sum
@@ -0,0 +1,2 @@
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
diff --git a/vendor/github.com/gorilla/sessions/options.go b/vendor/github.com/gorilla/sessions/options.go
new file mode 100644
index 0000000000000000000000000000000000000000..38ba72fb6c2f3be835510c6bd6a066bb061d3dc2
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/options.go
@@ -0,0 +1,18 @@
+// +build !go1.11
+
+package sessions
+
+// Options stores configuration for a session or session store.
+//
+// Fields are a subset of http.Cookie fields.
+type Options struct {
+	Path   string
+	Domain string
+	// MaxAge=0 means no Max-Age attribute specified and the cookie will be
+	// deleted after the browser session ends.
+	// MaxAge<0 means delete cookie immediately.
+	// MaxAge>0 means Max-Age attribute present and given in seconds.
+	MaxAge   int
+	Secure   bool
+	HttpOnly bool
+}
diff --git a/vendor/github.com/gorilla/sessions/options_go111.go b/vendor/github.com/gorilla/sessions/options_go111.go
new file mode 100644
index 0000000000000000000000000000000000000000..388112aad1c25a35e8bdcaf205add57a405d0b19
--- /dev/null
+++ b/vendor/github.com/gorilla/sessions/options_go111.go
@@ -0,0 +1,22 @@
+// +build go1.11
+
+package sessions
+
+import "net/http"
+
+// Options stores configuration for a session or session store.
+//
+// Fields are a subset of http.Cookie fields.
+type Options struct {
+	Path   string
+	Domain string
+	// MaxAge=0 means no Max-Age attribute specified and the cookie will be
+	// deleted after the browser session ends.
+	// MaxAge<0 means delete cookie immediately.
+	// MaxAge>0 means Max-Age attribute present and given in seconds.
+	MaxAge   int
+	Secure   bool
+	HttpOnly bool
+	// Defaults to http.SameSiteDefaultMode
+	SameSite http.SameSite
+}
diff --git a/vendor/github.com/gorilla/sessions/sessions.go b/vendor/github.com/gorilla/sessions/sessions.go
index fe0d2bc8fa4a3da5963f310b5cb3c6ae1855dee4..c052b289112da473c649455e4232d6ee99beaefc 100644
--- a/vendor/github.com/gorilla/sessions/sessions.go
+++ b/vendor/github.com/gorilla/sessions/sessions.go
@@ -5,41 +5,25 @@
 package sessions
 
 import (
+	"context"
 	"encoding/gob"
 	"fmt"
 	"net/http"
 	"time"
-
-	"github.com/gorilla/context"
 )
 
 // Default flashes key.
 const flashesKey = "_flash"
 
-// Options --------------------------------------------------------------------
-
-// Options stores configuration for a session or session store.
-//
-// Fields are a subset of http.Cookie fields.
-type Options struct {
-	Path   string
-	Domain string
-	// MaxAge=0 means no 'Max-Age' attribute specified.
-	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'.
-	// MaxAge>0 means Max-Age attribute present and given in seconds.
-	MaxAge   int
-	Secure   bool
-	HttpOnly bool
-}
-
 // Session --------------------------------------------------------------------
 
 // NewSession is called by session stores to create a new session instance.
 func NewSession(store Store, name string) *Session {
 	return &Session{
-		Values: make(map[interface{}]interface{}),
-		store:  store,
-		name:   name,
+		Values:  make(map[interface{}]interface{}),
+		store:   store,
+		name:    name,
+		Options: new(Options),
 	}
 }
 
@@ -123,7 +107,8 @@ const registryKey contextKey = 0
 
 // GetRegistry returns a registry instance for the current request.
 func GetRegistry(r *http.Request) *Registry {
-	registry := context.Get(r, registryKey)
+	var ctx = r.Context()
+	registry := ctx.Value(registryKey)
 	if registry != nil {
 		return registry.(*Registry)
 	}
@@ -131,7 +116,7 @@ func GetRegistry(r *http.Request) *Registry {
 		request:  r,
 		sessions: make(map[string]sessionInfo),
 	}
-	context.Set(r, registryKey, newRegistry)
+	*r = *r.WithContext(context.WithValue(ctx, registryKey, newRegistry))
 	return newRegistry
 }
 
@@ -193,15 +178,7 @@ func Save(r *http.Request, w http.ResponseWriter) error {
 // the Expires field calculated based on the MaxAge value, for Internet
 // Explorer compatibility.
 func NewCookie(name, value string, options *Options) *http.Cookie {
-	cookie := &http.Cookie{
-		Name:     name,
-		Value:    value,
-		Path:     options.Path,
-		Domain:   options.Domain,
-		MaxAge:   options.MaxAge,
-		Secure:   options.Secure,
-		HttpOnly: options.HttpOnly,
-	}
+	cookie := newCookieFromOptions(name, value, options)
 	if options.MaxAge > 0 {
 		d := time.Duration(options.MaxAge) * time.Second
 		cookie.Expires = time.Now().Add(d)
diff --git a/vendor/github.com/gorilla/sessions/store.go b/vendor/github.com/gorilla/sessions/store.go
index 4ff6b6c322c1759a092c8f3778ae9fca85e4018b..bb7f9647d6d15c8c17a13d4031b36f42aab1143f 100644
--- a/vendor/github.com/gorilla/sessions/store.go
+++ b/vendor/github.com/gorilla/sessions/store.go
@@ -47,9 +47,6 @@ type Store interface {
 // It is recommended to use an authentication key with 32 or 64 bytes.
 // The encryption key, if set, must be either 16, 24, or 32 bytes to select
 // AES-128, AES-192, or AES-256 modes.
-//
-// Use the convenience function securecookie.GenerateRandomKey() to create
-// strong keys.
 func NewCookieStore(keyPairs ...[]byte) *CookieStore {
 	cs := &CookieStore{
 		Codecs: securecookie.CodecsFromPairs(keyPairs...),
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 52c3fcf3c4622740a2c10a9021174911e548579b..02e412820d9f5bf6fa27aa7a57f88cb8babe59eb 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -105,10 +105,10 @@
 			"revisionTime": "2018-10-29T16:52:05Z"
 		},
 		{
-			"checksumSHA1": "g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=",
+			"checksumSHA1": "oHwfghigjTz8Xs+ssxwdl/atb44=",
 			"path": "github.com/gorilla/context",
-			"revision": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42",
-			"revisionTime": "2016-08-17T18:46:32Z"
+			"revision": "51ce91d2eaddeca0ef29a71d766bb3634dadf729",
+			"revisionTime": "2018-10-12T15:35:48Z"
 		},
 		{
 			"checksumSHA1": "uzOh/6ll8f2HnCNHDWRenwQ/Owo=",
@@ -117,28 +117,28 @@
 			"revisionTime": "2018-10-12T15:34:37Z"
 		},
 		{
-			"checksumSHA1": "bffinqNcCczds9fAuZGjoRWdvdU=",
+			"checksumSHA1": "22kXObb09lweSbdIjPZGeLBjnkg=",
 			"path": "github.com/gorilla/handlers",
-			"revision": "350d97a79266938cd77a9192b7d995132d4e2b5b",
-			"revisionTime": "2018-10-12T15:33:34Z"
+			"revision": "7e0847f9db758cdebd26c149d0ae9d5d0b9c98ce",
+			"revisionTime": "2018-07-27T23:06:46Z"
 		},
 		{
-			"checksumSHA1": "+i76HB4vEOFz+vLYsUmThS6oeQI=",
+			"checksumSHA1": "5NQxGXStdEEDvT6IPRtJk7Qg+GQ=",
 			"path": "github.com/gorilla/mux",
-			"revision": "24fca303ac6da784b9e8269f724ddeb0b2eea5e7",
-			"revisionTime": "2017-09-05T17:10:44Z"
+			"revision": "3d80bc801bb034e17cae38591335b3b1110f1c47",
+			"revisionTime": "2018-10-30T15:25:28Z"
 		},
 		{
-			"checksumSHA1": "NScdwrIOpSH9hoP7i3a6JGmE5rw=",
+			"checksumSHA1": "mo95lsTo6uV/Yw9IhdwEA2mKJHI=",
 			"path": "github.com/gorilla/securecookie",
-			"revision": "e65cf8c5df817c89aeb47ecb46064e802e2de943",
-			"revisionTime": "2018-10-10T17:46:47Z"
+			"revision": "61b4ad17eb88d0d1118560d1101176279be2bc88",
+			"revisionTime": "2019-10-28T04:23:04Z"
 		},
 		{
-			"checksumSHA1": "dxwmtflRNX8Q+gtB5EchmMwFnpQ=",
+			"checksumSHA1": "cwc8hi1B7gYshKGFEEhrsvSUsxU=",
 			"path": "github.com/gorilla/sessions",
-			"revision": "a3acf13e802c358d65f249324d14ed24aac11370",
-			"revisionTime": "2017-10-08T21:47:40Z"
+			"revision": "400b592ab70b9f8ee876cf47bc6c794d255dd4aa",
+			"revisionTime": "2019-10-06T15:13:01Z"
 		},
 		{
 			"checksumSHA1": "SGc5vSs9tXhrGJ5ncymDyMvTg24=",