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.
Related
i receive a response body "bad request" with "httptest.Client().Postform"
type testServer struct {
*httptest.Server
}
func newTestServer(t *testing.T, h http.Handler) *testServer {
ts := httptest.NewTLSServer(h)
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatal(err)
}
ts.Client().Jar = jar
ts.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return &testServer{ts}
}
func (ts *testServer) postForm(t *testing.T, urlPath string, form url.Values) (int, http.Header, string) {
rs, err := ts.Client().PostForm(ts.URL+urlPath, form)
if err != nil {
t.Fatal(err)
}
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
return rs.StatusCode, rs.Header, string(body)
}
I don't know where is the problem, i have also verified the url it's correct.
Always badrequest with POST but with GET request it's works fine.
this is the handler object :
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.Authenticated)
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
standart := alice.New(app.recoverPanic, app.logRequest, securityHeaders)
return standart.Then(router)
}
the test function :https://go.dev/play/p/k45-JYTYCOS
the app.userSignupPost:
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
var form userSignupForm
err := app.decodPostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.Name), "name", "this field must not be blank")
form.CheckField(validator.NotBlank(form.Email), "email", "this field must not be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "this field must be a valid email address")
form.CheckField(validator.NotBlank(form.Password), "password", "this field must not be blank")
form.CheckField(validator.MinChars(form.Password, 8), "password", "password must bee at least 8 caracter long")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "signup.tmpl.html", data)
return
}
err = app.users.Insert(form.Name, form.Email, form.Password)
if err != nil {
if errors.Is(err, models.ErrDuplicateEmail) {
form.AddFieldError("email", "Email already exist")
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "signup.tmpl.html", data)
} else {
fmt.Println("error user postform")
app.serverError(w, err)
}
return
}
app.sessionManager.Put(r.Context(), "flash", "Signup Successful. Please log in")
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
}
It appears that you're using https://github.com/justinas/alice to register handlers - you don't want to do this. That package is for middleware chaining - e.g. "before all requests to this URL, first authenticate the request" - you'd put the authentication into a middleware function and then add it to the chain.
So every POST /user/signup request is getting passed first to app.userSignup() (what you are using to handle GET requests). This is because calling alice.ThenFunc() appends the passed handler to the chain and then returns the entire chain as a handler - you need to read this part of the Alice docs carefully before using it.
Replace this line:
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
with
router.Handler(http.MethodPost, "/user/signup", http.HandlerFunc(app.userSignupPost))
You may not need the additional decoration of http.HandlerFunc() - try it with and without to see what works. I cannot say for sure without knowing what the body of app.userSignupPost() looks like (same for the other handler functions as well).
You'll then need to do the same for the other handler registration lines - you shouldn't be using middleware chaining for your end handlers. An http.Handler is used for saying, "send any request to path /PP/ppp with method XXXX to this function." Middleware chaining is for preprocessing (authentication, authorization, etc.) - a whole host of things can be done there, but end request handling shouldn't be one of them.
I'm still curious if your use of PostForm() is going to cause you issues for the reason I cited in my comment on your question - try a raw Post() and see if the behavior differs, but after refactoring to take out the alice goop (at least temporarily). When testing a handler, I'd start off with a much more minimal approach - test that the handler itself works before muddying the waters with both alice and what looks like this package.
I think i found the problem , the session cookie are not the same for get and post request. i don't know why it has changed.They use the same http.Client()
I'm building an API that scrapes some data off a webpage.
To do so, i need to send a GET request to a home page, scrape a 'RequestVerificationToken' from the HTML, then send another POST request to the same URL with a username, password, and the RequestVerificationToken.
I've been able to do this previously with Python:
session_requests = requests.session()
result = session_requests.get(LOGIN_URL)
parser = createBS4Parser(result.text)
return parser.find('input', attrs={'name': '__RequestVerificationToken'})["value"]
pageDOM = session_requests.post(
LOGIN_URL,
data=requestPayload, //RequestVerificationToken is in here
headers=requestHeaders
)
It seems like when i reuse the session_requests variable in Python, it's reusing the previous instance of the HTTP request.
However, when i try to do this in Go, I get an error due to an invalid token. I assume that this is because for the POST request, Go is using a new instance.
Is there any way I can get the same behavior from Go as I was with Python?
package main
import (
"fmt"
"log"
"github.com/gocolly/colly"
"github.com/gocolly/colly/proxy"
)
func main() {
//initiates the configuration
c := colly.NewCollector(colly.AllowURLRevisit())
//defining the proxy chain
revpro, err := proxy.RoundRobinProxySwitcher("socks5://127.0.0.1:9050", "socks5://127.0.0.1:9050")
if err != nil {
log.Fatal(err)
}
c.SetProxyFunc(revpro)
//parsing the required field from html we are extracting the csrf_token required for the login
c.OnHTML("form[role=form] input[type=hidden][name=CSRF_TOKEN]", func(e *colly.HTMLElement) {
csrftok := e.Attr("value")
fmt.Println(csrftok)
//posting the csrf value along with password
err := c.Post("https://www.something.com/login.jsp", map[string]string{"CSRF_TOKEN": csrftok, "username": "username", "password": "password"})
if err != nil {
log.Fatal(err)
}
return
})
//The website to visit
c.Visit("https://www.something.com/login.jsp")
//maintaining the connection using clone not initiating a callback request
d := c.Clone()
d.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
fmt.Printf("Link found: %q -> %s\n", e.Text, link)
})
d.Visit("https://skkskskskk.htm")
}
I've constructed a simple script which goes on a website, and does a few things on there (automating a checkout process on lego.com)
This is all done using the go http client with a cookiejar on it, but when I try to print out the cookies after all the activity on the site, nothing prints - not sure if I'm doing something wrong here.
type Program struct {
taskInfo task.Task
client http.Client
jar *cookiejar.Jar
// Task Specific Variables
maxOrderQty string
sessionID string
sku string
shipMethodUID string
}
The HTTP client is initialized below
func (pr *Program) initializeClient() {
pr.jar, _ = cookiejar.New(nil)
pr.client = http.Client{
Timeout: time.Second * 10,
Jar: pr.jar,
}
}
After that, i make a few calls to the same domain (mixture of GET and POST) using said client. When I try to print out the cookies, nothing prints.
func (pr *Program) getSessionCookies() {
log.Debug("Getting Cookies")
u, _ := url.Parse("https://www.lego.com/")
for _, cookie := range pr.jar.Cookies(u) {
fmt.Printf(" %s: %s\n", cookie.Name, cookie.Value)
}
}
Go's http package support redirects by default, but the cookie jar must be refreshed, specially if the domain changes.
I spent a great deal of time debugging it some time ago, the solution I found was to specify a redirect handler:
client.CheckRedirect = getRedirectHandler(req, &client)
Which had the following code:
func getRedirectHandler(req *Request, client *http.Client) redirectHandler {
fn := func(request *http.Request, via []*http.Request) error {
if req.FollowRedirect {
log.Debug(fmt.Sprintf("Redirecting to %s", request.URL.String()))
jar, err := getCookieJarWithURL(request.URL.String(), req.Cookies)
if err != nil {
return err
}
client.Jar = jar
return nil
}
return errors.New("Redirect not allowed")
}
return fn
}
The full code can be found in this gist:
I hope that saves you some time :)
I'm starting to build a regular web app with golang and Angular2, and most importantly I'm trying to secure my login with the help of auth0.com. I download the quickstart code from here and try to run the code, it worked for a while and then next time I run it, the /tmp/session file cannot be found any more.
Here are some basic idea of the code auth0.com provides.
1. Initialize the gorilla sessions filesystemstore
2. Then start the authentification process
code is provided below
app.go
var (
Store *sessions.FilesystemStore
)
func Init() error {
Store = sessions.NewFilesystemStore("", []byte("something-very-secret"))
gob.Register(map[string]interface{}{})
return nil
}
login.go
func LoginHandler(w http.ResponseWriter, r *http.Request) {
domain := os.Getenv("AUTH0_DOMAIN")
aud := os.Getenv("AUTH0_AUDIENCE")
conf := &oauth2.Config{
ClientID: os.Getenv("AUTH0_CLIENT_ID"),
ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"),
RedirectURL: os.Getenv("AUTH0_CALLBACK_URL"),
Scopes: []string{"openid", "profile"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://" + domain + "/authorize",
TokenURL: "https://" + domain + "/oauth/token",
},
}
if aud == "" {
aud = "https://" + domain + "/userinfo"
}
// Generate random state
b := make([]byte, 32)
rand.Read(b)
state := base64.StdEncoding.EncodeToString(b)
session, err := app.Store.Get(r, "state")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["state"] = state
err = session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
audience := oauth2.SetAuthURLParam("audience", aud)
url := conf.AuthCodeURL(state, audience)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
The Error log is
open /tmp/session_46CNLLHBKD... : no such file or directory
I try to understand the code and findout that error log comes from login.go line 39(session, err := app.Store.Get(r, "state")). And I started to track down the code and find out.
login.go:39 -->store.go: 180-->session.go:132-->store.go:186-->store.go:272
you can find store.go and session.go here.
The error log comes from this line: fdata, err := ioutil.ReadFile(filename)
Through the whole process I have not found any function call to save the session file.
I don't understand this error and I don't know why I can run the code at the very beginning, please help me with this problem.
Your any suggestion will be greatly appreciated.Thanks a lot.
I figure out the answer myself
It turns out that I changed my secret key while initializing the gorilla session filesystemstore, but I have not deleted my cookie file in chrome, so it cannot find the tmp sesiion file needed.
I change the key, then delete the coorsponding cookie file and everything is ok now.
I am developing a RESTful API with Go but owing to app configuration, authentication etc I have quite a few global variables.
I am using Julien Schmidt's httprouter because of popular recommendation and am looking for a feasible way to avoid global variables.
Here is some of the code.
I am using a middleware for authenticating a user using gorrila/securecookie.
func AuthMiddleware(handler httprouter.Handle, isLoggedIn bool) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {if isLoggedIn {
currUser, err := GetCurrentUser(r)
if currUser.Username != "" {
handler(w, r, ps)
return
}
responseWriter(w, false, "User not logged in", nil)
return
}
handler(w, r, ps)
}
}
After this, I want to be able to use the currUser object inside the handler that the request is forwarded to such as this one instead of calling GetCurrentUser once again
func foobar(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
var currQuestion CurrQuestions
err = Db.db.Raw("SELECT * FROM users WHERE U.user_id = ?", currUser.UserID).Find(&currQuestion).Error
if err != nil {
responseWriter(w, false, "Internal Server Error", "")
return
}
responseWriter(w, true, "", GameData{})
}
You can use the context package. Values stored in context are only accessible in the current controller. In your middleware (at the end) do the following:
ctx := context.WithValue(r.Context(), "User", user)
r = r.WithContext(ctx)
handler(w, r, ps)
Now you can fetch this value in you handler:
user, ok := r.Context().Value("User").(<type of your user struct>)
The string "User" is the key that is used to save the value. You can change it any way you like, but a const is a good idea.
if ok is true, the user has been fetched and is the correct type. If ok is false, the user was not set or it has the wrong type.