Sessions variables in golang not saved while using gorilla sessions
Asked Answered
S

5

14

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.

Spine answered 18/2, 2014 at 21:24 Comment(0)
N
30

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/")))
    }
    
Normanormal answered 18/2, 2014 at 21:46 Comment(4)
I added the init() and removed the session.Options which was setting the path to /loginSession, but it still does not work.Spine
It is still not working.I added print statements printing the session variable in session handler and after log in ,this is what I got as output. Session in SessionHandler &{ map[] 0xc2000f3330 true 0xc2000c0ea0 loginSession} Session after logging: &{ map[email:[email protected] name:Tushar] 0xc2001a7930 true Session in SessionHandler &{ map[] 0xc2001a7c00 true 0xc2000c0ea0 loginSession} The last session handler is printed when you open a new tab and enter localhost:8100/Spine
@tushR Can you update your question with a running version of the code that checks/handles all session-related errors and takes into account my suggestions above?Normanormal
I have the router declared outside the main() so its in global scope and is accesible in SessionHandler() and after adding the block to check error in saving session, it does not go inside that block, hence there is no error in saving the session.Spine
C
6

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.

Carlyn answered 11/2, 2015 at 7:54 Comment(1)
I was just having an issue where my cookie wasn't being set using gorilla/sessions. Your answer was spot on. Thanks!Any
T
2

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,
}
Terrellterrena answered 19/2, 2014 at 4:20 Comment(8)
I am using Firefox and after removing the domain , it still does not work.The problem is store.get() in the SessionHandler function does not return the saved session we save in LoginHandler.Spine
Delete the existing cookie from your browser and then set Domain: "" (which is bad, and should only be done in testing!). I managed to get this working after doing that and hard-coding the session values: the / route redirected to '/html/home.html. Deleting the session had it re-direct to /html/login.html`. There are still a lot of issues with your application: you shouldn't reconnect to the DB on every request; check your session errors; and your static file server doesn't do anything. Have a read through golang.org/doc/articles/wiki and look at refactoring this early.Normanormal
I have html and angularjs at the frontend and golang at the backend, so how can setting session variables in golang be reflected as cookies in browser. Do i need to set the session variables from golang into cookies in angularjs code? I don't have much experience at front end so could you kindly explain the flow of creating sessions at backend and getting them reflected at front end.Spine
@tushR if you have an $http call to authenticate that correctly sets a cookie in the response (via Set-Cookie header), any further $http calls to that domain will be "authenticated"/part of the session. Use developer tools to determine whether Set-Cookie is part of the login HTTP response, and then whether the browser is sending that same cookie (via Cookie header) on subsequent $http calls. My analysis of the problem was that you were trying to set a 'localhost' domained cookie, which the browser was ignoring and not sending in subsequent requests.Terrellterrena
@Alex, thanks for the info, but my question still remains the same.Should I set the cookies in AngularJS with session variables in Golang?Spine
I don't understand the question. The server session is not known about by AngularJS. Logging in should set a cookie. Accessing a protected resource (such as "/api/my_profile") will instruct the server to render your profile info if a server session exists (according to the cookie, decrypted by gorilla/sessions), or an unauthenticated error.Terrellterrena
@Alex, so for the login page once the user logs in for the first time, we should set the username and password in the cookie via angularjs and set other profile related info in server's session.Hence next time when the user logs in we should check if username and password exists in browser cookie, if exists, we should load the other profile related info from the server's session?Spine
@Alex If this flow is right, the problem I am facing is, lets say the user logs in for the first time, set the details in browser cookie and save detail in gorilla session, Now when I open a new tab and and i type in "localhost:8100/" it should redirect me to the home page with user's profile loaded from the saved gorilla session, but when i try to retrieve the saved gorilla session(i.e. session, _ := store.Get(req, "loginSession")), it creates a new session with the same name.Spine
W
2

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

Whelan answered 25/10, 2019 at 15:46 Comment(0)
G
0

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.

Godavari answered 13/9, 2017 at 6:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.