A simple authentication example

Let's build a secure API that gives access to clients only after logging in. In the process, we will define three endpoints:

/healthcheck is the data API, but it first has to log in using the /login endpoint. Our API should reject all unauthenticated requests. Create a project directory called simpleAuth, like this:

mkdir -p $GOPATH/src/github.com/git-user/chapter14/simpleAuth
touch $GOPATH/src/github.com/git-user/chapter14/simpleAuth/main.go

In the program, we can see how to enable session-based authentication to API endpoints using the gorilla/ sessions package. Follow these steps:

  1. We need imports for our program. The main ones are mux and sessions, as seen in the following code block:
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
)
  1. Now, create a cookie store to store the written cookie information. We can do that using the sessions.NewCookieStore method. It takes a byte array of the secret key. The secret key is fetched from the SESSION_SECRET environment variable, like this:
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))

You can set any key you wish to that environment variable.

  1. Let us create mock usernames and a password as we don't have a signup mechanism, as follows:
var users = map[string]string{"naren": "passme", "admin": "password"}

These mock username/password combinations are checked against a client request.

  1. Now, add a login handler that sets the cookie. The API is a POST request with credentials supplied in the POST body, as follows:
func LoginHandler(w http.ResponseWriter, r *http.Request) {
...
}
  1. In this function, we should first parse the POST body, and get the username and password, like this:
err := r.ParseForm()

if err != nil {
http.Error(w, "Please pass the data as URL form encoded",
http.StatusBadRequest)
return
}
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
  1. Once we have collected the username and password, our plan is to validate them with the mock data. If the credentials are matching, then set the request session to authenticated. Otherwise, show the error message accordingly, as follows:
if originalPassword, ok := users[username]; ok {
session, _ := store.Get(r, "session.id")
if password == originalPassword {
session.Values["authenticated"] = true
session.Save(r, w)
} else {
http.Error(w, "Invalid Credentials", http.StatusUnauthorized)
return
}
} else {
http.Error(w, "User is not found", http.StatusNotFound)
return
}
w.Write([]byte("Logged In successfully"))

This completes the login handler.

  1. In a similar way, let us define the logout handler. The logout handler takes an incoming GET request and sets the session variable authenticated to false, like this:
// LogoutHandler removes the session
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session.id")
session.Values["authenticated"] = false
session.Save(r, w)
w.Write([]byte(""))
}
  1. If you see the logout handler implementation, then we have modified the session object to invalidate the client session, as follows:
session, _ := store.Get(r, "session.id")
session.Values["authenticated"] = false
session.Save(r, w)
    1. In this way, simple authentication can be implemented using client sessions in any programming language, including Go.

    Don't forget to save the cookie after modifying it. The code for this is session.Save(r, w).

    1. Now, let us define our /healthcheck. data API. It is an API that sends the system time back. It returns a response if the client session is authenticated. Otherwise, it returns a 403 Forbidden response. The session object from the request can be used for the validity check, as follows:
    // HealthcheckHandler returns the date and time
    func HealthcheckHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    if (session.Values["authenticated"] != nil) && session.Values
    ["authenticated"] != false {
    w.Write([]byte(time.Now().String()))
    } else {
    http.Error(w, "Forbidden", http.StatusForbidden)
    }
    }
    1. All the API handlers are ready. We have to write the main function, where we map API endpoints (routes) to the preceding handlers. We use the mux router for that purpose. Then, we pass this router to an HTTP server that runs on http://localhost:8000, as follows:
    func main() {
    r := mux.NewRouter()
    r.HandleFunc("/login", LoginHandler)
    r.HandleFunc("/healthcheck", HealthcheckHandler)
    r.HandleFunc("/logout", LogoutHandler)
    http.Handle("/", r)

    srv := &http.Server{
    Handler: r,
    Addr: "127.0.0.1:8000",
    // Good practice: enforce timeouts for servers you create!
    WriteTimeout: 15 * time.Second,
    ReadTimeout: 15 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
    }
    1. This finishes the secured application. We can run the program from the  simpleAuth root directory by typing the following code:
    go run main.go

    This starts the server on http://localhost:8000.

    The error codes can mean different things. For example, Forbidden (403) is issued when the user tries to access a resource without authentication, whereas Resource Not Found (404) is issued when the given resource does not exist on the server.

    In the next section, we'll introduce a new tool to query an API, called Postman. The Postman tool has a nice User Interface (UI) and runs on all platforms. We are going to test the simpleAuth example with this new tool.