I have been pulling my hair out recently, I need to find a way to save cookies after a post request to a URI, so that I can send requests to other endpoints and maintain that session. I am trying to add an item to cart but without saving the cookies the cart will be empty. (shopping cart) I am currently using this to handle cookies but doesn't seem to forward the cookies to next request:
func (c *CookieClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
for {
zap.S().Info("Saving Cookie")
if err := c.Client.Do(req, resp); err != nil {
return err
}
statusCode := resp.Header.StatusCode()
if statusCode != fasthttp.StatusMovedPermanently &&
statusCode != fasthttp.StatusFound &&
statusCode != fasthttp.StatusSeeOther &&
statusCode != fasthttp.StatusTemporaryRedirect &&
statusCode != fasthttp.StatusPermanentRedirect {
break
}
location := resp.Header.PeekBytes(strLocation)
if len(location) == 0 {
return fmt.Errorf("Redirect with missing Location header")
}
u := req.URI()
u.UpdateBytes(location)
resp.Header.VisitAllCookie(func(key, value []byte) {
c := fasthttp.AcquireCookie()
defer fasthttp.ReleaseCookie(c)
c.ParseBytes(value)
if expire := c.Expire(); expire != fasthttp.CookieExpireUnlimited && expire.Before(time.Now()) {
zap.S().Info("Deleting Expired Cookie")
req.Header.DelCookieBytes(key)
} else {
req.Header.SetCookieBytesKV(key, c.Value())
}
})
}
return nil
}
Probably the authors can have an efficient way:
You can retrieve the cookie with the following method, then you can reassign it to another request.
func ParseTokenFromRequest(ctx *fasthttp.RequestCtx) string {
token := string(ctx.Request.Header.Cookie("GoLog-Token")) // GoLog-Token is the hardcoded name of the cookie
return token
}
Then you can create the cookie with the value already retrieved:
//CreateCookie Method that return a cookie valorized as input (GoLog-Token as key)
func CreateCookie(key string, value string, expire int) *fasthttp.Cookie {
if strings.Compare(key, "") == 0 {
key = "GoLog-Token"
}
log.Debug("CreateCookie | Creating Cookie | Key: ", key, " | Val: ", value)
authCookie := fasthttp.Cookie{}
authCookie.SetKey(key)
authCookie.SetValue(value)
authCookie.SetMaxAge(expire)
authCookie.SetHTTPOnly(true)
authCookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
return &authCookie
}
And then you can forward the cookie or save it into a (maybe in-memory) db:
authcookie := CreateCookie("GoLog-Token", token, cfg.Redis.Token.Expire)
ctx.Response.Header.SetCookie(authcookie)
// store cookie here
Related
go version: 1.19
gin version (or commit ref): 1.8.1
operating system: Ubuntu
I have a saas project which is based upon Rest APIs. All apis are developed in GO using gin package. When the user logs in then I set current user details in the request context so that I can access these details furthere to display some data. However I had a case in which 2 requests hits in parallel & the context values for the 1st request are override with the context values in the 2nd request. Due to this, my data is displaying wrong.
package main
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
)
func main() {
g := gin.Default()
g.Use(ParseJWTToken)
g.GET("/hello/:name", hello)
g.Run(":9000")
}
func hello(c *gin.Context) {
c.Keys = make(map[string]interface{})
c.Keys["current_user_id"] = 10
c.Keys["current_user_name"] = c.Param("name")
fmt.Println(c.Keys)
c.String(200, "Hello %s", c.Param("name"))
}
var role, userName string
var userId float64
func ParseJWTToken(c *gin.Context) {
merchantDatabase := make(map[string]interface{})
if values, _ := c.Request.Header["Authorization"]; len(values) > 0 {
bearer := strings.Split(c.Request.Header["Authorization"][0], "Bearer")
bearerToken := strings.TrimSpace(bearer[1])
var userAgent string
var userAgentCheck bool
if values, _ := c.Request.Header["User-Agent"]; len(values) > 0 {
userAgent = values[0]
}
_ = config.InitKeys()
token, err := jwt.Parse(bearerToken, func(token *jwt.Token) (interface{}, error) {
return config.SignKey, nil
})
if err != nil {
c.Abort()
return
}
if !token.Valid {
c.Abort()
return
}
if len(token.Claims.(jwt.MapClaims)) > 0 {
for key, claim := range token.Claims.(jwt.MapClaims) {
if key == "user_agent" {
if claim == userAgent {
userAgentCheck = true
}
}
if key == "role" {
role = claim.(string)
}
if key == "id" {
userId = claim.(float64)
}
if key == "name" {
userName = claim.(string)
}
}
}
merchantDatabase["userid"] = userId
merchantDatabase["role"] = role
merchantDatabase["username"] = userName
c.Keys = merchantDatabase
if userAgentCheck {
c.Next()
} else {
c.Abort()
return
}
} else {
c.Abort()
return
}
}
This issue is not produced every time for parallel requests.
How can I fix that ?
I have used global variables for the details that were overridden. Declaring these inside the middleware fixed the issue. Find complete thread here: https://github.com/gin-gonic/gin/issues/3437
func Login(c echo.Context) error {
user := &users.User{}
if err := c.Bind(&user); err != nil {
return err
}
return token.SigIn(c, user.Email, user.Password)
}
This is my Login function that retrieve the token when the user send the requests.
the Signin func that handle the token
func SigIn(c echo.Context, email, password string) error {
user := users.User{}
db := database.SetUp()
if err := db.Where("email = ?", email).First(&user).Error; gorm.IsRecordNotFoundError(err) {
restErr := errors.NewBadRequestError("Invalid credentials")
return c.JSON(http.StatusBadRequest, restErr)
}
if user.VerifyPassword(password) != nil {
restErr := errors.NewUnauthorizedError("Couldn't log you in with these credentials")
return c.JSON(http.StatusUnauthorized, restErr)
}
//user is successfull
return CreateToken(c)
}
the CreateToken func is as follow
type TokenJWT struct {
Token string `json:"token"`
}
func CreateToken(c echo.Context) error {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["authorized"] = true
claims["name"] = "Pascal Gaetan"
claims["exp"] = time.Now().Add(time.Hour * 1).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("my_secret_key"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, TokenJWT{
Token: t,
})
}
when everyhting is succesfull, i would like to get the authenticated user through an URL /api/me that calls a Me function
Let me split your question into two parts: the first one is how to easily encode and decode user in or from JWT token and the second part is how to write a generic code which can retrieve user from everywhere.
From your example I mentioned that you created a MapClaims but to reduce parsing complexity it will be better to create a token using a custom claims type. If you are using dgrijalva/jwt-go, then according to documentation you can do something like that
type UserClaims struct {
Name string `json:"name"`
jwt.StandardClaims
}
// encode it as before, but with your created type
t := jwt.New(signer)
userClaims := &UserClaims{Name: "Burmese"}
t.Claims = userClaims
tokenString, err = t.SignedString(]byte("my_secret_key"))
then you can parse your user in your router/framework middleware with
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("my_secret_key"), nil
})
if claims, ok := token.Claims.(*UserClaims); ok && token.Valid {
fmt.Printf("%v %v", claims.Name, claims.StandardClaims.ExpiresAt)
} else {
fmt.Println(err)
}
This example was adopted from an official documentation here
Now you know how to parse authenticated user struct with ease and the next logic move is to wrap it into your middleware. Whether there are a lot of implementation details like you can retrieve JWT from cookie, header or query, also defining some ordering on them, the gist the following: you should have wrapped abovementioned code into your middleware and after parsing the struct you can pass it via your request context. I don't use echo and other frameworks, but for pure net/http you can pass your parsed struct from middleware with
context.WithValue(ctx, UserCtxKey, claims)
Hope it helps!
This is a fairly common design pattern to create an authenticated client and then call various action methods on it. You could do something like the following:
type Client struct {
... // other members
token string // unexported unless there is a special reason to do otherwise
}
func NewClient(c echo.Context, email, password string) (*Client, error) {
user := users.User{}
cl := Client{}
... // your original method
cl.token = token
return &cl, nil
}
func (c *Client) DoSomething(...) ... { ... }
I'm a bit new to Go and I'm having trouble converting a response object from an API call into different structures based on the request type
Essentially, I have one func which sends out a request
func (fpc *FPClient) request(path string, method string, params interface{}, Token string, response interface{}) *dto.AppError {
client := &http.Client{
Timeout: time.Second * 15,
}
requestBody, err := json.Marshal(params)
if err != nil {
//
}
req, _ := http.NewRequest(method, path, bytes.NewBuffer(requestBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Cookie", fmt.Sprintf("cookie=%s;", Token))
req.SetBasicAuth(fpc.username, fpc.password)
resp, err := client.Do(req)
if err != nil {
//
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
//
}
if FPErr := fpc.processErrors(resp, body); FPerr != nil {
return FPErr
}
responseData := FPApiSuccess{Head: response, Body: response}
if err := json.Unmarshal(body, &responseData); err != nil {
//
}
fmt.Printf("\n\n client Response : \n %+v \n", responseData.Body)
return nil
}
The struct for FPApiSuccess is:
type FPApiSuccess struct {
Body interface{} `json:"body"`
Head interface{} `json:"head"`
}
Right now, there are 2 calling functions and the API response expected is a bit different for both
Both API responses have the below structure
{
"head": {},
"body": {}
}
However, the nested details in each key is different based on the API used.
I want to capture the head and body keys in a struct argument I give and send it back to the calling function. The response argument in the request function is a different struct type based on the calling function.
I'm unable to get this to work - I'm only getting back a blank struct from the request function. This is the fmt.PrintF log
client Response :
&{Body:{BOrderID:0 CC: Exch: EOID: Type: Local:0 Message: } Head:{ResponseCode: Status: StatusDescription:}}
This is an empty struct - ideally, it should be populated with the values retrieved from the API.
For reference, heres the struct passed as an argument as response in the request function:
type FPApiResponse struct {
Body FPBodyResponse `json:"body"`
Head FPHeadResponse `json:"head"`
}
type FPHeadResponse struct {
ResponseCode string `json:"responseCode"`
Status string `json:"status"`
StatusDescription string `json:"statusDescription"`
}
type FPBodyResponse struct {
BOrderID int `json:"BOrderID"`
CC string `json:"CC"`
Exch string `json:"Exch"`
EOID string `json:"EOID"`
Type string `json:"Type"`
Local int `json:"Local"`
Message string `json:"Message"`
}
Update
So I did this; instead of
responseData := FPApiSuccess{Head: response, Body: response}
I did this
responseData := fivePaisaApiSuccess{}
So now, I get the below in console
Client Response :
{Body:map[BOrderID:0 CC:52715111 Type:D Local:0 Message:Invalid Session ] Head:map[responseCode:5POrdReq status:0 statusDescription:Success]}
So essentially, this works, but the calling function doesn't seem to get the proper response:
Here's the calling function
func (fpc *FPClient) PlaceOrder(orderParams dto.OrderBodyParams, variety string, Token string) (string, *dto.AppError) {
var result FPApiResponse
headParams := dto.FFPOrderHeadParams{
//
}
FPOrderParams := dto.FPOrderParams{
//
}
FPErr := fpc.request(FPURL+FPPlaceOrder, http.MethodPost, FPOrderParams, brokerAccessToken, &result)
if FPErr != nil {
return "", FPErr
}
fmt.Printf("\n\n Client result : \n %+v \n", result)
if result.Head.Status != "0" {
//
}
if result.Body.Status != 0 {
//
}
return strconv.Itoa(result.Body.Broker), nil
}
The result value is blank:
{Body:{BOId:0 CC: Exch: Type: Local:0 Message: Status:0} Head:{ResponseCode: Status: StatusDescription:}}
I don't understand, this pointer is getting populated in the request function
Here's the struct I'm passing to the request:
type FPApiResponse struct {
Body FPBodyResponse `json:"body"`
Head FPHeadResponse `json:"head"`
}
I am writing a web app using the martini-contrib session library. I seem to have run into an issue though. My session don't seem to be moving between requests in the browser.
I have followed the exact guidelines that were outline in the example code, yet my code does not work. Below is the pertinent parts:
Login page:
m.Get("/login", binding.Bind(LoginForm{}), func(r render.Render, session sessions.Session, form LoginForm) string {
// Get info from the database.
conn, err := sql.Open("sqlite3", "ocdns.db")
defer conn.Close()
// Prepare the statement.
stmt, err := conn.Prepare(`
SELECT user_id, username, name_first, name_last, role, team_id
FROM User
WHERE username = ? AND password = ?
LIMIT 1;
`)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// Query the database and set appropriate items if a row was actually returned.
var id string
var username string
var name_first string
var name_last string
var role string
var team_id string
err = stmt.QueryRow(form.Username, form.Password).Scan(&id, &username, &name_first, &name_last, &role, &team_id)
if err != nil {
log.Print("!! Bad login from " + form.Username + " with " + form.Password)
log.Fatal(err)
} else {
log.Print(">" + id + "<")
log.Print(">" + username + "<")
log.Print(">" + name_first + "<")
log.Print(">" + name_last + "<")
log.Print(">" + role + "<")
log.Print(">" + team_id + "<")
session.Set("id", id)
session.Set("username", username)
session.Set("name_first", name_first)
session.Set("name_last", name_last)
session.Set("role", role)
session.Set("team_id", team_id)
v := session.Get("name_first")
if v == nil {
log.Print("!! Uh oh.")
}
log.Print(v.(string))
return "OK"
}
return "Bad"
})
Session check page:
m.Get("/session", func(session sessions.Session) string {
var c Context
i := session.Get("id")
if i == nil {
c.Id = -1
}
c.Id, _ = strconv.Atoi(i.(string))
i = session.Get("username")
if i == nil {
log.Print("!! username")
}
if vs, ok := i.(string); ok {
c.Username = vs
} else {
log.Print(vs)
}
log.Print(c)
j, _ := json.Marshal(c)
return string(j)
})
Context struct:
type Context struct {
Id int `json:"id"`
Username string `json:"username"`
NameFirst string `json:"name_first"`
NameLast string `json:"name_last"`
Role string `json:"role"`
TeamId int `json:"team_id"`
}
Finally, my session declaration:
// Create session store.
store := sessions.NewCookieStore([]byte("secret123"))
m.Use(sessions.Sessions("my_session", store))
I have tried emptying my cache to get it to work, but it won't. I did look in Chrome's developer tools to see if storage was being used and I did find that the session moved between two pages, but the output of the session request was an empty Context json stucture.
What I need is to figure out why my session data seems to be getting lost between requests and what I can do to fix it. I have looked on here and while nothing fits my exact problem, I did find that there were ones that had problems with their web server configuration.
Thanks in advance!
You should change your first function call as follow to make sure it must goto session first before login.
m.Get("/login", SOME_FUNC_TO_SEESION, func(r render.Render, session sessions.Session, form LoginForm) string {
Here is sample video, hope it help you. Your code should work well.
https://gophercasts.io/lessons/8-auth-part-2
Session Variables are not maintained across request while using gorilla sessions web toolkit.
When I start the server and type localhost:8100/ page is directed to login.html since session values do not exist.After I login I set the session variable in the store and the page is redirected to home.html. But when I open a new tab and type localhost:8100/ the page should be directed to home.html using already stored session variables, but the page is instead redirected to login.html.
Following is the code.
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gocql/gocql"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"net/http"
"time"
)
var store = sessions.NewCookieStore([]byte("something-very-secret"))
var router = mux.NewRouter()
func init() {
store.Options = &sessions.Options{
Domain: "localhost",
Path: "/",
MaxAge: 3600 * 1, // 1 hour
HttpOnly: true,
}
}
func main() {
//session handling
router.HandleFunc("/", SessionHandler)
router.HandleFunc("/signIn", SignInHandler)
router.HandleFunc("/signUp", SignUpHandler)
router.HandleFunc("/logOut", LogOutHandler)
http.Handle("/", router)
http.ListenAndServe(":8100", nil)
}
//handler for signIn
func SignInHandler(res http.ResponseWriter, req *http.Request) {
email := req.FormValue("email")
password := req.FormValue("password")
//Generate hash of password
hasher := md5.New()
hasher.Write([]byte(password))
encrypted_password := hex.EncodeToString(hasher.Sum(nil))
//cassandra connection
cluster := gocql.NewCluster("localhost")
cluster.Keyspace = "gbuy"
cluster.DefaultPort = 9042
cluster.Consistency = gocql.Quorum
session, _ := cluster.CreateSession()
defer session.Close()
//select query
var firstname string
stmt := "SELECT firstname FROM USER WHERE email= '" + email + "' and password ='" + encrypted_password + "';"
err := session.Query(stmt).Scan(&firstname)
if err != nil {
fmt.Fprintf(res, "failed")
} else {
if firstname == "" {
fmt.Fprintf(res, "failed")
} else {
fmt.Fprintf(res, firstname)
}
}
//store in session variable
sessionNew, _ := store.Get(req, "loginSession")
// Set some session values.
sessionNew.Values["email"] = email
sessionNew.Values["name"] = firstname
// Save it.
sessionNew.Save(req, res)
//store.Save(req,res,sessionNew)
fmt.Println("Session after logging:")
fmt.Println(sessionNew)
}
//handler for signUp
func SignUpHandler(res http.ResponseWriter, req *http.Request) {
fName := req.FormValue("fName")
lName := req.FormValue("lName")
email := req.FormValue("email")
password := req.FormValue("passwd")
birthdate := req.FormValue("date")
city := req.FormValue("city")
gender := req.FormValue("gender")
//Get current timestamp and format it.
sysdate := time.Now().Format("2006-01-02 15:04:05-0700")
//Generate hash of password
hasher := md5.New()
hasher.Write([]byte(password))
encrypted_password := hex.EncodeToString(hasher.Sum(nil))
//cassandra connection
cluster := gocql.NewCluster("localhost")
cluster.Keyspace = "gbuy"
cluster.DefaultPort = 9042
cluster.Consistency = gocql.Quorum
session, _ := cluster.CreateSession()
defer session.Close()
//Insert the data into the Table
stmt := "INSERT INTO USER (email,firstname,lastname,birthdate,city,gender,password,creation_date) VALUES ('" + email + "','" + fName + "','" + lName + "','" + birthdate + "','" + city + "','" + gender + "','" + encrypted_password + "','" + sysdate + "');"
fmt.Println(stmt)
err := session.Query(stmt).Exec()
if err != nil {
fmt.Fprintf(res, "failed")
} else {
fmt.Fprintf(res, fName)
}
}
//handler for logOut
func LogOutHandler(res http.ResponseWriter, req *http.Request) {
sessionOld, err := store.Get(req, "loginSession")
fmt.Println("Session in logout")
fmt.Println(sessionOld)
if err = sessionOld.Save(req, res); err != nil {
fmt.Println("Error saving session: %v", err)
}
}
//handler for Session
func SessionHandler(res http.ResponseWriter, req *http.Request) {
router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
session, _ := store.Get(req, "loginSession")
fmt.Println("Session in SessionHandler")
fmt.Println(session)
if val, ok := session.Values["email"].(string); ok {
// if val is a string
switch val {
case "": {
http.Redirect(res, req, "html/login.html", http.StatusFound) }
default:
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
} else {
// if val is not a string type
http.Redirect(res, req, "html/login.html", http.StatusFound)
}
}
Can somebody tell me what I am doing wrong. Thanks in advance.
First up: you should never, ever, use md5 to hash passwords. Read this article on why, and then use Go's bcrypt package. You should also parameterise your SQL queries else you are open to catastrophic SQL injection attacks.
Anyway: there are a few problems you need to address here:
Your sessions aren't "sticking" is that you're setting the Path as /loginSession - so when a user visits any other path (i.e. /), the session isn't valid for that scope.
You should be setting up a session store on program initialisation and setting the options there:
var store = sessions.NewCookieStore([]byte("something-very-secret"))
func init() {
store.Options = &sessions.Options{
Domain: "localhost",
Path: "/",
MaxAge: 3600 * 8, // 8 hours
HttpOnly: true,
}
The reason you might set a more specific path is if logged in users are always within a sub-route like /accounts. In your case, that's not what's happening.
I should add that Chrome's "Resource" tab in the Web Inspector (Resources > Cookies) is incredibly useful for debugging issues like these as you can see the cookie expiry, path and other settings.
You're also checking session.Values["email"] == nil, which doesn't work. An empty string in Go is just "", and because session.Values is a map[string]interface{}, you need to type assert the value to a string:
i.e.
if val, ok := session.Values["email"].(string); ok {
// if val is a string
switch val {
case "":
http.Redirect(res, req, "html/login.html", http.StatusFound)
default:
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
} else {
// if val is not a string type
http.Redirect(res, req, "html/login.html", http.StatusFound)
}
We deal with the "not a string" case so we're explicit about what the program should do if the session is not how we expected (client modified it, or an older version of our program used a different type).
You are not checking errors when saving your sessions.
sessionNew.Save(req, res)
... should be:
err := sessionNew.Save(req, res)
if err != nil {
// handle the error case
}
You should get/validate the session in SessionHandler before serving static files (you are doing it in a very roundabout way, however):
func SessionHandler(res http.ResponseWriter, req *http.Request) {
session, err := store.Get(req, "loginSession")
if err != nil {
// Handle the error
}
if session.Values["email"] == nil {
http.Redirect(res, req, "html/login.html", http.StatusFound)
} else {
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
// This shouldn't be here - router isn't scoped in this function! You should set this in your main() and wrap it with a function that checks for a valid session.
router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
}
The problem is you're writing to the response before calling session.Save. That prevents the headers from being written and thus your cookie from being sent to the client.
In the code after session.Query you're calling Fprintf on the response, as soon as this code executes, calling sessionNew.Save essentially does nothing. Remove any code that writes to the response and try again.
I guess gorilla toolkit's session ought to return an error when calling Save if the response has already been written to.
Following on from the comment chain, please try removing the Domain constraint from the session options, or replace it with a FQDN that resolves (using /etc/hosts for example).
This appears to be a bug in Chromium where cookies with an explicit 'localhost' domain aren't sent. The issue doesn't seem to present itself in Firefox.
I was able to get your demo working using
store.Options = &sessions.Options{
// Domain: "localhost",
MaxAge: 3600 * 1, // 1 hour
HttpOnly: true,
}
In my case the problem was the Path. I know the question is not about it, but this post appears first when you search Google. So, I was starting the session in a path like:
/usuario/login
So the path was set to /usuario, and then, when I made another requests from / the cookie was not set because / is not same as /usuario
I fixed it by specifying a Path, i know this should be obvious but took me some hours to realize it. So:
&sessions.Options{
MaxAge: 60 * 60 * 24,
HttpOnly: true,
Path: "/", // <-- This is very important
}
More info about general cookies: https://developer.mozilla.org/es/docs/Web/HTTP/Cookies
Use a server side "FilesystemStore" instead of a "CookieStore" to save the session variables. Another alternative would be to update the session as a context variable for the request i.e., store the session in the context and let the browser pass it around in every request, using the context.Set() from the gorilla/context package.
Using "CookieStore" is heavy for the client because as the amount of information stored in the cookie grows, more information is transmitted over the wire for every request and response. The advantage it serves is that there is no need to store the session information on the server side. If it is not a constraint to store session information on the server, the ideal way should be to store login and authentication related information on a server side "non-cookie" session store and just pass a token to the client. The server would maintain a map of the token and session information. The "FilesystemStore" allows you to do this.
Though both the "FilesystemStore" and "CookieStore" implement the "Store" interface, each of their "Save()" function's implementations are slightly different. The source code for both the functions, CookieStore.Save() and FilesystemStore.Save() will help us understand why "CookieStore" is not able to persist the session information. The FilesystemStore's Save() method apart from writing the session information to the response header, also saves the information on the server side session file. In a "CookieStore" implementation, if the browser is not able to send the new modified cookie from a response to the next request, the request might fail. In a "FilesystemStore" implementation, the token that is given to the browser always remains the same. The session information is updated in a file and is fetched based on the requesting token, whenever required.