Implement Token-based Authentication with Golang and MySQL 8 Server

Updated on December 8, 2021
Implement Token-based Authentication with Golang and MySQL 8 Server header image

Introduction

Token-based authentication is a form of access control protocol. To implement it, you require users to verify their identity (For instance, by providing their usernames and passwords). Then, you issue the users with a time-based secret that they can use to access your application.

With most companies relying on Application Programming Interfaces (APIs), token-based authentication is the most convenient and secure way to handle authentication for multiple users. The majority of web companies that use token-based authentication include Twitter, Google, Github, Facebook, and more.

In this guide, you'll implement token-based authentication with Golang and MySQL 8 on your Linux server.

Prerequisites

To follow along with this tutorial, you will require:

1. Create a Database and User Account

Your application requires a form of a database to store users' account details. SSH to your server and follow the steps below to create a database and a user account.

  1. Log in to your MySQL database server as root.

     $ sudo mysql -u root -p
  2. Enter your MySQL root password and press Enter to proceed. Then, create a sample_db database and a sample_db_user user account. Replace EXAMPLE_PASSWORD with a strong value.

     mysql> CREATE DATABASE sample_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
            CREATE USER 'sample_db_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
            GRANT ALL PRIVILEGES ON sample_db.* TO 'sample_db_user'@'localhost';
            FLUSH PRIVILEGES;
  3. Switch to the new sample_db.

     mysql> USE sample_db;
  4. Create a system_users table. This table holds the user_ids, usernames, and bcrypt hashed passwords for any users accessing your application.

     mysql> CREATE TABLE system_users (
                user_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
                username VARCHAR(50),
                password VARCHAR(255)
            ) ENGINE = InnoDB;
  5. Next, create an authentication_tokens table. When users confirm their identity by providing their correct usernames and passwords, you'll store their time-based tokens(auth_token) in this table for any subsequent authentications.

     mysql> CREATE TABLE authentication_tokens (
                token_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
                user_id BIGINT,
                auth_token VARCHAR(255),
                generated_at DATETIME,
                expires_at   DATETIME
            ) ENGINE = InnoDB;
  6. Log out from the MySQL server.

     mysql> QUIT;

2. Create a Project Directory

When working on any Golang project, creating a dedicated directory to separate your application from the rest of your Linux files is advisable. This makes troubleshooting your application easier in the future.

  1. Create a project directory.

     $ mkdir project
  2. Switch to the new project directory.

     $ cd project
  3. You will now add all Golang source-code files for this tutorial under this directory.

3. Create a main.go File

