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
sudouser. - 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 -pEnter your MySQL
rootpassword and press Enter to proceed. Then, create asample_dbdatabase and asample_db_useruser account. ReplaceEXAMPLE_PASSWORDwith 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_userstable. This table holds theuser_ids,usernames, andbcrypthashedpasswordsfor 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_tokenstable. 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
projectdirectory.$ mkdir projectSwitch to the new
projectdirectory.$ cd projectYou 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
nanoto open a newmain.gofile.$ nano main.goThen, 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
mainfunction, 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/registrationsroute handles users' registrations through theregistrationsHandlerfunction. Then, to authenticate to your system and receive a time-based token, users will be hitting the/authenticationsendpoint, which runs theauthenticationsHandlerfunction. You also have a "/test" resource that users will access with their time-based tokens. This resource gets content from thetestResourceHandlerfunction.... func main() { http.HandleFunc("/registrations", registrationsHandler) http.HandleFunc("/authentications", authenticationsHandler) http.HandleFunc("/test", testResourceHandler) http.ListenAndServe(":8081", nil) } ...The
registrationsHandlerfunction retrieves submittedusernameandpasswordfor any users you're adding to your system and directs the same to aregisterUserfunction in aregistrations.go.gofile which you'll create next.Then, the
authenticationsHandlerextracts log in credentials(usernameandpassword) using the statementreq.BasicAuth(). Then, it passes these details to agenerateTokenfunction under anauthentication.gofile, which you'll create later. In case the credentials match a valid account on thesystem_userstable, you're issuing the user with a token.Next, you have the
testResourceHandlerfunction. Under this function, you're retrieving the time-based token from theAuthorizationheader submitted by the client's request. Then, you're passing it to avalidateTokenfunction under theauthentication.gofile 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.gofile usingnano.$ nano registrations.goThen, enter the following information into the
registrations.gofile. ReplaceEXAMPLE_PASSWORDwith the correct password for yoursample_dbdatabase.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
registerUserfunction that inserts data into yoursample_dbdatabase in thesystem_userstable. You're using the statementhashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 14)to hash the plain-text passwords for security purposes. The function returns aSuccessmessage 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/bcryptpackages 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.gofile using nano.$ nano authentications.goThen, enter the following into the
authentications.gofile. ReplaceEXAMPLE_PASSWORDwith the correct password for yoursample_dbdatabase.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
generateTokenfunction, you're accepting ausernameand apassword. Then, you're running aSELECTstatement against thesystem_userstable 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 tobase64using the statementbase64.URLEncoding.EncodeToString(...). then, you're permanently saving the token to theauthentication_tokenstable.In the
validateTokenfunction, you're checking the provided token on theauthentication_tokenstable 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/bcryptNext, 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
curlcommand below to add a samplejohn_doe'saccount to your application. ReplaceEXAMPLE_PASSWORDwith 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.
SuccessNext, make a request to the
/authenticationsendpoint usingjohn_doe'scredentials to get a time-based token.$ curl -u john_doe:EXAMPLE_PASSWORD http://localhost:8081/authenticationsYou 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.gofile.{ "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 thecurlcommand below and include your token in anAuthorizationheader preceded by the termBearer. In the following command, you're querying the/testresource/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/testYou should receive the following response, which shows you're now authenticated to the system using the time-based token.
Welcome, john_doeAttempt authenticating to the application using an invalid token. For instance,
fakerandomtoken.$ curl -H "Authorization: Bearer fakerandomtoken" http://localhost:8081/testYour 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/authenticationsOutput.
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: