Login/logout in web application (Go)
Mar 9, 2014Goal: simple example for session management using secure cookies.
Serve two pages - an index page providing a login form and an internal page that is only accessible to authenticated users (= users that have used the login form). The internal page provides a possibility to log out.
This has to be implemented using only the Golang standard packages and the Gorilla toolkit.
The following sections describe a possible implementation (~ 120 lines of code). The full code can be found here.
Basic setup
The implementation uses - besides the standard net/http
package - the Gorilla request multiplexer and the secure cookie package for storing session information on the client.
1 package main
2
3 import (
4 "fmt"
5 "github.com/gorilla/mux"
6 "github.com/gorilla/securecookie"
7 "net/http"
8 )
9
10 var cookieHandler = securecookie.New(
11 securecookie.GenerateRandomKey(64),
12 securecookie.GenerateRandomKey(32))
13
14 // ..
15
16 var router = mux.NewRouter()
17
18 func main() {
19
20 router.HandleFunc("/", indexPageHandler)
21 router.HandleFunc("/internal", internalPageHandler)
22
23 router.HandleFunc("/login", loginHandler).Methods("POST")
24 router.HandleFunc("/logout", logoutHandler).Methods("POST")
25
26 http.Handle("/", router)
27 http.ListenAndServe(":8000", nil)
28 }
Four request handlers are registered: for every page and for the log-in/log-out operation.
Log-in and log-out handler are also restricted to POST
methods.
More restrictions could be defined, e.g. the indexPageHandler
and the internalPageHandler
only support GET
requests.
Furthermore, a secure cookie handler is initialized.
The required parameters (hashKey
and blockKey
) are generated randomly.
Static pages
There are two pages required for the example. The following implementation does not use any fancy template stuff - only plain HTML template strings.
1 const indexPage = `
2 <h1>Login</h1>
3 <form method="post" action="/login">
4 <label for="name">User name</label>
5 <input type="text" id="name" name="name">
6 <label for="password">Password</label>
7 <input type="password" id="password" name="password">
8 <button type="submit">Login</button>
9 </form>
10 `
11
12 func indexPageHandler(response http.ResponseWriter, request *http.Request) {
13 fmt.Fprintf(response, indexPage)
14 }
15
16 const internalPage = `
17 <h1>Internal</h1>
18 <hr>
19 <small>User: %s</small>
20 <form method="post" action="/logout">
21 <button type="submit">Logout</button>
22 </form>
23 `
24
25 func internalPageHandler(response http.ResponseWriter, request *http.Request) {
26 userName := getUserName(request)
27 if userName != "" {
28 fmt.Fprintf(response, internalPage, userName)
29 } else {
30 http.Redirect(response, request, "/", 302)
31 }
32 }
The indexPageHandler
just returns the defined template.
The internalPageHandler
first tries to extract the user name from the request.
If a valid user name is available, the internal page is returned.
If no user is authenticated, a redirect to the login page is sent.
Login and logout
Both operations - login and logout - have their own POST
request handler.
1 func loginHandler(response http.ResponseWriter, request *http.Request) {
2 name := request.FormValue("name")
3 pass := request.FormValue("password")
4 redirectTarget := "/"
5 if name != "" && pass != "" {
6 // .. check credentials ..
7 setSession(name, response)
8 redirectTarget = "/internal"
9 }
10 http.Redirect(response, request, redirectTarget, 302)
11 }
12
13 func logoutHandler(response http.ResponseWriter, request *http.Request) {
14 clearSession(response)
15 http.Redirect(response, request, "/", 302)
16 }
The loginHandler
reads the name and the password from the submitted form.
If the credentials pass the sophisticated check, the user name is stored in a session and a redirect to the internal page is sent.
Otherwise, the user is redirected to the login page.
The logoutHandler
clears any existing session and redirects to the login page.
Session handling
The last part of the code snippet deals with session management.
1 func setSession(userName string, response http.ResponseWriter) {
2 value := map[string]string{
3 "name": userName,
4 }
5 if encoded, err := cookieHandler.Encode("session", value); err == nil {
6 cookie := &http.Cookie{
7 Name: "session",
8 Value: encoded,
9 Path: "/",
10 }
11 http.SetCookie(response, cookie)
12 }
13 }
14
15 func getUserName(request *http.Request) (userName string) {
16 if cookie, err := request.Cookie("session"); err == nil {
17 cookieValue := make(map[string]string)
18 if err = cookieHandler.Decode("session", cookie.Value, &cookieValue); err == nil {
19 userName = cookieValue["name"]
20 }
21 }
22 return userName
23 }
24
25 func clearSession(response http.ResponseWriter) {
26 cookie := &http.Cookie{
27 Name: "session",
28 Value: "",
29 Path: "/",
30 MaxAge: -1,
31 }
32 http.SetCookie(response, cookie)
33 }
setSession
puts the provided user name into a simple string map.
Afterwards, the secure cookie handler is used to encode the value map.
The resulting (encrypted) session value is then stored in a standard http.Cookie
instance.
The function getUserName
implements the whole sequence the other way around:
First, the cookie is read from the request.
Then the secure cookie handler is used to decode/decrypt the cookie value.
The result is a string map and the user name is returned.
Finally, clearSession
deletes the current session by setting a cookie with a negative MaxAge
.
Afterwards, the session information is deleted from the client.
The described approach uses a client-side session approach. This has the advantage that session information can be checked by only looking at the request - no database access is required. An important disadvantage is that it might be possible to do replay attacks on your web application and you have no possibility to revoke sessions (except timeouts). There are also hybrid approaches.
The provided snippets should be sufficient to implement the ideas described in Securely Implementing Client-side Sessions.
Tweet Follow @schoebel