Chinedu Imoh Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Building an authentication server with Go

5 min read 1415

Building an authentication server from scratch can be tedious; you are literally tasked with creating a digital security system to save, validate and coordinate access to the application’s resources on the server.

However, there is no need to reinvent the wheel! Many resources like this article document the process of creating an authentication server with different languages, as we will be doing with Go.

In this article, we will build a simple authentication server to demonstrate how you can securely store users’ passwords in Go.

Sensitive data like passwords can (but should never) be stored directly in the database; it poses a severe risk to users and developers (if the database gets compromised, the passwords get exposed). We will be using the bcrypt algorithm to hash and salt our passwords for optimum security.

First, let’s take a look at the prerequisites and overview for this tutorial.

Prerequisites and overview

Before beginning this tutorial, you should have the following:

We will build an HTTP server with two routes:

  1. The /login route accepts username and password, then authenticates them before granting access
  2. The /signup route receives the username and password, then hashes the password before storing them in the database

Initializing an HTTP server

Let’s start by initializing the HTTP server with the required routes.

Firstly, create a project folder and initialize Go modules with the following command:

mkdir server
cd server
go mod init server

We will use the gorilla/mux toolkit to handle our route and the pq package for the Postgres driver.

We made a custom demo for .
No really. Click here to check it out.

Enter the following command to download the necessary packages locally:

go get "github.com/gorilla/mux"
go get "github.com/lib/pq"
go get "golang.org/x/crypto/bcrypt"

Next, create a main.go file inside the project folder and add the following code:

package main
import (
        "log"
        "net/http"
        "github.com/gorilla/mux"
        _ "github.com/lib/pq"
        "golang.org/x/crypto/bcrypt"
)
func main() {
    router := mux.NewRouter()
    // "Login" and "Signup" are handler that we will implement
    router.HandleFunc("/login", Login)
    router.HandleFunc("/signup", signUP)
    log.Fatal(http.ListenAndServe(":8080", router))
}

In the code above, we started by importing the necessary packages and then initializing a new router in the main function. Next is the declaration of the login and signup route, and lastly, we called the http.ListenAndServe() and passed the port 8080 and router into it.

Creating a PostgreSQL database

Firstly, open the PostgreSQL shell and click Enter five times to connect to the database (enter a password if needed).

Enter the following command to create the database:

CREATE DATABASE godb;

Then, connect to the database with the following:

\c godb

Lastly, create the table users with the username and password columns:

create table users (
  username text primary key,
  password text
);

Setting up the database

Right under import, declare the package-level variable db, which will reference our database instance and the information we need to collect:

var db *sql.DB
const (
    host     = "localhost"
    port     = 5432
    user     = "postgres"
    password = "gggg"
    dbname   = "mydb"
)
...

Next, we need to create the database initialization function:

func initDB(){
        var err error

        psqInfo := fmt.Sprintf("host=%s port=%d user=%s "+
        "password=%s dbname=%s sslmode=disable",host, port, user, password, dbname)

        db, err = sql.Open("postgres", psqInfo)
        if err != nil {
                panic(err)
        }
}

The variable psqInfo we created and passed to the SQL.Open method holds all the values we need to connect to our database in a string.

Add the function initDB() to the function main under the handler functions:
<

// initialize our database connection
initDB()

Handler function implementation

As mentioned previously, our server has two routes: a “sign up” route and a “log in” route. In this section, we will learn how to implement handlers for these two routes.

Sign up

Before a user can log in, they have to have signed up first, so let’s implement the Signup handler.

The Signup handler accepts a POST request with a JSON body containing the user’s signup credentials:

{
  "username": "johndoe",
  "password": "mysecurepassword"
}

Firstly, we need to create a struct that models the structure of the credentials we want to retrieve from the user, both in the request body and the database under the initDB function:

type Credentials struct {
        Password string `json:"password", db:"password"`
        Username string `json:"username", db:"username"`
}

Next, we need to create the Signup HTTP handler:

func Signup(w http.ResponseWriter, r *http.Request){
  ...

Now, parse and decode the request body into creds (an instance of Credentials). If there is something wrong with the request body, a “Bad Request” status 400 is sent to the browser:

creds := &Credentials{}
err := json.NewDecoder(r.Body).Decode(creds)
if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
}
...

We need to salt and hash the password using the bcrypt algorithm. The second argument is the cost of hashing, which we arbitrarily set as eight (this value can be more or less, depending on the computing power you wish to utilize):

hashedPassword, err := bcrypt.GenerateFromPassword([]byte(creds.Password), 8)

Next, insert the username and the hashed password into the database; if there is an issue with inserting the data into the database, an “Internal Server Error” status 500 is sent to the browser.

We will reach this point if the credentials are correctly stored in the database, and a status 200 is sent back to the browser with the string “Sign-Up Successful:”

if _, err = db.Query("insert into users values ($1, $2)", creds.Username, string(hashedPassword)); 
  if _, err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
 fmt.Fprintln(w, "Sign-Up Successful")
}

Log in

The Login handler receives credentials and authenticates them by comparing them with the entries in the database for that particular user.

Now, similarly to how we initialized the Signup handler, we initialize the Login handler function with the following:

func Login(w http.ResponseWriter, r *http.Request){
  ...

Once again, we parse and decode the request body into creds:

creds := &Credentials{}
err := json.NewDecoder(r.Body).Decode(creds)
if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
}
...

Next, we need to get the existing entry present in the database for the given username. As before, any issue with throw an “Internal Server Error” status 500 to the browser:

result := db.QueryRow("select password from users where username=$1", creds.Username)
if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
}
...

Below, we create another instance of Credentials to store the credentials we retrieved from the database:

storedCreds := &Credentials{}
...

Now, we need to store the obtained password in storedCreds. If an entry with the username does not exist, an “Unauthorized” status 401 is sent to the browser:

err = result.Scan(&storedCreds.Password)
if err != nil {
  if err == sql.ErrNoRows {
          w.WriteHeader(http.StatusUnauthorized)
          return
  }
...

If the error is any other type, an “internal server error” status 500 is sent to the browser:

w.WriteHeader(http.StatusInternalServerError)
return
}
...

Next, we need to compare the stored hashed password with the password retrieved from the user.

If the two passwords don’t match, an “Unauthorized” status 401 is sent to the browser:

if err = bcrypt.CompareHashAndPassword([]byte(storedCreds.Password), []byte(creds.Password)); err != nil {
      w.WriteHeader(http.StatusUnauthorized)
      return
      }
fmt.Fprintln(w, "User Logged In")
}

If the two passwords match, then the user’s retrieved password was correct, and they are authorized. The default status 200 is sent along with the string “User Logged In.”

Testing

For this testing phase, we are going to use the Postman application. Start up the server with the command below:

go run main.go

Next, open Postman, create a new request, set header Content-Type to application/json, and parse login credentials in the request’s body in JSON format as shown below:

{
    "username": "johndoe",
    "password": ""
}

Make a signup POST request to the route below:

http://localhost:8080/signup

You will get a response “Sign-Up Successful” in the body with a status 200. Now, retry it with the login route below:

http://localhost:8080/login

You will get a response “User Logged In” in the body with a status 200.

Conclusion

In this article, we created a PostgreSQL database, set it up, and integrated it with Go to create our authentication server. With the help of the bcrypt algorithm, we were able to hash and salt the password before saving it in the database for optimal security.

The authentication server we built in this post is basic but elegant; further modification can be done to improve adapt the code to the authentication needs of any web application. For example, JWTs can be integrated to seamlessly handle authorization and sessions.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Chinedu Imoh Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Leave a Reply