Getting started with Golang web application (and authentication) Part 1
How to implement an authentication system on a web application using Go
The purpose of this article is to create an authentication system using Golang. For weeks, I searched for tutorials that performed this kind of task but the results were the same: implementing Golang backend as JSON or Rest API. Therefore, I built mine and wrote a blog about it. This is part 1 of the article. I hope it is insightful and maybe you can learn a thing or two from it. The frontend will be in HTML and styled with Bootstrap, using a MySQL database and Go as the backend:
- Setting up the project
- Setting up the frontend
- Setting up the backend
The finished project can be found here: Github
PermalinkSetting up the Project
We start by creating a new folder calledgolangwebauth
and cd into it.
Create a templates
folder, an errorpages
folder inside the templates directory, initialize our go mod
and create a main.go
file.
PermalinkSetting up the frontend - HTML and Bootstrap
We set up our frontend by creating some HTML pages in the templates directory. First, ourindex.html
file serves as a landing page, dashboard.html
serves as the logged-in page after the user logs in, login.html
as the login page and register.html
serving as the register page. Inside the errorpages directory: 401.html
page for error 401 - http.StatusUnauthorized and 400.html
for the error 400 - http.StatusBadRequest.
Insert the code for the files:
index.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<title>Web App Authentication in Go</title>
</head>
<body>
<div class="form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-5">
<h1>Simple authentication in Go</h1>
</div>
<div class="form-group col-md-3">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<a class="btn btn-primary" href="/register">Register</a>
<a class="btn btn-outline-secondary" href="/login">Login</a>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous">
</script>
</body>
</html>
login.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Go Auth</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
</head>
<body>
<form method="POST">
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<h1>Login - Go Auth</h1>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<label for="email">Email</label>
<input type="email" class="form-control" id="email" name="email" placeholder="Email">
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="Password">
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-primary">Sign In</button>
<a href="/register" type="submit" class="btn btn-outline-primary">Sign Up</a>
<a href="/" type="submit" class="btn btn-outline-secondary">Home</a>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
</form>
</body>
</html>
register.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Go Auth</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css"
integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
</head>
<body>
<form id="registerForm" method="POST">
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<h1>Register - Go Auth</h1>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<label>First Name</label>
<input type="text" class="form-control" id="FirstName" name="FirstName" placeholder="First Name" required>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<label>Last Name</label>
<input type="text" class="form-control" id="LastName" name="LastName" placeholder="Last Name" required>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<label>Email</label>
<input type="email" class="form-control" id="email" name="email" placeholder="Email" required>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
<div class="form-row form-group">
<div class="form-group col-md-2">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-primary">Sign Up</button>
</div>
<div class="form-group col-md-4">
</div>
<div class="form-group col-md-2">
</div>
</div>
</form>
<a href="/login" type="submit" class="btn btn-outline-primary">Sign in</a>
<a href="/" type="submit" class="btn btn-outline-secondary">Home</a>
</body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.js"></script>
<script>
$('#registerForm').validate({ // initialize the plugin
rules: {
"FirstName": {
required: true,
minlength: 3
},
"LastName": {
required: true,
minlength: 3
},
"email": {
required: true,
email: true
},
"password": {
required: true,
minlength: 8
}
},
messages: {
"FirstName": {
required: "Please, enter your first name",
minlength: "Minimum length should be 3 digits",
},
"LastName": {
required: "Please, enter your last name",
minlength: "Minimum length should be 3 digits",
},
"email": {
required: "Please, enter an email",
email: "Email is invalid"
},
"password": {
required: "Please,enter a password",
minlength: "Minimum length should be 8"
}
},
submitHandler: function (form) {
form.submit();
}
});
</script>
</html>
dashboard.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dashboard</title>
<meta charset="UTF-8" />
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans:400,400i,700,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<link rel="stylesheet" href="../asset/css/style.css" crossorigin="anonymous">
<link rel="stylesheet" href="../asset/css/responsive.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css">
</head>
<body class="success-body">
<div class="card">
<div class="flex invite-user">
<p>Welcome {{.FirstName}} {{.LastName}} !</p>
<a href="/logouth">Log out</a>
</div>
<h1>Successfully logged in </h1>
</div>
</body>
</html>
400.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bad Request</title>
<!-- Google font -->
<link href="https://fonts.googleapis.com/css?family=Josefin+Sans:400,700" rel="stylesheet">
<!-- Custom stlylesheet -->
<link type="text/css" rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<div id="notfound">
<div class="notfound">
<div class="notfound-404">
<h1>4<span>0</span>0</h1>
</div>
<p>Bad request.</p>
<a href="/">home page</a>
</div>
</div>
</body><!-- This templates was made by Colorlib (https://colorlib.com) -->
</html>
401.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Unauthorized Access</title>
<!-- Google font -->
<link href="https://fonts.googleapis.com/css?family=Josefin+Sans:400,700" rel="stylesheet">
<!-- Custom stlylesheet -->
<link type="text/css" rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<div id="notfound">
<div class="notfound">
<div class="notfound-404">
<h1>4<span>0</span>1</h1>
</div>
<p>Unauthorized Access.</p>
<a href="/">home page</a>
</div>
</div>
</body><!-- This templates was made by Colorlib (https://colorlib.com) -->
</html>
We create a static
directory in our base directory, create a css
folder and add a style.css
file. Inside the style.css
, put in:
style.css
:
* {
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
body {
padding: 0;
margin: 0;
}
#notfound {
position: relative;
height: 100vh;
background-color: #222;
}
#notfound .notfound {
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.notfound {
max-width: 460px;
width: 100%;
text-align: center;
line-height: 1.4;
}
.notfound .notfound-404 {
height: 158px;
line-height: 153px;
}
.notfound .notfound-404 h1 {
font-family: 'Josefin Sans', sans-serif;
color: #222;
font-size: 220px;
letter-spacing: 10px;
margin: 0px;
font-weight: 700;
text-shadow: 2px 2px 0px #c9c9c9, -2px -2px 0px #c9c9c9;
}
.notfound .notfound-404 h1>span {
text-shadow: 2px 2px 0px #ffab00, -2px -2px 0px #ffab00, 0px 0px 8px #ff8700;
}
.notfound p {
font-family: 'Josefin Sans', sans-serif;
color: #c9c9c9;
font-size: 16px;
font-weight: 400;
margin-top: 0px;
margin-bottom: 15px;
}
.notfound a {
font-family: 'Josefin Sans', sans-serif;
font-size: 14px;
text-decoration: none;
text-transform: uppercase;
background: transparent;
color: #c9c9c9;
border: 2px solid #c9c9c9;
display: inline-block;
padding: 10px 25px;
font-weight: 700;
-webkit-transition: 0.2s all;
transition: 0.2s all;
}
.notfound a:hover {
color: #ffab00;
border-color: #ffab00;
}
@media only screen and (max-width: 480px) {
.notfound .notfound-404 {
height: 122px;
line-height: 122px;
}
.notfound .notfound-404 h1 {
font-size: 122px;
}
}
Now, the frontend is complete. Some pages or specific features might not work (e.g. dashboard.html with the welcome {{.FirstName}}) until we connect the backend, which is our next task.
PermalinkSetting up the backend
The backend is where the bulk of the work is. We would be connecting the routes to the pages, authenticating by registering our users, storing their details in the MySQL database and logging our users in. In ourmain.go
file, we apply the following code:
package main
import (
"html/template"
"log"
"net/http"
"github.com/gorilla/context"
)
var tpl = template.Must(template.ParseGlob("templates/*.html"))
func indexHandler(w http.ResponseWriter, r *http.Request) {
tpl.ExecuteTemplate(w, "index.html", nil)
}
func registerHandler(w http.ResponseWriter, r *http.Request) {
tpl.ExecuteTemplate(w, "register.html", nil)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
tpl.ExecuteTemplate(w, "login.html", nil)
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/login", http.StatusPermanentRedirect)
}
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
tpl.ExecuteTemplate(w, "dashboard.html", nil)
}
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/logouth", logoutHandler)
http.HandleFunc("/register", registerHandler)
http.HandleFunc("/dashboard", dashboardHandler)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
log.Println("Server started on: http://localhost:8000")
err := http.ListenAndServe(":8000", context.ClearHandler(http.DefaultServeMux)) // context to prevent memory leak
if err != nil {
log.Fatal(err)
}
}
Our main function handles the routing, starts our server and use a context package to prevent memory leaks when running our server. The http.Handle("/static/"...) helps us serve our CSS to the frontend: The tpl variable holds our templates, and the functions execute the templates:
Before running our application, we run: go get github.com/gorilla/context
to install the context package.
Then run our server by running go run main.go
which starts on port 8000:
Our home page (localhost:8000) shows this:
We create a .env
file in our root directory which houses some environmental variables:
PermalinkSetting up the database
Permalink: I am using MySQL and the terminal for this, but you can use other databases such as Postgres or MongoDB and you can also use MySQL workbench if you do not want to use the terminal.
Log into the MySQL workspace and run the commands:
-- Creating our database
CREATE DATABASE golangwebauth;
USE DATABASE golangwebauth;
CREATE TABLE user(
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
firstname VARCHAR(255) NOT NULL,
lastname VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
createdDate TIMESTAMP
);
-- Checking the content of the user table
DESCRIBE user;
-- Getting all the records in the user table
SELECT * FROM user;
- We create a function to connect our code to the MySQL database:We use this package: github.com/joho/godotenv to get the environmental variables from our .env file and do not forget to run
func dbConn() (db *sql.DB) { err := godotenv.Load(".env") if err != nil { log.Fatal("Error loading .env file") } dbDriver := os.Getenv("DB_DRIVER") dbUser := os.Getenv("DB_USER") dbPass := os.Getenv("DB_PASSWORD") dbName := os.Getenv("DB_NAME") fmt.Println(dbDriver, dbUser, dbPass, dbName) db, err = sql.Open(dbDriver, dbUser+":"+dbPass+"@tcp(127.0.0.1:3306)/"+dbName+"?parseTime=true") if err != nil { panic(err.Error()) } fmt.Println("DB Connected!!") return db }
go get github.com/joho/godotenv
to download the package and remember to add it as an import.
PermalinkNext, we work on the registration
Permalink: Our register.html looks like this:
We create a struct type to hold the fields in our database:
type User struct {
ID int
FirstName string `json:"firstname" validate:"required, gte=3"`
LastName string `json:"lastname" validate:"required, gte=3"`
Email string `json:"email"`
Password string `json:"password"`
CreatedDate time.Time `json:"createdDate"`
}
We update our registerHandler function to add the database function and users into our database:
func registerHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
db := dbConn()
firstName := r.FormValue("FirstName")
lastName := r.FormValue("LastName")
email := r.FormValue("email")
fmt.Printf("%s, %s, %s\n", firstName, lastName, email)
password, err := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost)
if err != nil {
fmt.Println(err)
tpl.ExecuteTemplate(w, "Register", err)
}
dt := time.Now()
createdDateString := dt.Format("2006-01-02 15:04:05")
// Convert the time before inserting into the database
createdDate, err := time.Parse("2006-01-02 15:04:05", createdDateString)
if err != nil {
log.Fatal("Error converting the time:", err)
}
_, err = db.Exec("INSERT INTO user(firstname, lastname,email,password,createdDate) VALUES(?,?,?,?,?)", firstName, lastName, email, password, createdDate)
if err != nil {
fmt.Println("Error when inserting: ", err.Error())
panic(err.Error())
}
log.Println("=> Inserted: First Name: " + firstName + " | Last Name: " + lastName)
http.Redirect(w, r, "/login", http.StatusMovedPermanently)
} else if r.Method == "GET" {
tpl.ExecuteTemplate(w, "register.html", nil)
}
}
We add this package: golang.org/x/crypto/bcrypt to our imports for hashing the user's password and we run go get golang.org/x/crypto/bcrypt
to install the package. Also, import the MySQL driver: github.com/go-sql-driver/mysql to aid in our MySQL connection.
What our import looks like: Stop the server and start again, then fill the registration form and viola, our user's details have been inserted into the database:
Next is the loginHandler function so the user can log in: We update the loginHandler function to check if the user is making a POST request, then we get the values of the email and password, validating the password and checking if it corresponds to the hashed password in the database, then we execute the dashboardHandler function if all things are correct.
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
db := dbConn()
email := r.FormValue("email")
password := r.FormValue("password")
fmt.Printf("%s, %s\n", email, password)
if strings.Trim(email, " ") == "" || strings.Trim(password, " ") == "" {
fmt.Println("Parameter's can't be empty")
http.Redirect(w, r, "/login", http.StatusMovedPermanently)
return
}
checkUser, err := db.Query("SELECT id, createdDate, password, firstname, lastname, email FROM user WHERE email=?", email)
if err != nil {
panic(err.Error())
}
user := &User{}
for checkUser.Next() {
var id int
var password, firstName, lastName, email string
var createdDate time.Time
err = checkUser.Scan(&id, &createdDate, &password, &firstName, &lastName, &email)
if err != nil {
panic(err.Error())
}
user.ID = id
user.FirstName = firstName
user.LastName = lastName
user.Email = email
user.Password = password
user.CreatedDate = createdDate
}
errf := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if errf != nil && errf == bcrypt.ErrMismatchedHashAndPassword { //Password does not match!
fmt.Println(errf)
http.Redirect(w, r, "/login", http.StatusMovedPermanently)
} else {
tpl.ExecuteTemplate(w, "dashboard.html", user)
return
}
} else if r.Method == "GET" {
tpl.ExecuteTemplate(w, "login.html", nil)
}
}
Restart our server : Our dashboard after user has logged in:
And we are done with part 1. Having completed this project, Part 2 will include the use of JWT(JSON Web Token) authentication which is used when a user logs in, they have access to the dashboard but if they aren't logged in, they do not have access (our error pages come into play here). Till next time, stay safe and happy coding.