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:
- Working knowledge of Go
- Go installed on your local machine
- PostgreSQL DB installed on your local machine
We will build an HTTP server with two routes:
- The
/login
route accepts username and password, then authenticates them before granting access - 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.
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.
The post Building an authentication server with Go appeared first on LogRocket Blog.