You'll create a main.go file in this step. This file contains the main function that runs when you start your application.

  1. Use nano to open a new main.go file.

     $ nano main.go
  2. Then, enter the following information into the file.

     package main
    
     import (
         "net/http"
         "fmt"
         "strings"
         "encoding/json"
     )
    
     func main() {
         http.HandleFunc("/registrations",   registrationsHandler)
         http.HandleFunc("/authentications", authenticationsHandler)
         http.HandleFunc("/test", testResourceHandler)
         http.ListenAndServe(":8081", nil)
     }
    
     func registrationsHandler(w http.ResponseWriter, req *http.Request) {
    
         req.ParseForm()
    
         if req.FormValue("username") == "" || req.FormValue("password") == "" {
    
                 fmt.Fprintf(w, "Please enter a valid username and password.\r\n")
    
         } else {    
    
             response, err := registerUser(req.FormValue("username"), req.FormValue("password"))
    
             if err != nil {
                 fmt.Fprintf(w, err.Error()) 
             } else {
                 fmt.Fprintf(w, response)
             }
         }
    
     }
    
     func authenticationsHandler(w http.ResponseWriter, req *http.Request) {
    
         username, password, ok := req.BasicAuth()
    
         if ok {
    
             tokenDetails, err := generateToken(username, password)
    
             if err != nil {
                 fmt.Fprintf(w, err.Error()) 
             } else { 
    
                 enc := json.NewEncoder(w)
                 enc.SetIndent("", "  ")
             enc.Encode(tokenDetails)
             }
         } else {
    
                 fmt.Fprintf(w, "You require a username/password to get a token.\r\n") 
         }
    
     }
    
     func testResourceHandler(w http.ResponseWriter, req *http.Request) {
    
         authToken := strings.Split(req.Header.Get("Authorization"), "Bearer ")[1]
    
         userDetails, err := validateToken(authToken)       
    
         if err != nil {
    
            fmt.Fprintf(w, err.Error()) 
    
         } else {
    
             username := fmt.Sprint(userDetails["username"])
    
             fmt.Fprintf(w, "Welcome, " + username + "\r\n")
         }
    
     }
  3. Save and close the file when you're through with editing.

  4. In this file, you've imported the following packages:

    • net/http: Provides HTTP implementations.
    • fmt: This package allows you to work with basic string formats as well as printing output.
    • strings: This is a package for manipulating strings.
    • encoding/json: This package allows you to encode and decode JSON data. Very useful when working on API-based projects.
  5. Then, you have the main function, which is executed first when the program starts. You're implementing a handler function for the multiple URL paths that provide functionalities in your app. In this application, you only have three routes. The /registrations route handles users' registrations through the registrationsHandler function. Then, to authenticate to your system and receive a time-based token, users will be hitting the /authentications endpoint, which runs the authenticationsHandler function. You also have a "/test" resource that users will access with their time-based tokens. This resource gets content from the testResourceHandler function.

     ...
     func main() {
         http.HandleFunc("/registrations",   registrationsHandler)
         http.HandleFunc("/authentications", authenticationsHandler)
         http.HandleFunc("/test", testResourceHandler)
         http.ListenAndServe(":8081", nil)
     }
     ...
  6. The registrationsHandler function retrieves submitted username and password for any users you're adding to your system and directs the same to a registerUser function in a registrations.go.go file which you'll create next.

  7. Then, the authenticationsHandler extracts log in credentials(username and password) using the statement req.BasicAuth(). Then, it passes these details to a generateToken function under an authentication.go file, which you'll create later. In case the credentials match a valid account on the system_users table, you're issuing the user with a token.

  8. Next, you have the testResourceHandler function. Under this function, you're retrieving the time-based token from the Authorization header submitted by the client's request. Then, you're passing it to a validateToken function under the authentication.go file to check if the token is valid. You're then greeting any authenticated user with a welcome message.

4. Create a registrations.go File

To register users into your application, you'll add entries to the system_users table that you've already created in your database.

  1. Open a new registrations.go file using nano.

     $ nano registrations.go
  2. Then, enter the following information into the registrations.go file. Replace EXAMPLE_PASSWORD with the correct password for your sample_db database.

     package main
    
     import ( 
         "database/sql"
         _ "github.com/go-sql-driver/mysql"
         "golang.org/x/crypto/bcrypt" 
     )
    
     func registerUser(username string, password string) (string, error) {
    
         db, err := sql.Open("mysql", "sample_db_user:EXAMPLE_PASSWORD@tcp(127.0.0.1:3306)/sample_db")                                                       
         if err != nil {
             return "", err
         }            
    
         queryString := "insert into system_users(username, password) values (?, ?)"
    
         stmt, err := db.Prepare(queryString) 
    
         if err != nil {
             return "", err
         }
    
         defer stmt.Close()  
    
         hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 14)  
    
         _, err = stmt.Exec(username, hashedPassword)  
    
         if err != nil {
             return "", err
         }
    
         return "Success\r\n", nil
    
     }
  3. Save and close the file when you're through with editing.

  4. The above file has a single registerUser function that inserts data into your sample_db database in the system_users table. You're using the statement hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 14) to hash the plain-text passwords for security purposes. The function returns a Success message once you've created a user into the database.

  5. You've imported the database/sql, github.com/go-sql-driver/mysql, and golang.org/x/crypto/bcrypt packages to implement MySQL database and password hashing functions.

5. Create an authentication.go File

