Giter VIP home page Giter VIP logo

scs's Introduction

SCS: HTTP Session Management for Go

GoDoc Go report card Test coverage

Features

  • Automatic loading and saving of session data via middleware.
  • Choice of 19 different server-side session stores including PostgreSQL, MySQL, MSSQL, SQLite, Redis and many others. Custom session stores are also supported.
  • Supports multiple sessions per request, 'flash' messages, session token regeneration, idle and absolute session timeouts, and 'remember me' functionality.
  • Easy to extend and customize. Communicate session tokens to/from clients in HTTP headers or request/response bodies.
  • Efficient design. Smaller, faster and uses less memory than gorilla/sessions.

Instructions

Installation

This package requires Go 1.12 or newer.

go get github.com/alexedwards/scs/v2

Note: If you're using the traditional GOPATH mechanism to manage dependencies, instead of modules, you'll need to go get and import github.com/alexedwards/scs without the v2 suffix.

Please use versioned releases. Code in tip may contain experimental features which are subject to change.

Basic Use

SCS implements a session management pattern following the OWASP security guidelines. Session data is stored on the server, and a randomly-generated unique session token (or session ID) is communicated to and from the client in a session cookie.

package main

import (
	"io"
	"net/http"
	"time"

	"github.com/alexedwards/scs/v2"
)

var sessionManager *scs.SessionManager

func main() {
	// Initialize a new session manager and configure the session lifetime.
	sessionManager = scs.New()
	sessionManager.Lifetime = 24 * time.Hour

	mux := http.NewServeMux()
	mux.HandleFunc("/put", putHandler)
	mux.HandleFunc("/get", getHandler)

	// Wrap your handlers with the LoadAndSave() middleware.
	http.ListenAndServe(":4000", sessionManager.LoadAndSave(mux))
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	// Store a new key and value in the session data.
	sessionManager.Put(r.Context(), "message", "Hello from a session!")
}

func getHandler(w http.ResponseWriter, r *http.Request) {
	// Use the GetString helper to retrieve the string value associated with a
	// key. The zero value is returned if the key does not exist.
	msg := sessionManager.GetString(r.Context(), "message")
	io.WriteString(w, msg)
}
$ curl -i --cookie-jar cj --cookie cj localhost:4000/put
HTTP/1.1 200 OK
Cache-Control: no-cache="Set-Cookie"
Set-Cookie: session=lHqcPNiQp_5diPxumzOklsSdE-MJ7zyU6kjch1Ee0UM; Path=/; Expires=Sat, 27 Apr 2019 10:28:20 GMT; Max-Age=86400; HttpOnly; SameSite=Lax
Vary: Cookie
Date: Fri, 26 Apr 2019 10:28:19 GMT
Content-Length: 0

$ curl -i --cookie-jar cj --cookie cj localhost:4000/get
HTTP/1.1 200 OK
Date: Fri, 26 Apr 2019 10:28:24 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

Hello from a session!

Configuring Session Behavior

Session behavior can be configured via the SessionManager fields.

sessionManager = scs.New()
sessionManager.Lifetime = 3 * time.Hour
sessionManager.IdleTimeout = 20 * time.Minute
sessionManager.Cookie.Name = "session_id"
sessionManager.Cookie.Domain = "example.com"
sessionManager.Cookie.HttpOnly = true
sessionManager.Cookie.Path = "/example/"
sessionManager.Cookie.Persist = true
sessionManager.Cookie.SameSite = http.SameSiteStrictMode
sessionManager.Cookie.Secure = true

Documentation for all available settings and their default values can be found here.

Working with Session Data

Data can be set using the Put() method and retrieved with the Get() method. A variety of helper methods like GetString(), GetInt() and GetBytes() are included for common data types. Please see the documentation for a full list of helper methods.

The Pop() method (and accompanying helpers for common data types) act like a one-time Get(), retrieving the data and removing it from the session in one step. These are useful if you want to implement 'flash' message functionality in your application, where messages are displayed to the user once only.

Some other useful functions are Exists() (which returns a bool indicating whether or not a given key exists in the session data) and Keys() (which returns a sorted slice of keys in the session data).

Individual data items can be deleted from the session using the Remove() method. Alternatively, all session data can be deleted by using the Destroy() method. After calling Destroy(), any further operations in the same request cycle will result in a new session being created --- with a new session token and a new lifetime.

Behind the scenes SCS uses gob encoding to store session data, so if you want to store custom types in the session data they must be registered with the encoding/gob package first. Struct fields of custom types must also be exported so that they are visible to the encoding/gob package. Please see here for a working example.

Loading and Saving Sessions

Most applications will use the LoadAndSave() middleware. This middleware takes care of loading and committing session data to the session store, and communicating the session token to/from the client in a cookie as necessary.

If you want to customize the behavior (like communicating the session token to/from the client in a HTTP header, or creating a distributed lock on the session token for the duration of the request) you are encouraged to create your own alternative middleware using the code in LoadAndSave() as a template. An example is given here.

Or for more fine-grained control you can load and save sessions within your individual handlers (or from anywhere in your application). See here for an example.

Configuring the Session Store

By default SCS uses an in-memory store for session data. This is convenient (no setup!) and very fast, but all session data will be lost when your application is stopped or restarted. Therefore it's useful for applications where data loss is an acceptable trade off for fast performance, or for prototyping and testing purposes. In most production applications you will want to use a persistent session store like PostgreSQL or MySQL instead.

The session stores currently included are shown in the table below. Please click the links for usage instructions and examples.

Package
badgerstore Badger based session store
boltstore Bolt based session store
bunstore Bun based session store
buntdbstore BuntDB based session store
cockroachdbstore CockroachDB based session store
consulstore Consul based session store
etcdstore Etcd based session store
firestore Google Cloud Firestore based session store
gormstore GORM based session store
leveldbstore LevelDB based session store
memstore In-memory session store (default)
mongodbstore MongoDB based session store
mssqlstore MSSQL based session store
mysqlstore MySQL based session store
pgxstore PostgreSQL based session store (using the pgx driver)
postgresstore PostgreSQL based session store (using the pq driver)
redisstore Redis based session store
sqlite3store SQLite3 based session store

Custom session stores are also supported. Please see here for more information.

Using Custom Session Stores

scs.Store defines the interface for custom session stores. Any object that implements this interface can be set as the store when configuring the session.

type Store interface {
	// Delete should remove the session token and corresponding data from the
	// session store. If the token does not exist then Delete should be a no-op
	// and return nil (not an error).
	Delete(token string) (err error)

	// Find should return the data for a session token from the store. If the
	// session token is not found or is expired, the found return value should
	// be false (and the err return value should be nil). Similarly, tampered
	// or malformed tokens should result in a found return value of false and a
	// nil err value. The err return value should be used for system errors only.
	Find(token string) (b []byte, found bool, err error)

	// Commit should add the session token and data to the store, with the given
	// expiry time. If the session token already exists, then the data and
	// expiry time should be overwritten.
	Commit(token string, b []byte, expiry time.Time) (err error)
}

type IterableStore interface {
	// All should return a map containing data for all active sessions (i.e.
	// sessions which have not expired). The map key should be the session
	// token and the map value should be the session data. If no active
	// sessions exist this should return an empty (not nil) map.
	All() (map[string][]byte, error)
}

Using Custom Session Stores (with context.Context)

scs.CtxStore defines the interface for custom session stores (with methods take context.Context parameter).

type CtxStore interface {
	Store

	// DeleteCtx is the same as Store.Delete, except it takes a context.Context.
	DeleteCtx(ctx context.Context, token string) (err error)

	// FindCtx is the same as Store.Find, except it takes a context.Context.
	FindCtx(ctx context.Context, token string) (b []byte, found bool, err error)

	// CommitCtx is the same as Store.Commit, except it takes a context.Context.
	CommitCtx(ctx context.Context, token string, b []byte, expiry time.Time) (err error)
}

type IterableCtxStore interface {
	// AllCtx is the same as IterableStore.All, expect it takes a
	// context.Context.
	AllCtx(ctx context.Context) (map[string][]byte, error)
}

Preventing Session Fixation

To help prevent session fixation attacks you should renew the session token after any privilege level change. Commonly, this means that the session token must to be changed when a user logs in or out of your application. You can do this using the RenewToken() method like so:

func loginHandler(w http.ResponseWriter, r *http.Request) {
	userID := 123

	// First renew the session token...
	err := sessionManager.RenewToken(r.Context())
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	// Then make the privilege-level change.
	sessionManager.Put(r.Context(), "userID", userID)
}

