├── .gitignore ├── go.mod ├── go.sum ├── test.http ├── main.go ├── README.md └── handlers.go /.gitignore: -------------------------------------------------------------------------------- 1 | go-session-auth-example -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sohamkamani/go-session-auth-example 2 | 3 | go 1.16 4 | 5 | require github.com/google/uuid v1.3.0 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 2 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | -------------------------------------------------------------------------------- /test.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8080/signin 2 | 3 | {"username":"user2","password":"password2"} 4 | 5 | #### 6 | 7 | GET http://localhost:8080/welcome 8 | 9 | ### 10 | 11 | POST http://localhost:8080/refresh 12 | 13 | ### 14 | 15 | GET http://localhost:8080/logout -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | // "Signin" and "Signup" are handlers that we have to implement 10 | http.HandleFunc("/signin", Signin) 11 | http.HandleFunc("/welcome", Welcome) 12 | http.HandleFunc("/refresh", Refresh) 13 | http.HandleFunc("/logout", Logout) 14 | // start the server on port 8080 15 | log.Fatal(http.ListenAndServe(":8080", nil)) 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Session Cookie Authentication in Go 2 | 3 | Example repo for my post on [session cookie authentication in Go](https://www.sohamkamani.com/golang/session-cookie-authentication/) 4 | 5 | ## Running our application 6 | 7 | To run this application, build and run the Go binary: 8 | 9 | ```sh 10 | go build 11 | ./go-session-auth-example 12 | ``` 13 | 14 | Now, using any HTTP client with support for cookies (like [Postman](https://www.getpostman.com/apps), or your web browser) make a sign-in request with the appropriate credentials: 15 | 16 | ``` 17 | POST http://localhost:8080/signin 18 | 19 | {"username":"user2","password":"password2"} 20 | ``` 21 | 22 | You can now try hitting the welcome route from the same client to get the welcome message: 23 | 24 | ``` 25 | GET http://localhost:8080/welcome 26 | ``` 27 | 28 | Hit the refresh route, and then inspect the clients cookies to see the new value of the `session_token`: 29 | 30 | ``` 31 | POST http://localhost:8080/refresh 32 | ``` 33 | 34 | 35 | Finally, call the logout route to clear session data: 36 | 37 | ``` 38 | GET http://localhost:8080/logout 39 | ``` 40 | 41 | Calling the welcome and refresh routes after this will result in a `401` error. -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // we create a map to store username and password pairs. In a real-world application, this would be stored in a database 13 | var users = map[string]string{ 14 | "user1": "password1", 15 | "user2": "password2", 16 | } 17 | 18 | // this map stores the users sessions. For larger scale applications, you can use a database or cache for this purpose 19 | var sessions = map[string]session{} 20 | 21 | // each session contains the username of the user and the time at which it expires 22 | type session struct { 23 | username string 24 | expiry time.Time 25 | } 26 | 27 | // we'll use this method later to determine if the session has expired 28 | func (s session) isExpired() bool { 29 | return s.expiry.Before(time.Now()) 30 | } 31 | 32 | // Create a struct that models the structure of a user in the request body 33 | type Credentials struct { 34 | Password string `json:"password"` 35 | Username string `json:"username"` 36 | } 37 | 38 | func Signin(w http.ResponseWriter, r *http.Request) { 39 | var creds Credentials 40 | // Get the JSON body and decode into credentials 41 | err := json.NewDecoder(r.Body).Decode(&creds) 42 | if err != nil { 43 | // If the structure of the body is wrong, return an HTTP error 44 | w.WriteHeader(http.StatusBadRequest) 45 | return 46 | } 47 | 48 | // Get the expected password from our in memory map 49 | expectedPassword, ok := users[creds.Username] 50 | 51 | // If a password exists for the given user 52 | // AND, if it is the same as the password we received, the we can move ahead 53 | // if NOT, then we return an "Unauthorized" status 54 | if !ok || expectedPassword != creds.Password { 55 | w.WriteHeader(http.StatusUnauthorized) 56 | return 57 | } 58 | 59 | // Create a new random session token 60 | sessionToken := uuid.NewString() 61 | expiresAt := time.Now().Add(120 * time.Second) 62 | 63 | // Set the token in the session map, along with the user whom it represents 64 | sessions[sessionToken] = session{ 65 | username: creds.Username, 66 | expiry: expiresAt, 67 | } 68 | 69 | // Finally, we set the client cookie for "session_token" as the session token we just generated 70 | // we also set an expiry time of 120 seconds 71 | http.SetCookie(w, &http.Cookie{ 72 | Name: "session_token", 73 | Value: sessionToken, 74 | Expires: expiresAt, 75 | }) 76 | } 77 | 78 | func Welcome(w http.ResponseWriter, r *http.Request) { 79 | // We can obtain the session token from the requests cookies, which come with every request 80 | c, err := r.Cookie("session_token") 81 | if err != nil { 82 | if err == http.ErrNoCookie { 83 | // If the cookie is not set, return an unauthorized status 84 | w.WriteHeader(http.StatusUnauthorized) 85 | return 86 | } 87 | // For any other type of error, return a bad request status 88 | w.WriteHeader(http.StatusBadRequest) 89 | return 90 | } 91 | sessionToken := c.Value 92 | 93 | // We then get the name of the user from our session map, where we set the session token 94 | userSession, exists := sessions[sessionToken] 95 | if !exists { 96 | // If the session token is not present in session map, return an unauthorized error 97 | w.WriteHeader(http.StatusUnauthorized) 98 | return 99 | } 100 | if userSession.isExpired() { 101 | delete(sessions, sessionToken) 102 | w.WriteHeader(http.StatusUnauthorized) 103 | return 104 | } 105 | // Finally, return the welcome message to the user 106 | w.Write([]byte(fmt.Sprintf("Welcome %s!", userSession.username))) 107 | } 108 | 109 | func Refresh(w http.ResponseWriter, r *http.Request) { 110 | // (BEGIN) The code from this point is the same as the first part of the `Welcome` route 111 | c, err := r.Cookie("session_token") 112 | if err != nil { 113 | if err == http.ErrNoCookie { 114 | w.WriteHeader(http.StatusUnauthorized) 115 | return 116 | } 117 | w.WriteHeader(http.StatusBadRequest) 118 | return 119 | } 120 | sessionToken := c.Value 121 | 122 | userSession, exists := sessions[sessionToken] 123 | if !exists { 124 | w.WriteHeader(http.StatusUnauthorized) 125 | return 126 | } 127 | if userSession.isExpired() { 128 | delete(sessions, sessionToken) 129 | w.WriteHeader(http.StatusUnauthorized) 130 | return 131 | } 132 | // (END) The code until this point is the same as the first part of the `Welcome` route 133 | 134 | // If the previous session is valid, create a new session token for the current user 135 | newSessionToken := uuid.NewString() 136 | expiresAt := time.Now().Add(120 * time.Second) 137 | 138 | // Set the token in the session map, along with the user whom it represents 139 | sessions[newSessionToken] = session{ 140 | username: userSession.username, 141 | expiry: expiresAt, 142 | } 143 | 144 | // Delete the older session token 145 | delete(sessions, sessionToken) 146 | 147 | // Set the new token as the users `session_token` cookie 148 | http.SetCookie(w, &http.Cookie{ 149 | Name: "session_token", 150 | Value: newSessionToken, 151 | Expires: time.Now().Add(120 * time.Second), 152 | }) 153 | } 154 | 155 | func Logout(w http.ResponseWriter, r *http.Request) { 156 | c, err := r.Cookie("session_token") 157 | if err != nil { 158 | if err == http.ErrNoCookie { 159 | // If the cookie is not set, return an unauthorized status 160 | w.WriteHeader(http.StatusUnauthorized) 161 | return 162 | } 163 | // For any other type of error, return a bad request status 164 | w.WriteHeader(http.StatusBadRequest) 165 | return 166 | } 167 | sessionToken := c.Value 168 | 169 | // remove the users session from the session map 170 | delete(sessions, sessionToken) 171 | 172 | // We need to let the client know that the cookie is expired 173 | // In the response, we set the session token to an empty 174 | // value and set its expiry as the current time 175 | http.SetCookie(w, &http.Cookie{ 176 | Name: "session_token", 177 | Value: "", 178 | Expires: time.Now(), 179 | }) 180 | } 181 | --------------------------------------------------------------------------------