Sessions: Third Party Stores Not Applying Options to Codecs correctly.

Created on 10 Jul 2015  ·  17Comments  ·  Source: gorilla/sessions

We just upgraded this package and it's not setting cookies correctly. It's setting stuff like _ga=GA1.1.922831813.14264788986 instead of the regular base64 encoded cookie data. Any idea whats going on or when this broke so we can revert back to a working version? Thanks!

bug stale

Most helpful comment

Note that this also affects the majority of store implementations, as they all use securecookie for saving the ID to their backend store.

redistore has half of a correct implementation if you manually call SetMaxAge but doesn't directly apply this for the default case. It just so happens that the default (30 days) is the same as securecookie's.

From a quick look it appears that _all_ other stores are affected—setting a MaxAge via the Options struct is not applied to the underlying securecookie instances generated from CodecsFromPairs. Most end users would not have run into this bug as I suspect most users set expiry dates _less_ than what securecookie is implicitly embedding into the HMAC.

FWIW: I also believe that "far future" cookie expiries aren't great unless you have a very specific need for it.

All 17 comments

The "_ga" cookie is unrelated to gorilla/sessions - it's a Google Analytics
cookie.

If gorilla/sessions isn't setting cookies (there haven't been any breaking
changes) can you post the relevant code and the output of the Cookies pane
from your browser? (inspector > resources > cookies under Chrome)

On Fri, Jul 10, 2015 at 7:12 AM marksalpeter [email protected]
wrote:

We just upgraded this package and it's not setting cookies correctly. It's
setting stuff like _ga=GA1.1.922831813.14264788986 instead of the regular
base64 encoded cookie data. Any idea whats going on or when this broke so
we can revert back to a working version? Thanks!


Reply to this email directly or view it on GitHub
https://github.com/gorilla/sessions/issues/48.

Hi Matt,

I'm also working on the project with Mark and I can give you some more details. So we just updated the package because the original problem was with the cookies all expiring after 30 days even when we set the MaxAge to some really large number. Here is the Authentication code that is live right now and causing problems for us:

const (
AUTH_SESSION_NAME = "authentication-session"
)

var (
config = jconfig.LoadConfig( global.CONFIG() + "securecookie.json" )
authKey = []byte(config.GetString("authorization_key"))
encryptionKey = []byte(config.GetString("encryption_key"))
session = sessions.NewCookieStore(authKey, encryptionKey)
)

func init() {

apiRouter.HandleFunc("/auth/"   , Authenticate      )
apiRouter.HandleFunc("/deauth/"  , Deauthenticate   )
apiRouter.HandleFunc("/reauth/" , ReAuthenticate    )

// register complex data types for saving in sessions
gob.Register(&models.Device{})
gob.Register(&models.Manager{})
gob.Register(&models.SalesRep{})

// modify the options of the session store so that the auth cookie never expires (this is set so it expires in 200 years...)
session.Options.MaxAge = 6307200000

}