Multiple Sessions per Request

It is possible for an application to support multiple sessions per request, with different lifetime lengths and even different stores. Please see here for an example.

Enumerate All Sessions

To iterate throught all sessions, SCS offers to all data stores an All() function where they can return their own sessions.

Essentially, in your code, you pass the Iterate() method a closure with the signature func(ctx context.Context) error which contains the logic that you want to execute against each session. For example, if you want to revoke all sessions with contain a userID value equal to 4 you can do the following:

err := sessionManager.Iterate(r.Context(), func(ctx context.Context) error {
	userID := sessionManager.GetInt(ctx, "userID")

	if userID == 4 {
		return sessionManager.Destroy(ctx)
	}

	return nil
})
if err != nil {
	log.Fatal(err)
}

Flushing and Streaming Responses

Flushing responses is supported via the http.NewResponseController type (available in Go >= 1.20).

func flushingHandler(w http.ResponseWriter, r *http.Request) {
	sessionManager.Put(r.Context(), "message", "Hello from a flushing handler!")

	rc := http.NewResponseController(w)

	for i := 0; i < 5; i++ {
		fmt.Fprintf(w, "Write %d\n", i)

		err := rc.Flush()
		if err != nil {
			log.Println(err)
			return
		}

		time.Sleep(time.Second)
	}
}

For a complete working example, please see this comment.

Note that the http.ResponseWriter passed on by the LoadAndSave() middleware does not support the http.Flusher interface directly. This effectively means that flushing/streaming is only supported by SCS if you are using Go >= 1.20.

Compatibility

You may have some problems using this package with Go frameworks that do not propagate the request context from standard-library compatible middleware, like Echo and Fiber. If you are using Echo, please use the echo-scs-session fork of this package instead.

Contributing

Bug fixes and documentation improvements are very welcome! For feature additions or behavioral changes, please open an issue to discuss the change before submitting a PR. For new stores, please also open an issue to establish whether there is wider demand for the store before submitting a PR.

scs's People

Contributors

alexedwards avatar alinnert avatar bishopofturkey avatar bramvbilsen avatar cgossain avatar daniel-sogbey avatar decafe09 avatar dependabot[bot] avatar fnoopv avatar gandaldf avatar gernest avatar hycner avatar joesonw avatar jum avatar lafriks avatar makslevental avatar mvrhov avatar nevernotmove avatar pangaunn avatar royallthefourth avatar sh4nks avatar slinso avatar srenatus avatar stereosteve avatar stillwondering avatar testwill avatar verygoodsoftwarenotvirus avatar xuo avatar zaeph avatar zchenyu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

scs's Issues

Multiple Set-Cookie headers in response

Here is small example:

package main

import (
	"net/http"

	"github.com/alexedwards/scs"
)

var sessionManager = scs.NewCookieManager("u46IpCV9y5Vlur8YvODJEhgOY8m9JVE4")

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/auth", login)

	http.ListenAndServe(":4000", sessionManager.Use(mux))
}

func login(w http.ResponseWriter, r *http.Request) {
	session := sessionManager.Load(r)
        
        // authenticate user ...

	err := session.RenewToken(w)
	if err != nil {
		http.Error(w, err.Error(), 500)
                return
	}

	if err := session.PutInt(w, "userId", 1); err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	if err := session.PutBool(w, "isAdmin", true); err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
}

Here is /auth response:

HTTP/1.1 200 OK
Set-Cookie: session=rXoTm-uiZM8eYszKWKTbUP-D-SBV0Pdx8DyMpX7jL55yVcOwRxLROJSeDHeuW0iYifwVpUnEiXyhU_H-Vl-1-2LpnjHnOvx1TS1yYuoccQP6P56iEXyCzngQRt_UkHGG5Wva5-0; Path=/; HttpOnly
Set-Cookie: session=6HjYzvYsALD_UvkBNlrheCKijlQDAAd2rMsWN_URq7uO12n2ng2t-7PYSmHSV8l3n0TKtb6_y0-DKuc--uikxCBJ-NuvR0a91vj7dauPu_TMrCGtfa1cOgLpr1R2MhTCDt4qUNwmBNEblJrViAiW; Path=/; HttpOnly
Set-Cookie: session=ewJmYE3psbCYtX39Zgj-Utv5XTjCJ5SfbsO32daXPyOovU0y0O2OPQtv6QlL9Zv-yZ-XJuYqPvkZQ5tt_tjLqtpKdvohTOLAjLp7XO9yfVjV5rgtC6hG5b9W0Hb_8shOUdZdKZdM936IEAbaRkltlDOEYqBSbFcXoZIjJUKs; Path=/; HttpOnly
Date: Thu, 26 Oct 2017 09:08:01 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

