Go Basic Sessions
28 May 2019
golang
When a user logs into any applicaiton, their authorizaiton has to be persisted otherwise they will be required to re-authenticate with every request. One of the ways to accomplish this is to store a users “session”.
A session is started once a user logs in and expires either after a specific time delay or on logout. Each logged in user and device has a unique session reference, or id, that is sent with every request the user makes after they are authenticated. Sessions can store informaiton about the current state of the user, but, in our example below we’re only going to be using a session to verify authorization.
Outline
In this post we’ll be looking into how to create, persist and clear user sessions. As a simple example we’ll be using the filesystem as the backend session store, but in reality any type of database/persistent store can be used as a session store.
Our application will consist of three endpoint /login
, /authenticated
, and /logout
.
/login
-> Will create and persist our sessions/authenticated
-> Will only show if a user is authenticated, otherwise it will 403/logout
-> Will clear our sessions
Base HTTP server
Lets begin by defining our base HTTP server that will handle the three routes
import (
"github.com/strattonw/go-basic-sessions/handlers"
"log"
"net/http"
)
func main() {
http.HandleFunc("/login", handlers.Login)
http.HandleFunc("/authenticated", handlers.Authenticated)
http.HandleFunc("/logout", handlers.Logout)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Constants and structs
For this example we’re going to hard code our super secure username and password combination, which, so very obviously, should never be done in the real world. We’re also going to define a struct
that we will use to receive the authentication credentials from the user.
const (
u = "admin"
p = "root"
path = "/tmp/"
cookieName = "strattonDevSession"
)
type Credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
Authentication handler
The first handler we’re going to define is the handler that will be behind authentication. We’re doing this one first to make sure that we get a failed authenticaion when there is no possible way for us to become authenticated.
As we’re using the filesystem as the session store our authentication handlers job is to check to make sure the file related to the session exists. If the file doesn’t exist we can assume the session is invalid or it has expired.
func Authenticated(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
log.Println("Could not find session cookie on request")
w.WriteHeader(http.StatusUnauthorized)
return
}
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if _, err := os.Stat(path + c.Value); os.IsNotExist(err) {
log.Println("Session did not exist")
w.WriteHeader(http.StatusUnauthorized)
} else {
_, _ = w.Write([]byte("Welcome\n"))
}
}
If we test this handle out we’ll see that we properly send back a Http 401 response type.
stratton.dev$ curl -i localhost:8080/authenticated
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:38:01 GMT
Content-Length: 0
stratton.dev$ curl -i --cookie "strattonDevSession=sessionPlease" localhost:8080/authenticated
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:39:56 GMT
Content-Length: 0
Login handler
Next lets make the handler that will allow us to create the sessions when a user logs in. The main parts of our login handler will be to decode the credentials json, verify the credential accuracy and, if the credentials are valid, create the session and add it as a cookie to the response.
func Login(w http.ResponseWriter, r *http.Request) {
var credentials Credentials
err := json.NewDecoder(r.Body).Decode(&credentials)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if credentials.Username != u || credentials.Password != p {
log.Println("Credentials was incorrect")
w.WriteHeader(http.StatusUnauthorized)
return
}
token, err := uuid.NewV4()
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = os.Create(path + token.String())
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: token.String(),
Expires: time.Now().Add(24 * time.Hour),
})
}
First lets make sure we get a Http 401 response when passing in invalid credentials.
stratton.dev$ curl -d '{"username":"NotTheUsername","password":"root"}' -X POST -i localhost:8080/login
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:46:25 GMT
Content-Length: 0
stratton.dev$ curl -d '{"username":"admin","password":"NotThePassword"}' -X POST -i localhost:8080/login
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:45:48 GMT
Content-Length: 0
Since we know we’ll fail with incorrect credentials lets make a request with valid credentials to create our session.
stratton.dev$ curl -d '{"username":"stratton","password":".dev"}' -X POST -i localhost:8080/login
HTTP/1.1 200 OK
Set-Cookie: strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab; Expires=Thu, 30 May 2019 05:48:35 GMT
Date: Wed, 29 May 2019 05:48:35 GMT
Content-Length: 0
Awesome, we now have a session, lets copy that cookie and add it to our authenticated endpoint request. It should be noted that in a normal, non-curl, flow, the web browser would handle adding the cookie to subsequent requests for the user.
stratton.dev$ curl -i --cookie "strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab" localhost:8080/authenticated
HTTP/1.1 200 OK
Date: Wed, 29 May 2019 05:51:25 GMT
Content-Length: 8
Content-Type: text/plain; charset=utf-8
Welcome
We’re now able to have users login and view authenticated pages using their session. The last step we need is to make sure that the users are able to logout and remove their session.
Logout handler
The main goal for our logout handler is to remove the session from our session store, currently the filesystem, so the session cannot be used for authorization in the future. As we have the filesystem as our session store, to remove the session the store all we have to do is delete the session file.
func Logout(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
log.Println("Returning success on logout request without session cookie")
return
}
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
err = os.Remove(path + c.Value)
if err != nil {
if os.IsNotExist(err) {
return
}
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
Lets try this handler out by calling /logout
and then /authenticated
both with the session we generated during login. If all goes well /logout
should return Http 200 and /authenticated
should return Http 401, as the session is no longer valid.
stratton.dev$ curl -i --cookie "strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab" localhost:8080/logout
HTTP/1.1 200 OK
Date: Wed, 29 May 2019 05:54:35 GMT
Content-Length: 0
stratton.dev$ curl -i --cookie "strattonDevSession=587105bc-c9b5-4900-9641-7203f24bafab" localhost:8080/authenticated
HTTP/1.1 401 Unauthorized
Date: Wed, 29 May 2019 05:54:38 GMT
Content-Length: 0
Awesome, we can now properly logout and remove our sessions from the session store.
Source
Conclusion
Here we’ve shown a very simple way to implement session based authentication in Go. There are lots of places that we can take this basis, such as, changing the session store to redis or sql and adding the ability to expire sessions within the session store (currently sessions will live forever, or until we restart as we use /tmp
for the filepath by default).