// this handler grants permissions in the system for devices and managers
// if the requester is already authroized it will display their information. To autherize a defferent
// account, first the client must deauthenticate.
//
// Params
// user:
//
// note: the data returned from this auth function is only guarenteed to be accurate
// when the user first authenticates. otherwise it will display data cached in their session
//
func Authenticate(response http.ResponseWriter, request *http.Request) {

log.Println("Authenticate")

// open the session
auth_session, err :=  session.Get(request, AUTH_SESSION_NAME)
if err != nil {
    log.Println("there was an error retreiving the session:", err)
    InternalServerError(response, request)
    return
}

// already authroized as a manager
if manager, ok := auth_session.Values["manager"].(*models.Manager); ok {
    log.Printf("already logged in as manager %d", manager.Id)
    Success(response, request, manager)

// already authrorized as a device
} else if device, ok := auth_session.Values["device"].(*models.Device); ok {
    log.Printf("already logged in as device %d", device.Id)
    Success(response, request, device)

// attempt to gain authroization
} else {            
    fp  := parsers.FormParser(request)  

    // login as a manager
    if username, password := fp.GetString("user", ""), fp.GetString("pass", ""); username != "" && password != ""  {

        db := database.Open()
        defer db.Close()


        if manager := db.LoginAsManager(username, password); manager != nil {
            auth_session.Values["manager"] = manager

            // manager session error
            if err := auth_session.Save(request, response); err != nil {
                log.Printf("manager could not save session %s !\n", err.Error())
                InternalServerError(response, request)

            // manager login in success
            } else {
                log.Printf("logged in as manager %d !\n", manager.Id)
                Success(response, request, manager)     
            }

        // manager login failed
        } else {
            log.Printf("manager credentials not valid!")
            Unauthroized(response, request)
        }

    // login as a salesrep
    } else if username, password := fp.GetString("username", ""), fp.GetString("password", ""); username != "" && password != "" {

        db := database.Open()
        defer db.Close()


        if sales_rep_id := db.LoginAsSalesRep(username, password); sales_rep_id > 0 {

            sales_reps := db.GetSalesRep(&models.SalesRep{ Id: sales_rep_id });

            auth_session.Values["sales_rep"] = sales_reps[0];

            // sales rep session error
            if err := auth_session.Save(request, response); err != nil {
                log.Printf("sales rep could not save session %s !\n", err.Error())
                InternalServerError(response, request)

            // sales rep login in success
            } else {
                log.Printf("logged in as sales rep %d !\n", manager.Id)
                Success(response, request, manager)     
            }

        // sales rep login failed
        } else {
            log.Printf("sales rep credentials not valid!")
            Unauthroized(response, request)
        }

    // login as a device
    } else if pin_code := fp.GetInt32("PinCode", -1); pin_code > 0 {

        db := database.Open()
        defer db.Close()

        if devices := db.GetDevice(&models.Device{ PinCode:pin_code }, nil); len(devices) > 0 {
            auth_session.Values["device"] = &devices[0]

            // device session error
            if err := auth_session.Save(request, response); err != nil {
                log.Printf("device could not save session %s !\n", err.Error())
                InternalServerError(response, request)

            // device login in success
            } else {
                log.Printf("logged in as device %d !\n", devices[0].Id)
                Success(response, request, devices[0])      
            }

        // device login failed
        } else {
            log.Printf("device credentials not valid!")
            Unauthroized(response, request)
        }


    // no valid credentials were provided   
    } else {
        log.Println("no valid deivce or manager credentials")
        BadRequest(response, request)
    }

}

}

And this is what we are getting in the logs after 30 days:

Device Cookie: authentication-session=MTQzNTg3MDA2OXw0YzhnUnJsOU9OVHoyUHVjQlNtYjJ6eGpNUW1QV19teVRaSW9qWUt1ZGItYnQ4U1o5OEZHcUNQb096dW5JOGRtRU5wclVIU2h3dS00MUMtOHg0d2ZYRGxGbzVLUkRLaWVMNjQwcm51aEx5T1pfVlJKT0trb2VPeXNuUk9QeWVkaWVfQ09UNjhQMGlndmN1dTJjZlNBVm00VHdFOWJScGZKVGgyd2QtblZwelFwT29md1VlZW0zazl5d2g4WTdaSHFvOXNoMTNqdmFveTBlTE9DMW0zT3dvVDJyV211dXFodFNjZUdRa1NiMVlQT1B2czU4MXNGWjU1ZjZBbzdGN2Z6NGpfaGNMSi1rT2NuS0p1ZEJicV92dFlYc21VNjhGTmlZbUdodm1wbTVLTDdKNVhubzctc0dEX3pKaDJNemE5Y214THFlSlMxSnhMeU5JZFVUck13Z2loaUJveGNBUmZQbDFhYWsyVFdXektnWXFWS1FuNERjdTB5cktBMWR2TEJPTl9NLXhfWF95OWhfY0xVeEp5TV9nV2NGOVhsSTM1aVVIYVNjcHVQWjlXUDZ5eXAxYUNjclMwOGNMSXZqOGtPQVc4eFpTd0V3Rl9XSnZfdWd3NGJFRlhEV2x1aV91RXcwQVBXNWQyTE5hd3dHd0tPeVNEaGZxNFpUMTVLNm8xZ3Y1Q0EtYmxSbHptMTgwTzRDS0JxOWxJanZseGRhWHE5VkxxYVY0YkYxU1Q0cFZPa0lXMnlOTEhwMVpoYkxrZkVJc0tlanE4RC1hQWhsQWJXelFvemRSMnlDMzF4NHc9PXxg5O_KaJDVq4lIdQgeHiHcnSMw1IEPDyg3-9XEIBBPxw==; Path=/; Expires=Mon, 15 May 2215 20:47:49 UTC; Max-Age=6307200000

2015/07/10 15:56:04 /routers/api/Auth.go:70: securecookie: expired timestamp

The cookies seem to all expire after 30 days regardless of what we set MaxAge to. Any help you be huge for us. Thank you!

Alright, I've mocked up a minimal demo program where I set MaxAge on the entire store to 2 months (86400 * 60):

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte("some-appropriately-auth-key"))

func SomeHandler(w http.ResponseWriter, r *http.Request) {
    session, err := store.Get(r, "example")
    if err != nil {
        http.Error(w, "No good!", 500)
        return
    }

    session.Values["gorilla"] = "sessions!"
    err = session.Save(r, w)
    if err != nil {
        http.Error(w, "No good!", 500)
        return
    }

    fmt.Fprintf(w, "%v", session.Values["gorilla"])
}

func main() {
    store.Options = &sessions.Options{
        MaxAge: 86400 * 60, // 2 months
    }

    http.HandleFunc("/", SomeHandler)
    log.Fatal(http.ListenAndServe(":8002", nil))
}

This gives me the expiry time I'd expect:

Expiry time test

Manually setting my clock forward ~33 days also nets me the expired timestamp error from securecookie - which is in error.

Clearing the cookies in the browser, resetting the clock back to now() and then forcing MaxAge on the underlying *securecookie.SecureCookie via the following:

    store.Options = &sessions.Options{
        MaxAge: 86400 * 60, // 2 months
    }
    for _, s := range store.Codecs {
        if cookie, ok := s.(*securecookie.SecureCookie); ok {
            cookie.MaxAge(86400 * 90)
        }
    }

This still gives me a browser cookie with an expiry +2 months in the future because the maxAge field of each codec doesn't impact what the browser sees when we write out the http.Cookie. However, setting my clock +33 days into the future does not generate the expired timestamp error.

So I did some digging:

TL;DR: The root cause is that the http.Cookie written to the browser gets the timestamp we set in our options, but the HMAC for the underlying securecookie instance is a concatenation of date|value|mac - where date is the _default_ MaxAge that our Options aren’t fixing. The HMAC validation subsequently fails once we hit > 30 days.

The fix will be for sessions.NewCookieStore to range over the codecs it creates and apply Options.MaxAge to each *securecookie.SecureCookie instance to allow it to correct for the mismatch between the cookie expiry and the HMAC timestamp validation.

cc/ @kisielk to validate my findings before I push a fix.

PS: Unrelated to this, it’s best practice to open a single DB pool in your program, be that via a global or by explicitly passing a pointer. Opening and closing the pool per-request has a performance hit. *sql.DB is safe for concurrent access.

Note that this also affects the majority of store implementations, as they all use securecookie for saving the ID to their backend store.

redistore has half of a correct implementation if you manually call SetMaxAge but doesn't directly apply this for the default case. It just so happens that the default (30 days) is the same as securecookie's.

From a quick look it appears that _all_ other stores are affected—setting a MaxAge via the Options struct is not applied to the underlying securecookie instances generated from CodecsFromPairs. Most end users would not have run into this bug as I suspect most users set expiry dates _less_ than what securecookie is implicitly embedding into the HMAC.

FWIW: I also believe that "far future" cookie expiries aren't great unless you have a very specific need for it.

I would also like to point out that the current time stamp is completely unencrypted in the current implementation. I could, theoretically, base64 decode the cookie, change the time stamp to the current day, base64 encode the cookie, and then the MaxAge property wouldn't matter at all, even if you did fix the options.MaxAge problem.

Yea the "far future" cookie was just for a test, we have the MaxAge set to 0 on our production server now. We found the issue over the weekend and wrote a fix similar, but slightly less concise than yours.