You'll create a single file with these functions to generate and validate time-based tokens.

  1. Open a new authentications.go file using nano.

     $ nano authentications.go
  2. Then, enter the following into the authentications.go file. Replace EXAMPLE_PASSWORD with the correct password for your sample_db database.

     package main
    
     import (    
         "database/sql"
         _ "github.com/go-sql-driver/mysql"
         "golang.org/x/crypto/bcrypt" 
         "time"
         "crypto/rand"
         "encoding/base64"
         "errors"
     )
    
     func generateToken(username string, password string) (map[string]interface{}, error) {
    
         db, err := sql.Open("mysql", "sample_db_user:EXAMPLE_PASSWORD@tcp(127.0.0.1:3306)/sample_db")                                                       
         if err != nil {
             return nil, err
         }            
    
         queryString := "select user_id, password from system_users where username = ?"
    
         stmt, err := db.Prepare(queryString) 
    
         if err != nil {
             return nil, err
         }
    
         defer stmt.Close()
    
         userId := 0
         accountPassword := ""              
    
         err = stmt.QueryRow(username).Scan(&userId, &accountPassword)
    
         if err != nil {
    
             if err == sql.ErrNoRows {
                 return nil, errors.New("Invalid username or password.\r\n")
             }
    
            return nil, err
         }
    
         err = bcrypt.CompareHashAndPassword([]byte(accountPassword), []byte(password))
    
         if err != nil {
             return nil, errors.New("Invalid username or password.\r\n")
         }
    
    
         queryString = "insert into authentication_tokens(user_id, auth_token, generated_at, expires_at) values (?, ?, ?, ?)"
         stmt, err = db.Prepare(queryString) 
    
         if err != nil {
             return nil, err
         }
    
         defer stmt.Close() 
    
         randomToken := make([]byte, 32)
    
         _, err = rand.Read(randomToken)
    
     if err != nil {
             return nil, err        
     }  
    
         authToken   := base64.URLEncoding.EncodeToString(randomToken)
    
         const timeLayout = "2006-01-02 15:04:05"
    
         dt := time.Now()
         expirtyTime := time.Now().Add(time.Minute * 60)
    
         generatedAt := dt.Format(timeLayout)
         expiresAt   := expirtyTime.Format(timeLayout)
    
         _, err = stmt.Exec(userId, authToken, generatedAt, expiresAt)  
    
         if err != nil {
             return nil, err
         }
    
         tokenDetails := map[string]interface{}{
             "token_type":   "Bearer",
             "auth_token" :  authToken,
             "generated_at": generatedAt,
             "expires_at":   expiresAt,
         }
    
         return tokenDetails, nil
     }
    
    
     func validateToken(authToken string) (map[string]interface{}, error) {
    
         db, err := sql.Open("mysql", "sample_db_user:EXAMPLE_PASSWORD@tcp(127.0.0.1:3306)/sample_db") 
    
         if err != nil {
             return nil, err
         }      
    
         queryString := `select 
                     system_users.user_id,
                     username,
                     generated_at,
                     expires_at                         
                 from authentication_tokens
                 left join system_users
                 on authentication_tokens.user_id = system_users.user_id
                 where auth_token = ?`
    
         stmt, err := db.Prepare(queryString) 
    
         if err != nil {
             return nil, err
         }
    
         defer stmt.Close()
    
         userId      := 0
         username    := ""
         generatedAt := ""
         expiresAt   := ""      
    
         err = stmt.QueryRow(authToken).Scan(&userId, &username, &generatedAt, &expiresAt)
    
         if err != nil {
    
             if err == sql.ErrNoRows {
                 return nil, errors.New("Invalid access token.\r\n")
             }
    
            return nil, err
         }
    
         const timeLayout = "2006-01-02 15:04:05"
    
         expiryTime, _  := time.Parse(timeLayout, expiresAt)
         currentTime, _ := time.Parse(timeLayout, time.Now().Format(timeLayout))
    
         if expiryTime.Before(currentTime) {
             return nil, errors.New("The token is expired.\r\n")
         }
    
         userDetails := map[string]interface{}{
             "user_id":     userId,
             "username":    username,
             "generated_at": generatedAt,
             "expires_at" : expiresAt,            
         } 
    
         return userDetails, nil
    
     }
  3. Save and close the file when done with editing.

  4. In the generateToken function, you're accepting a username and a password. Then, you're running a SELECT statement against the system_users table to check if a record exists with that username. You're then using the statement if err == sql.ErrNoRows {} to determine if there is a matching row for the user. If the user doesn't exist, you're throwing an Invalid username or password. error. However, if there is a matching record, you're using the statement bcrypt.CompareHashAndPassword([]byte(accountPassword), []byte(password)) to determine if the account's password and the supplied password match.

  5. Next, you're using randomToken := make([]byte, 32) and _, err = rand.Read(randomToken) statements to generate a random token for the user. You're later encoding the token to base64 using the statement base64.URLEncoding.EncodeToString(...). then, you're permanently saving the token to the authentication_tokens table.

  6. In the validateToken function, you're checking the provided token on the authentication_tokens table to see if there is a match. If the token is valid, you're returning detailed information about the token, including the matching user's details and token values. Otherwise, you're throwing an error to the calling function.

  7. You're using the statement if expiryTime.Before(currentTime) {...} to check if the token has expired.

6. Test the Golang Token-based Authentication Logic

You'll now run and test Golang's token-based authentication logic in your application.

  1. Download all the Golang packages you're using to import them into your application.

     $ go get github.com/go-sql-driver/mysql
     $ go get golang.org/x/crypto/bcrypt
  2. Next, run the project.

     $ go run ./
  3. The above command has a blocking function that makes your web application listen on port 8081. Therefore, don't enter any other command on your terminal window.

  4. SSH to your server on another terminal window.

  5. Then, execute the curl command below to add a sample john_doe's account to your application. Replace EXAMPLE_PASSWORD with a strong value.

     $ curl -X POST http://localhost:8081/registrations -H "Content-Type: application/x-www-form-urlencoded" -d "username=john_doe&password=EXAMPLE_PASSWORD"
  6. You should now receive the following output.

     Success
  7. Next, make a request to the /authentications endpoint using john_doe's credentials to get a time-based token.

     $ curl -u john_doe:EXAMPLE_PASSWORD http://localhost:8081/authentications
  8. You should now get a JSON-based response showing the token details as shown below. The token is valid for sixty minutes(1 hour) since you defined this using the statement expirtyTime := time.Now().Add(time.Minute * 60) in the authentications.go file.

     {
       "auth_token": "sxGfdDPQvb8ygi7wuAHt90CjMspteY8lDLtvV4AENlw=",
       "expires_at": "2021-11-27 14:05:39",
       "generated_at": "2021-11-27 13:05:39",
       "token_type": "Bearer"
     }
  9. Copy the value of the auth_token. For example sxGfdDPQvb8ygi7wuAHt90CjMspteY8lDLtvV4AENlw=. Next, execute the curl command below and include your token in an Authorization header preceded by the term Bearer. In the following command, you're querying the /test resource/endpoint. In a production environment, you can query any resource that allows authentication using the time-based token.

       $ curl -H "Authorization: Bearer sxGfdDPQvb8ygi7wuAHt90CjMspteY8lDLtvV4AENlw=" http://localhost:8081/test
  10. You should receive the following response, which shows you're now authenticated to the system using the time-based token.

    Welcome, john_doe
  11. Attempt authenticating to the application using an invalid token. For instance, fakerandomtoken.

      $ curl -H "Authorization: Bearer fakerandomtoken" http://localhost:8081/test

    Your application should not allow you in, and you'll get the error below.

    Invalid access token.
  12. Next, attempt requesting a token without a valid user account.

    $ curl -u john_doe:WRONG_PASSWORD http://localhost:8081/authentications

    Output.

    Invalid username or password.
  13. Also, if you attempt authenticating to the system after sixty minutes, your token should be expired, and you should receive the following error.

    The token is expired.
  14. Your token-based authentication logic is now working as expected.

  15. Please note: When using Golang token-based authentication in a production environment, you should always use SSL/TLS certificates to prevent attacks during token requests, and responses flow.

Conclusion

In this guide, you've implemented token-based authentication with Golang and MySQL 8 on your Linux server.

Learn more Golang tutorials by visiting the following resources: