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:
- A Linux server.
- A non-root
sudo
user. - A MySQL database.
- The Golang package.
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.
Log in to your MySQL database server as
root
.$ sudo mysql -u root -p
Enter your MySQL
root
password and press Enter to proceed. Then, create asample_db
database and asample_db_user
user account. ReplaceEXAMPLE_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;
Switch to the new
sample_db
.mysql> USE sample_db;
Create a
system_users
table. This table holds theuser_ids
,usernames
, andbcrypt
hashedpasswords
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;
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;
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.
Create a
project
directory.$ mkdir project
Switch to the new
project
directory.$ cd project
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.
Use
nano
to open a newmain.go
file.$ nano main.go
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") } }
Save and close the file when you're through with editing.
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.
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 theregistrationsHandler
function. Then, to authenticate to your system and receive a time-based token, users will be hitting the/authentications
endpoint, which runs theauthenticationsHandler
function. You also have a "/test" resource that users will access with their time-based tokens. This resource gets content from thetestResourceHandler
function.... func main() { http.HandleFunc("/registrations", registrationsHandler) http.HandleFunc("/authentications", authenticationsHandler) http.HandleFunc("/test", testResourceHandler) http.ListenAndServe(":8081", nil) } ...
The
registrationsHandler
function retrieves submittedusername
andpassword
for any users you're adding to your system and directs the same to aregisterUser
function in aregistrations.go.go
file which you'll create next.Then, the
authenticationsHandler
extracts log in credentials(username
andpassword
) using the statementreq.BasicAuth()
. Then, it passes these details to agenerateToken
function under anauthentication.go
file, which you'll create later. In case the credentials match a valid account on thesystem_users
table, you're issuing the user with a token.Next, you have the
testResourceHandler
function. Under this function, you're retrieving the time-based token from theAuthorization
header submitted by the client's request. Then, you're passing it to avalidateToken
function under theauthentication.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.
Open a new
registrations.go
file usingnano
.$ nano registrations.go
Then, enter the following information into the
registrations.go
file. ReplaceEXAMPLE_PASSWORD
with the correct password for yoursample_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 }
Save and close the file when you're through with editing.
The above file has a single
registerUser
function that inserts data into yoursample_db
database in thesystem_users
table. You're using the statementhashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 14)
to hash the plain-text passwords for security purposes. The function returns aSuccess
message once you've created a user into the database.You've imported the
database/sql
,github.com/go-sql-driver/mysql
, andgolang.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.
Open a new
authentications.go
file using nano.$ nano authentications.go
Then, enter the following into the
authentications.go
file. ReplaceEXAMPLE_PASSWORD
with the correct password for yoursample_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 }
Save and close the file when done with editing.
In the
generateToken
function, you're accepting ausername
and apassword
. Then, you're running aSELECT
statement against thesystem_users
table to check if a record exists with that username. You're then using the statementif err == sql.ErrNoRows {}
to determine if there is a matching row for the user. If the user doesn't exist, you're throwing anInvalid username or password.
error. However, if there is a matching record, you're using the statementbcrypt.CompareHashAndPassword([]byte(accountPassword), []byte(password))
to determine if the account's password and the supplied password match.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 tobase64
using the statementbase64.URLEncoding.EncodeToString(...)
. then, you're permanently saving the token to theauthentication_tokens
table.In the
validateToken
function, you're checking the provided token on theauthentication_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.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.
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
Next, run the project.
$ go run ./
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.SSH to your server on another terminal window.
Then, execute the
curl
command below to add a samplejohn_doe's
account to your application. ReplaceEXAMPLE_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"
You should now receive the following output.
Success
Next, make a request to the
/authentications
endpoint usingjohn_doe's
credentials to get a time-based token.$ curl -u john_doe:EXAMPLE_PASSWORD http://localhost:8081/authentications
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 theauthentications.go
file.{ "auth_token": "sxGfdDPQvb8ygi7wuAHt90CjMspteY8lDLtvV4AENlw=", "expires_at": "2021-11-27 14:05:39", "generated_at": "2021-11-27 13:05:39", "token_type": "Bearer" }
Copy the value of the
auth_token
. For examplesxGfdDPQvb8ygi7wuAHt90CjMspteY8lDLtvV4AENlw=
. Next, execute thecurl
command below and include your token in anAuthorization
header preceded by the termBearer
. 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
You should receive the following response, which shows you're now authenticated to the system using the time-based token.
Welcome, john_doe
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.
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.
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.
Your token-based authentication logic is now working as expected.
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: