I've built an application that uses the Go Gorilla sessions package. Everything seems fine, except when on logout I implement
func logout(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "authsesh")
session.Values["access"] = "denied"
session.Save(r, w)
http.Redirect(w, r, "/", 302)
return
}
Because the page requiring authentication is cached by the browser, it can still be accessed after logout. How can I get around that? Is there a way to prevent the browser from caching the page? There's nothing wrong with the cookie, if I clear the cache and keep the cookie I can see the logout has had the desired effect.
Set the correct cache headers in your handler(s):
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
w.Header().Set("Expires", time.Unix(0, 0).Format(http.TimeFormat))
w.Header().Set("Pragma", "no-cache")
w.Header().Set("X-Accel-Expires", "0")
Note that we set multiple headers to account for proxies and HTTP/1.0 clients.
You can wrap these into middleware you can apply as well:
func NoCache(h http.Handler) http.Handler) {
fn := func(w http.ResponseWriter, r *http.Request) {
// Set the headers
}
return http.HandlerFunc(fn)
}
// In your router
http.Handle("/user-dashboard", NoCache(http.HandlerFunc(YourDashboardHandler))
Related
I'm trying to integrate saml using crewjam library with an open-source app in go.
After authentication test using samltest.id, I want to be redirected to the home page.
I have tried several ways, but nothing works well, i'm using gorilla/mux router:
func login(w http.ResponseWriter, r *http.Request) {
s := samlsp.SessionFromContext(r.Context())
if s == nil {
return
}
sa, ok := s.(samlsp.SessionWithAttributes)
if !ok {
return
}
fmt.Fprintf(w, "Token contents, %+v!", sa.GetAttributes())
w.Header().Add("Location", "http://localhost:8080/")
w.WriteHeader(http.StatusFound)
}
I have also tested :
http.Redirect(w, r, "http://localhost:8080/", http.StatusFound)
Can someone help me please?
Thanks :)
Calling w.Write or writing into it using Fmt.Fprintf requires HTTP status code to be set before, otherwise it sets default StatusOK
Server.go
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
Setting the status code multiple times throws superfluous log.
Therefore, Your code is setting the HTTP status code to 200 (http.StatusOk), so the redirect after that is simply impossible.
Solution:
func login(w http.ResponseWriter, r *http.Request) {
s := samlsp.SessionFromContext(r.Context())
if s == nil {
return
}
sa, ok := s.(samlsp.SessionWithAttributes)
if !ok {
return
}
// this line is removed
// fmt.Fprintf(w, "Token contents, %+v!", sa.GetAttributes())
w.Header().Add("Location", "http://localhost:8080/")
w.WriteHeader(http.StatusFound)
// Or Simply
// http.Redirect(w, r, "http://localhost:8080/", http.StatusFound)
}
Try to send your headers before writing content.
And optionally use a relative Location
w.Header().Add("Location", "/")
w.WriteHeader(http.StatusFound)
fmt.Fprintf(w, "Token contents, %+v!", sa.GetAttributes())
I have the following middleware, first sets currentUser in gorilla/context to current user acquired from database, and the second checks if currentUser exists and redirects otherwise:
package main
import (
"database/sql"
"github.com/gorilla/context"
"log"
"net/http"
"server/helpers"
)
func withCurrentUser(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userId := helpers.GetCurrentUserId(db, r)
if userId == nil {
next.ServeHTTP(w, r)
return
}
row := db.QueryRow("SELECT username FROM User WHERE id=?", userId)
var username string
switch err := row.Scan(&username); err {
case sql.ErrNoRows:
next.ServeHTTP(w, r)
return
case nil:
user := helpers.User{UserId: *userId, LoggedIn: true, Username: username}
context.Set(r, "currentUser", user)
default:
log.Fatal(err)
}
next.ServeHTTP(w, r)
})
}
func loginRequired(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := context.Get(r, "currentUser").(helpers.User)
log.Println(user, ok)
if !ok {
http.Redirect(w, r, "/login", 301)
return
}
next.ServeHTTP(w, r)
})
}
Then when I register a route that requires authenticated user, I do the following:
router.Handle("/create_post",
withCurrentUser(db, loginRequired(http.HandlerFunc(createPostGet))),
).Methods(http.MethodGet)
Where createPostGet:
func createPostGet(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("templates/createPost.html"))
user := context.Get(r, "currentUser").(helpers.User)
_ = tmpl.Execute(w, helpers.FormStruct{CurrentUser: user})
}
My problem is: even if user is authenticated and context is populated correctly, this route always redirects to login. I tried setting breakpoints inside loginRequired and adding log.Println (as you can see in code), and that function doesn't seem to be even called (no breakpoints stop, no log output, too).
What's happening and how to make sure loginRequired is called every time and checks context properly?
UPD: It doesn't seem to persist, I recompiled the app a few times and now it's working. Anyway, what might be the reason for such behavior (I'm positive I saved everything the first time).
UPD 2: I found out that the problem is connected to browser caching, but I still have no idea why it happens. When I disable browser caching, everything works and function is called every time, but while browser cache is enabled, the function isn't called at all. Any ideas?
OMG, that was stupid. I was using 301 redirect code in loginRequired, so redirect was permanent and browser didn't even make requests.
So I was writing a backend using Go, and while I was testing the login/logout function I noticed that if I try to access a page with an expired cookie I'm allowed to do so. This happens only if I use a faulty client, because if I use my browser it works correctly, which I guess means that the cookie is deleted from the web-browser but not from my back-end.
I've tested different solutions, but nothing seems to work, what I've noticed is this: I've taken note of the pointer of the map which maps the cookies and I've noticed that despite setting "authenticated" value to false when the faulty client tries to access the page the map has its value set to true.
this is the middle ware that handles pages in which the user should be logged in and this should block the faulty client from logging in because "authenticated" value from session-handler should be false ( but it is true, despite being set to false before )
func (s *server) loggedInOnly(handlerFunc http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
session, _ := s.store.Get(request, "session-handler")
log.Printf("loggedInOnly: %p %v", &session.Values, session.Values)
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(writer, "Forbidden", http.StatusForbidden)
return
}
handlerFunc(writer, request)
}
}
this is the logout function which should delete the cookie ( but it doesn't delete it internally )
func (s *server) handleLogout() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
session, _ := s.store.Get(request, "session-handler")
session.Values["authenticated"] = false
session.Options.MaxAge = -1
session.Save(request, writer)
log.Printf("logout: %p, %v", &session.Values, session.Values)
fmt.Fprint(writer, "Succesfully logged out")
log.Printf("%v was logged out", writer)
}
}
and this is the login function
func (s *server) handleLogin() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
log.Printf("handle login called")
session, _ := s.store.Get(request, "session-handler")
// can login or not
session.Values["authenticated"] = true
session.Save(request, writer)
Success(writer, "successfully logged in")
}
}
I should expect the cookie to be invalid from my webserver so that when the faulty client tries to reconnect authenticated is set to false and he shouldn't be able to access loggedInOnly pages, instead I he can because authenticated value is set to true ( despite being set to false right before ).
these are the logs:
2019/01/05 17:00:00 loggedInOnly: 0xc0421960b0 map[authenticated:true]
2019/01/05 17:00:00 logout: 0xc0421960b0, map[authenticated:false]
2019/01/05 17:00:01 loggedInOnly: 0xc0420a4560 map[authenticated:true]
2019/01/05 17:00:01 logout: 0xc0420a4560, map[authenticated:false]
as you can see "authenticated" is set before to false then immediatly after it is true despite nothing happening in between
While using Laravel, I really appreciated being able to use:
return Redirect::back();
to return to the previous URL after a POST request.
Is there a simple in-built way to do this in Go?
http.Redirect(w, r, backURL, http.StatusSeeOther)
where backURL is the URL that the POST request was made from.
I've looked through net/http and searched SE and google, but I haven't turned anything up. Failing an easy way, I'd appreciate any pointer toward the idiomatic way of doing this in Go.
The Redirect::back() function uses the URL from the Referer HTTP header specified by the client (browser).
You could access this header value like r.Header.Get("Referer"), but the http.Request type also provides a direct Request.Referer() method that returns the value of this Referer field, which also deals with its 2 possible forms ("Referer" and "Referrer").
This is how you can mimic the "go back" behavior:
func PostHandler(w http.ResponseWriter, r *http.Request) {
// Process form, then:
if rf := r.Referer(); rf != "" {
http.Redirect(w, r, rf, http.StatusSeeOther)
return
}
// No Referer specified, supply your own response
// or redirect to a default / home page
http.Redirect(w, r, "/", http.StatusSeeOther)
}
If you want to call this from many handlers, you may capture this functionality in a helper function:
func redirectBack(w http.ResponseWriter, r *http.Request) {
if rf := r.Referer(); rf != "" {
http.Redirect(w, r, rf, http.StatusSeeOther)
return
}
// No Referer specified, supply your own response
// or redirect to a default / home page
http.Redirect(w, r, "/", http.StatusSeeOther)
}
And then using it:
func PostHandler(w http.ResponseWriter, r *http.Request) {
// Process form, then:
redirectBack(w, r)
}
I setup a middleware and router to handle an AJAX request on path /api/on like this
http.HandleFunc("/",route.IndexHandler)
http.HandleFunc("/login",route.GoogleLoginHandler)
http.HandleFunc("/auth/google",route.GoogleCallbackHandler)
http.Handle("/dashboard",negroni.New(
negroni.HandlerFunc(route.IsAuthenticated),
negroni.Wrap(http.HandlerFunc(route.DashboardHandler)),
))
http.Handle("/api/on",negroni.New(
negroni.HandlerFunc(route.IsAuthenticated),
negroni.Wrap(http.HandlerFunc(route.TurnOn)),
))
Middleware IsAuthenticated allows to jump to the next router if there is a user session in my web, otherwise it will redirect
func IsAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
session, _ := util.GlobalSessions.SessionStart(w, r)
defer session.SessionRelease(w)
if session.Get("profile") == nil {
http.Redirect(w, r, "/", http.StatusMovedPermanently)
} else {
next(w, r)
}
}
And this is TurnOn handler for later process
func TurnOn(w http.ResponseWriter, r *http.Request) {
session, _ := util.GlobalSessions.SessionStart(w, r)
defer session.SessionRelease(w)
w.Header().Set("Access-Control-Allow-Origin","*")
w.Header().Set("Access-Control-Allow-Methods","POST")
w.Header().Set("Access-Control-Allow-Headers","Content-Type")
fmt.Println(r.URL)
}
After I turned on server, I conducted a POST request with JavaScript's client-side code using AJAX, but the console logged nothing. It seems that the TurnOn function was not reached even when AJAX request was sent successfully. Did I miss something obviously?