As you can see there are three different Set-Cookie headers with the same cookie-name in response which is wrong according to RFC 6265:

Servers SHOULD NOT include more than one Set-Cookie header field in
the same response with the same cookie-name.

Re-add Token() for v2

My middleware unit testing used the v1 Token() function for verifications of client-server responses. Does v2 have a way for middleware to access the private token directly?

Ignoring paths

Is it possible to set a list of folders that the session manager needs to ignore? This is quite useful for static files.

Changing a session timeout period

Hi,
Is it possible to change the session's expiry on a single session basis? I need some of my users to have much longer sessions than others, but it seems the session.IdleTimeout call has a global affect. Am I missing something?

Cheers,
Shmul

Provide a must have session middleware

Hi,

Firstly, thank you for this package, I've been rolling my own similar one and in frustration Google again and found this project. This greatly simplifies session management for me, without requiring bring in a library or using an overly simple package like gorilla/sessions (nothing wrong with it, I just needed more).

One piece that I'd also like this to handle is the ability to redirect a user if a session doesn't exist (new user or expired session). For example, keeping sessions for all handlers, except a few that are for logged in users only.

I'm currently handling this with my own middleware successfully. If the user does not have the key username set in the session, they are redirected to /login.

func MustExist(next http.Handler) http.Handler {
     return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
         if ok, _ := session.Exists(r, "username"); !ok {
             http.Redirect(w, r, "/login", http.StatusFound)
             return
         }
         next.ServeHTTP(w, r)
     })
 }

I'd love to know if there's a way to detect if there's a session at all, as in my case, and the common (but not all) cases, a user would just want to know that someone is logged in.

This isn't a lot of code to write, but it's also possible to get wrong, and I'd want to copy this exact signature across a few sites, so having this provided by the session package would be ideal.

To do this, I think it'd be best if it's just a check if the user has a valid session, but the problem I found was that an earlier handler detects the lack of session and added one. So this handler was never able to detect the lack of session. Is there a way to detect if a session doesn't currently exist for this user without adding storing keys?

Would it be possible to provide this middleware as part of the session package? I had a stab at an API:

  type MustExist struct {
      Key         string
      RedirectURL string
  }

  func (m *MustExist) Handler(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          if ok, _ := session.Exists(r, m.Key); !ok {
              http.Redirect(w, r, m.RedirectURL, http.StatusFound)
              return
          }
          next.ServeHTTP(w, r)
      })
  }

And it could be used like:

&MustExist{Key: "username", RedirectURL: "/login"}.Handler

But this is still requiring a string key be set, and the API itself is quite ugly. As a user, I'd prefer to just use:

// MustHaveSession redirects a user to redirectURL if no session exists (or it's expired).
// No other handlers or middleware are executed.
func MustHaveSession(redirectURL string) func(http.Handler) http.Handler

Is this possible?

Separate modification and saving of session data

See discussion in #23

This could be addressed by implementing Middleware which uses a custom ResponseWriter to intercept HTTP Responses, save the session data to the store and set the session cookie - similar to v0.1 of SCS (https://github.com/alexedwards/scs/blob/v0.1.0/session/manager.go)

Downside is that this causes problems with frameworks/applications which also try to call WriteHeader. See #15

A workaround might be to set the session cookie header each time the session is modified (or even loaded) and only handle saving to the store in the middleware. I think that would work for most stores, except the CookieStore, which needs the response headers to not have already been written.

Using scs with Echo

Is there a way to use the middle ware provided by scs within echo? I try to do as below way, but failed. Can you give me some suggestion?

package main

import (
    "github.com/labstack/echo"
    "github.com/labstack/echo/engine/standard"
    "github.com/labstack/echo/middleware"
    "github.com/alexedwards/scs/session"
    "github.com/alexedwards/scs/engine/memstore"
    "net/http"
)

func main() {
    sessionManager := session.Manage(memstore.New(0))
    e := echo.New()
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(standard.WrapMiddleware(sessionManager))
        e.SetDebug(true)

    e.GET("/", func(c echo.Context) error {
        err := session.PutString(c.Request().(*standard.Request).Request, "username", "admin")
        if err != nil {
            c.Logger().Error("session.PutString:", err)
            return err
        }

        if msg, err := session.GetString(c.Request().(*standard.Request).Request, "username"); err != nil || len(msg) == 0 {
            c.Logger().Info("session.GetString:", msg)
        }

        return c.String(http.StatusOK, "Hello, World!")
    })

    e.Run(standard.New(":1323"))
}

New Release?

Hello - would it be possible to release a new version? I noticed that there is an unreleased change with the updated location of the redigo library.

Appreciate your efforts!

ERROR illegal base64 data at input byte 5

I tried to use scs with echo on very simple application, but I get this error. I use session.PutObject to store the data, and session.GetObject to read the data.

package main

import (
	"github.com/alexedwards/scs"
	"github.com/labstack/echo"
	"net/http"
)

var sessionManager = scs.NewCookieManager("u46IpCV9y5Vlur8YvODJEhgOY8m9JVE4")

type UserModel struct {
	ID   string
	Name string
	Age  int
}

func main() {
	const SESSION_ID = "id"

	e := echo.New()

	e.HTTPErrorHandler = func(err error, c echo.Context) {
		report, ok := err.(*echo.HTTPError)
		if !ok {
			report = echo.NewHTTPError(http.StatusInternalServerError, err.Error())
		}

		c.Logger().Error(report)
		c.JSON(report.Code, report)
	}

	e.Use(echo.WrapMiddleware(sessionManager.Use))

	e.GET("/index", func(c echo.Context) error {
		session := sessionManager.Load(c.Request())

		user := new(UserModel)
		user.ID = "001"
		user.Name = "Noval"
		user.Age = 12

		err := session.PutObject(c.Response(), SESSION_ID, *user)
		if err != nil {
			return err
		}

		return c.Redirect(http.StatusTemporaryRedirect, "/home")
	})

	e.GET("/home", func(c echo.Context) error {
		session := sessionManager.Load(c.Request())

		user := new(UserModel)
		err := session.GetObject(SESSION_ID, user)
		if err != nil {
			return err
		}

		return c.JSON(http.StatusOK, user)
	})

	e.Logger.Fatal(e.Start(":9000"))
}

The error occur on the part where the session is being read.

{"Code":500,"Message":"illegal base64 data at input byte 5","Internal":null}

SessionManager scope

Can you please tell if SessionManager is global in scope meaning bootstrapped from some Init function

Error types leak through from storage systems

If I invoke a function like GetObject, it sometimes has a non-nil error that comes from the underlying storage system. In order to handle these gracefully, I'd need to program against the underlying storage system to handle something like "cache key not found". I'm specifically talking about the memcached backend here, but it looks like other storage systems use the same leaky abstraction. This defeats the abstraction offered by SCS since I can't easily handle errors generated by multiple backends in the same way. In short, my code that uses scs.Manager shouldn't need to know about memcache.ErrCacheMiss.

To fix this problem, I propose implementing new SCS error types that can map onto the most common storage backend errors in a sensible way:

ErrNotFound
ErrBackendFailure
ErrSomethingElse...

This would provide abstraction over the underlying errors without hiding too much about what's really happening. I'd be happy to program this myself and rework all of the storage backends to use it.

What do you think?

[discussion] consider switching to bbolt backend

Ben Johnson, the creator of bolt, has declared his project as feature complete and won't provide further support due to lack of time (see boltdb/bolt@fa5367d).

CoreOS forked the project over to bbolt and pushes development of the project forward. Popular projects based on boltdb, like storm are already on the migration path to bbolt.

Having said that, bolt works great for me, and you've already moved your API to v1, so switching the bolt backend over to bbolt would probably break your v1 contract.

Just opening this issue to raise awareness to the change in bolt's future.

And thanks for this very fine project ๐Ÿ˜„

Memcached support?

Looking around for a new session manager, one which supports memcached.

SCS looks interesting and is written for modern Go (unlike Gorilla), but doesn't (yet) support memcached.

Any plans on memcached support? ๐Ÿ˜„

Provide example of using SCS with Echo

Should scs include a folder dedicated to middleware examples? It would be similar to stores but dedicated to middleware for scs.

Or would it be better for scs to make reference to third party middleware implementers on its README?

It's a classic question. Either direction works for me primarily b/c scs doesn't have many open issues. If scs was highly trafficked in terms of issues, I'd probably suggest breaking-out the middleware to third parties.

I have some echo middleware to contribute - just let me know what you'd prefer. Others may find a landing spot for their ideas too. (see #16)

The problem of log.Println() in Use() function

I use SCS as the session middleware in the Echo framework, but in the process of operating session, if the Redis service is closed, it triggers the log.Println() in the Use() function.
My Code is like this:
echo.Use(echo.WrapMiddleware(session.Use))
I hope this error triggers Echo's error and shouldn't print error messages directly on the server console.
How do I do that?

Getting token value in v2

Congratulations on v2.

Would you consider adding a function similar to Token() in v1 for fetching the token value? I have a problem to keep track on the latest login by session token because I cannot get the value after calling RenewToken. Maybe is there an alternative way?

it'd work for me in my case If adding this function in data.go, but only a thought.

func (s *Session) Token(ctx context.Context) string {
	sd := s.getSessionDataFromContext(ctx)
	return sd.token
}

Thanks for the library.

Use Redis to store idle timeout issues

This is my code snippet, I use Redis storage, and define the IdleTimeout value is 20 minutes and the Lifetime value is 2 hours, but the session data is always in 20 minutes of failure, the page refreshes does not seem to automatically extend the idle timeout time?

SessionManager = scs.NewManager(
	redisstore.New(&redis.Pool{
		MaxIdle: 10,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", "127.0.0.1:6379", redis.DialDatabase(0))
		},
		IdleTimeout: time.Duration(20) * time.Minute,
	}),
)
SessionManager.IdleTimeout(20 * time.Minute)
SessionManager.Lifetime(time.Duration(2 * time.Hour)

[discussion] consider cookieless sessons

For an API I needed to store the session-id somewhere else instead of the cookie header field. So I added the possibility to also use a string instead of an http.Request. Sadly it breaks the public API so I won't issue a pull request, but maybe someone else has a better idea making it work without breaking changes.

https://github.com/ChristophPech/scs

Session doesn't work

Hi,
first thank you for this great package.
after some hours of trying to get it work, i decided to give a try the sample code
i use curl put get but hello world is not printed.

Use securecookie

Any objection to using securecookie to write and load the session token/key/id from the cookie?

I think it's a good idea to use signed cookies to prevent tampering. e.g. you can avoid unnecessary queries to the session store by throwing away invalid cookies

With you blessing, I'll open a PR to integrate securecookie into scs with the following features:

  1. Backwards compatible with existing valid scs cookies. We could accomplish this with one of the following approaches:
    1. Flag to enable securecookies
      • Won't "upgrade" cookies in-place
    2. Pack/encode the cookie value differently - Would have to use a non-base64 encoding character to signify differences
      • Will upgrade cookies in-place
      • Could encode the cookie value as a JSON string or just use a simple non-base64 character
    3. Migrate to another cookie name
      • Will upgrade cookies in-place
      • Secure cookie name would be configurable e.g. add secureName field to options struct
      • I prefer this approach since it's a cleaner design and we can use the configurable string enable/disable securecookies e.g. empty string disables secure cookies
  2. Unit-tested

Can't load session multiple times in same request and get saved value

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/alexedwards/scs"
)

var manager = scs.NewCookieManager("u46IpCV9y5Vlur8YvODJEhgOY8m9JVE4")

type Site struct {
}

func (site Site) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	session := manager.Load(req)
	session.PutString(w, "key", "value")
	fmt.Println(session.GetString("key"))

	session2 := manager.Load(req)
	// should be able to get saved value
	fmt.Println(session2.GetString("key"))
}

func main() {
	Server := httptest.NewServer(Site{})

	http.Get(Server.URL)
}

https://github.com/alexedwards/scs/blob/master/session.go#L41

Maybe should save session into request's context, and load it from context next time?

Improve storage to support exclusive locks

As I can see, the redis storage implementation doesn't support any locks.
There's no documentation on mechanism to how to do this right.
The current Store interface doesn't provide any locks interface even for custom implementations.

So, I'd really like to see this change so one could implement their own locks (until someone makes some library).

For example, there's only one implementation of locks I've found (a simple one):
https://github.com/bsm/redis-lock

[help] Strange behavior

Sorry, i cant share the code right now. Just need some light for finding the issue. Sometimes the session just doesnt work and create many tokens for the same user (im using mysqlstore, so is easy to see this). Now session only works after calling RenewToken. Did this descriptions identify with any common misuse of the library? Ive already tried a lot of things. Thanks and sorry for the lack of more info.

stale sessions in data store - `deadline` plus `saved` property?

How are folks handling stale sessions in the data store? For example, redis has its expire command but by default it is set to never expire.

If a vacuuming routine compares time.Now() > deadline, then the session should be deleted. That's the logic, yes?

Is there any use case for including a saved property (the date when a session was last saved) - in addition to the deadline? Right now I can't think of any. Maybe I just answered my own question but am double-checking with others.

Compatibility issues with the Echo framework

I use the scs/session package in Echo, and if I export anything to the browser in the HTTPErrorHandler function of Echo, the Echo of logger will print such warning messages:
Level:WARN, file:context.go, line:484, message:response, already, committed.
If I don't use session, there won't be such a problem.

package main

import (
	"time"

	"github.com/alexedwards/scs/engine/memstore"
	"github.com/alexedwards/scs/session"
	"github.com/labstack/echo"
)

func main() {
	app := echo.New()
	app.Debug = true
	sessionManager := session.Manage(memstore.New(12 * time.Hour))
	app.Use(echo.WrapMiddleware(sessionManager))
	app.HTTPErrorHandler = errorHandler
	app.Start(":80")
}

func errorHandler(err error, ctx echo.Context) {
	ctx.String(200, "test")
}

This is a bug or my usage is wrong?

I use echo v3, I repeat the request controller (refresh page), it will create a number of records in redis scs / session, this is a bug or I use the wrong method?
the code is probably this:

main.go

engine := redisstore.New(
	&redis.Pool{
		MaxIdle: 10,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", "127.0.0.1:6379")
		},
	},
)

sessionManager := session.Manage(
	engine,
	session.HttpOnly(true),
	session.IdleTimeout(20*time.Minute),
	session.Lifetime(3*time.Hour),
)
App.Use(echo.WrapMiddleware(sessionManager))

Controller

func Index(ctx echo.Context) error {
	data := make(map[string]interface{})
	session.PutString(ctx.Request(), "sess", "ok")
	data["sess"], _ = session.GetString(ctx.Request(), "sess")
	return ctx.Render(200, "index.jet", data)
}

Refresh three pages, redis appeared in three records, I think this is not normal?

1
2
3

Add manager.Opts() method to expose option

Since Opts is not exposed, custom middleware extension is not possible, in case I want to write my own middleware.
Referring

scs/manager.go

Line 114 in 876a0fd