Thanks for looking into the issue!

@wbaron - no problems. We'll get it fixed in the upstream package soon and
I'll ping you once done so you don't have to maintain your own fork (if you
don't want to).

@marksalpeter - Can you point out where? securecookie MACs the
name|date|value before base64 encoding it for a string representation. If
you decode on the client-side you can't modify the timestamp there (it's
MAC'ed—there's nothing you can actually mod) and if you tried to, the
cookie should fail validation on decode (request) since the MAC would fail
to correctly decode against the hash/auth key. Ref:
https://github.com/gorilla/securecookie/blob/master/securecookie.go#L185-L191

On Tue, Jul 14, 2015 at 10:40 PM wbaron [email protected] wrote:

Yea the "far future" cookie was just for a test, we have the MaxAge set to
0 on our production server now. We found the issue over the weekend and
wrote a fix similar to yours, with a bit less concision.

Thanks for looking into the issue!


Reply to this email directly or view it on GitHub
https://github.com/gorilla/sessions/issues/48#issuecomment-121257995.

@elithrar my mistake. when I decode the cookies I get [timestamp]|[MAC'ed info] I just assumed that the time stamp before the max info was what you were using.

@elithrar what you propose sounds good to me. Shame that it has to be applied to every store individually :/

@kisielk - Definitely a shame. I wanted to avoid touching the third party
stores but there wasn't a clean way to effectively couple third party store
Options.MaxAge fields with the s.MaxAge method in securecookie.

I'm considering providing a func CodecMaxAge(codecs []Codec, age int) []Codec function in securecookie (downside: can't be a method on an
interface + adds to the public API) that internally does the type
assertion + calls s.MaxAge(age) on each Codec. This keeps redundant (loop
over slice, type assert to cookie type) code out of the third party stores
as they'll just need to call securecookie.CodecMaxAge(mystore.Codecs, mystore.opts.MaxAge) in their NewXXXXStore function to set the
underlying s.maxAge field.

Open to better ideas but this seems to be the simplest.

On Fri, Jul 17, 2015 at 8:03 AM Kamil Kisiel [email protected]
wrote:

@elithrar https://github.com/elithrar what you propose sounds good to
me. Shame that it has to be applied to every store individually :/


Reply to this email directly or view it on GitHub
https://github.com/gorilla/sessions/issues/48#issuecomment-122133977.

Yeah I think's fine. I'll send out some PRs to the other stores to use it once you land the change here.

For tracking purposes:

  • [x] [gorilla/sessions](https://github.com/gorilla/sessions)
  • [ ] [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/bradleypeabody/gorilla-sessions-memcache](https://github.com/bradleypeabody/gorilla-sessions-memcache) - Memcache
  • [ ] [github.com/hnakamur/gaesessions](https://github.com/hnakamur/gaesessions) - Memcache on GAE
  • [x] [github.com/kidstuff/mongostore](https://github.com/kidstuff/mongostore) - MongoDB
  • [ ] [github.com/srinathgs/mysqlstore](https://github.com/srinathgs/mysqlstore) - MySQL
  • [x] [github.com/antonlindstrom/pgstore](https://github.com/antonlindstrom/pgstore) - PostgreSQL
  • [ ] [github.com/boj/redistore](https://github.com/boj/redistore) - Redis
  • [x] [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

I'll track these off once PR's have been merged.

This issue has been automatically marked as stale because it hasn't seen a recent update. It'll be automatically closed in a few days.

Looking over this issue after running across it from another package, it doesn't look like it should be closed...@elithrar's checklist still needs another 10 checks, unless that information is out-of-date.

It should be accurate: but not all stores are actively maintained or had a PR submitted.

This issue has been automatically marked as stale because it hasn't seen a recent update. It'll be automatically closed in a few days.

This issue has been automatically marked as stale because it hasn't seen a recent update. It'll be automatically closed in a few days.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CasperHK picture CasperHK  ·  11Comments

elithrar picture elithrar  ·  25Comments

danvonk picture danvonk  ·  9Comments

gtaylor picture gtaylor  ·  7Comments

cless picture cless  ·  23Comments