func (m *Manager) Use(next http.Handler) http.Handler {
, m.opts.Timeout cannot be accessed from outside package.

What if by adding manager.Opts() method, which returns opts.

Add support for Touch Interval Rate

Looking for some feedback on whether or not this is a good idea. I'd like finer control of Touch(). Why? For a high-volume site, each Touch is a new update to deadline and therefore a new write to the data store. While some handlers will always uses sessions others will not, such as serving static files. Admittedly there are other ways to design this. For example in middleware, I could skip calling Touch for any path serving static files. But a few seconds delay in Touch would serve the same purpose. (And for those static files, I'm still updating global statistics - just not session stats on the static files - which I'm not tracking.)

What if there was a TouchIntervalRate to control how often the deadline gets updated via touch? It could manifest itself a few ways. Arguably the easiest is a new function, session.TouchWithInterval().

// in session.go
// TouchWithInterval func controls the rate at which touches are automatically written via Touch
// there can be quite a few quick touches - which mean writes back to the datastore
// would require a private "isNew" var added to manager.Session
func TouchWithInterval(w http.ResponseWriter, interval time.Duration) error {
	if s.loadErr != nil {
		return s.loadErr
	}

	doTouch := true
	if !s.isNew && interval > 0 {
		timeToTouch := time.Now().Subtract(s.deadline).Add(interval)
		doTouch = timeToTouch.Afer(time.Now())
	}

	if doTouch {
		return Touch(w)
	}

	return nil
}

Thoughts?

Echo compatibility

Well done @alexedwards on re-imagining scs. ๐Ÿฅ‡ This was a comprehensive rewrite. A number of items I appreciate:

  • Status
  • Efficient load/save with help from Status
  • Session and SessionCookie are global in scope
  • I also like that IdleTimeout and Lifetime were split from the old options.
  • go.mod added for go modules support

For me, v2 solves a number of issues for which I had kept a permanent local fork of scs on my machine. (My fork was getting a bit dated too!)

I've done some testing and so far so good. I had one issue with a private token but this example provides a powerful solution to my issue.

use of closed network connection

I use SCS to implement session functionality in the echo framework, but I delete the sessionid in cookie and then refresh the browser and output the error:

redigo: unexpected type for Bytes, got type []interface {}
redigo: unexpected response line (possible server error or unsupported concurrent read by application)
write tcp 127.0.0.1:49741->127.0.0.1:6379: use of closed network connection

My code:

//Connect Redis
func connectRedis() (redis.Conn, error) {
	conn, err := redis.Dial("tcp", "127.0.0.1:6379")
	if err != nil {
		return nil, err
	}
	return conn, err
}

//Configure SCS
func SetSession() error {
	redisConn, err := connectRedis()
	if err != nil {
		return err
	}
	redisPool := redisstore.New(redis.NewPool(func() (redis.Conn, error) {
		return redisConn, nil
	}, 1))

	SessionManager = scs.NewManager(redisPool)
	SessionManager.Name("sessionid")
	SessionManager.Path("/")
	SessionManager.Lifetime(60 * time.Minute)
	SessionManager.Secure(false)
	SessionManager.HttpOnly(true)
	SessionManager.IdleTimeout(20 * time.Minute)
	SessionManager.Persist(true)
	return nil
}

//Echo middleware
func SessionMiddleware() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) error {
			//update session idletime
			session := SessionManager.Load(ctx.Request())
			err := session.Touch(ctx.Response().Writer)
			if err != nil {
				return err
			}
			return next(ctx)
		}
	}
}

Using scs with Negroni

Due to the nature of the middleware returned from session.Manage(), I'm not seeing a way to chain this middleware with Negroni.

Is there a way to use the middleware provided by scs within negroni?

go get github.com/alexedwards/scs/v2 fails

go get github.com/alexedwards/scs/v2
package github.com/alexedwards/scs/v2: cannot find package "github.com/alexedwards/scs/v2" in any of:
/usr/local/go/src/github.com/alexedwards/scs/v2 (from $GOROOT)
/home/wise/go/src/github.com/alexedwards/scs/v2 (from $GOPATH)

I'm moving a site I have in development to a new machine. I have the site working on another machine, I last set that one up maybe a week ago.

What's the easiest way to use scs and postgresql?

Allow access to session through context

The session middleware populates the context with the session using a custom string type as a key. Thus it cannot be accessed from the outside without the *http.Request object through Manager.Load(req).

Would you accept a patch adding Manager.LoadFromContext(ctx) method so one can access the session in a scope where the request is not available but its context is?

mysqlstore with MySQL < 5.6: UTC_TIMESTAMP(6) parameter unsupported

With MySQL before version 5.6, I get SQL errors in mysqlstore.go. They are caused by UTC_TIMESTAMP(6). The parameter was introduced in MySQL 5.6.4 (see the docs) and "is given to specify a fractional seconds precision from 0 to 6". In MySQL 5.6, they also changed the default format from 'YYYY-MM-DD HH:MM:SS' or YYYYMMDDHHMMSS.uuuuuu to 'YYYY-MM-DD HH:MM:SS' or YYYYMMDDHHMMSS.

I'm not sure if it a "MySQL version switch" in mysqlstore would be feasible. But is it necessary to store the expiry timestamp with such a precision at all? Would't one second be precise enough? Then we could omit the parameter of UTC_TIMESTAMP(6).

scs: no session data in context

Abstract: Example code not working - resulting in stacktrace

Example used

At first, I suspected some issues with the filesystem (though /tmp should be writable).
But even if you substitute the bolt examplecode by the memstore example code, the app crashes.

Operating system:
Linux MYBOX 5.1.16-1-default #1 SMP Wed Jul 3 12:37:47 UTC 2019 (2af8a22) x86_64 x86_64 x86_64 GNU/Linux

Go Version: go version go1.12.6 linux/amd64

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.