In this article, we will build a simple user authentication functionality using JWT (JSON Web Token). In the examples, I’m going to use a Go Echo framework. This will allow us to avoid writing some boilerplate code.
If you are not familiar with a JWT theory, please refer to this resource.
I believe that the easiest way to understand how to work with JWT authentication is by solving a real-world problem. Let’s say, that we have a website with an administration section, that should be accessible only by authenticated users, by providing some credentials. If authentication was successful, the user can access the administration section. If the user is inactive during a defined period of time, we should log him out from the system.
In the beginning, I’m going to create a main.go
file with initialization of the web server and some routers:
package main
import (
"fmt"
"github.com/alexsergivan/blog-examples/authentication/auth"
"github.com/alexsergivan/blog-examples/authentication/controllers"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
// Defining of the admin router group.
adminGroup := e.Group("/admin")
// Router for "/admin" path.
adminGroup.GET("", controllers.Admin())
// Starting the server.
e.Logger.Fatal(e.Start(":8777"))
}
And controllers/admin.go:
package controllers
import (
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)
func Admin() echo.HandlerFunc {
return func(c echo.Context) error {
return c.String(http.StatusOK, "Hi, you have access!")
}
}
If you run this code and go to http://localhost:8777/admin
, you will access this page without any authentication.
Let’s protect this path, by adding a JWT authentication.
First, what I’m going to create - it’s an auth
package, where we will keep all JWT related logic.
Please refer to the code below with added explanatory comments:
package auth
import (
"github.com/alexsergivan/blog-examples/authentication/user"
"github.com/labstack/echo/v4"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
)
const (
accessTokenCookieName = "access-token"
// Just for the demo purpose, I declared a secret here. In the real-world application, you might need to get it from the env variables.
jwtSecretKey = "some-secret-key"
)
func GetJWTSecret() string {
return jwtSecretKey
}
// Create a struct that will be encoded to a JWT.
// We add jwt.StandardClaims as an embedded type, to provide fields like expiry time.
type Claims struct {
Name string `json:"name"`
jwt.StandardClaims
}
// GenerateTokensAndSetCookies generates jwt token and saves it to the http-only cookie.
func GenerateTokensAndSetCookies(user *user.User, c echo.Context) error {
accessToken, exp, err := generateAccessToken(user)
if err != nil {
return err
}
setTokenCookie(accessTokenCookieName, accessToken, exp, c)
setUserCookie(user, exp, c)
return nil
}
func generateAccessToken(user *user.User) (string, time.Time, error) {
// Declare the expiration time of the token (1h).
expirationTime := time.Now().Add(1 * time.Hour)
return generateToken(user, expirationTime, []byte(GetJWTSecret()))
}
// Pay attention to this function. It holds the main JWT token generation logic.
func generateToken(user *user.User, expirationTime time.Time, secret []byte) (string, time.Time, error) {
// Create the JWT claims, which includes the username and expiry time.
claims := &Claims{
Name: user.Name,
StandardClaims: jwt.StandardClaims{
// In JWT, the expiry time is expressed as unix milliseconds.
ExpiresAt: expirationTime.Unix(),
},
}
// Declare the token with the HS256 algorithm used for signing, and the claims.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Create the JWT string.
tokenString, err := token.SignedString(secret)
if err != nil {
return "", time.Now(), err
}
return tokenString, expirationTime, nil
}
// Here we are creating a new cookie, which will store the valid JWT token.
func setTokenCookie(name, token string, expiration time.Time, c echo.Context) {
cookie := new(http.Cookie)
cookie.Name = name
cookie.Value = token
cookie.Expires = expiration
cookie.Path = "/"
// Http-only helps mitigate the risk of client side script accessing the protected cookie.
cookie.HttpOnly = true
c.SetCookie(cookie)
}
// Purpose of this cookie is to store the user's name.
func setUserCookie(user *user.User, expiration time.Time, c echo.Context) {
cookie := new(http.Cookie)
cookie.Name = "user"
cookie.Value = user.Name
cookie.Expires = expiration
cookie.Path = "/"
c.SetCookie(cookie)
}
// JWTErrorChecker will be executed when user try to access a protected path.
func JWTErrorChecker(err error, c echo.Context) error {
// Redirects to the signIn form.
return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("userSignInForm"))
}
After finishing the main JWT token functionality, let’s add the SignIn controllers, which will handle user authentication.
First, we need to add the new routers inside main()
function:
e.GET("/user/signin", controllers.SignInForm()).Name = "userSignInForm"
e.POST("/user/signin", controllers.SignIn())
In the code below I created a user
package with the user structure and a function that loads a dummy user from imaginary database. We gonna need it in our controllers to process and validate user data.
package user
import "golang.org/x/crypto/bcrypt"
type User struct {
Password string `json:"password" form:"password"`
Name string `json:"name" form:"name"`
}
func LoadTestUser() *User {
// Just for demonstration purpose, we create a user with the encrypted "test" password.
// In real-world applications, you might load the user from the database by specific parameters (email, username, etc.)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("test"), 8)
return &User{Password: string(hashedPassword), Name: "Test user"}
}
After this, we will create a controllers
package, where we add SignInForm()
and SignIn()
functions:
package controllers
import (
"github.com/alexsergivan/blog-examples/authentication/auth"
"github.com/alexsergivan/blog-examples/authentication/user"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
"html/template"
"net/http"
"path"
)
// SignInForm responsible for signIn Form rendering.
func SignInForm() echo.HandlerFunc {
return func(c echo.Context) error {
fp := path.Join("templates", "signIn.html")
tmpl, err := template.ParseFiles(fp)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
if err := tmpl.Execute(c.Response().Writer, nil); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return nil
}
}
// SignIn will be executed after SignInForm submission.
func SignIn() echo.HandlerFunc {
return func(c echo.Context) error {
// Load our "test" user.
storedUser := user.LoadTestUser()
// Initiate a new User struct.
u := new(user.User)
// Parse the submitted data and fill the User struct with the data from the SignIn form.
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
// Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(u.Password)); err != nil {
// If the two passwords don't match, return a 401 status.
return echo.NewHTTPError(http.StatusUnauthorized, "Password is incorrect")
}
// If password is correct, generate tokens and set cookies.
err := auth.GenerateTokensAndSetCookies(storedUser, c)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Token is incorrect")
}
return c.Redirect(http.StatusMovedPermanently, "/admin")
}
}
Now, in the /templates
folder we need to create a signIn.html
template with the simple SignIn form:
<!DOCTYPE html>
<html lang="en">
<form class="w-full" method="post" action="/user/signin">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
<button type="submit">Sign In</button>
</form>
</html>
Let’s also modify the admin
controller in controllers/admin.go
:
package controllers
import (
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)
func Admin() echo.HandlerFunc {
return func(c echo.Context) error {
// Gets user cookie.
userCookie, _ := c.Cookie("user")
return c.String(http.StatusOK, fmt.Sprintf("Hi, %s! You have been authenticated!", userCookie.Value))
}
}
And finally, we need to add a JWT Middleware to the adminGroup path inside the main()
function:
// Read more about JWT Middleware here: https://echo.labstack.com/middleware/jwt
adminGroup.Use(middleware.JWTWithConfig(middleware.JWTConfig{
Claims: &auth.Claims{},
SigningKey: []byte(auth.GetJWTSecret()),
TokenLookup: "cookie:access-token", // "<source>:<name>"
ErrorHandlerWithContext: auth.JWTErrorChecker,
}))
After that, execute go run main.go
, to run the server, and go to /admin
path. You will be redirected to the /user/signin
path, because you need to be authenticated to access it. That is exactly what we need! Just enter test
password and click on Sign In
button. You will see this message: Hi, Test user! You have been authenticated!
Awesome!
As you remember earlier, we set expiration time for the token: expirationTime := time.Now().Add(1 * time.Hour)
It means, that after 1 hour user will be automatically logged-out. This is something what we want to prevent, especially if user is still active and doing some work on our resource.
This is possible to solve, by introducing a Refresh token. This token will have a much longer life-time and will be used for refreshing the Access token.
Let’s modify our previous code.
First of all, we need to declare a secret for the Refresh token and cookie name to store the generated JWT. I will store it in a constant, but in the real-world applications please use environment variables for security reasons.
Inside auth
package (/auth/auth.go) we need to add these modifications:
const (
accessTokenCookieName = "access-token"
refreshTokenCookieName = "refresh-token"
jwtSecretKey = "some-secret-key"
jwtRefreshSecretKey = "some-refresh-secret-key"
)
func GetRefreshJWTSecret() string {
return jwtRefreshSecretKey
}
func GenerateTokensAndSetCookies(user *user.User, c echo.Context) error {
accessToken, exp, err := generateAccessToken(user)
if err != nil {
return err
}
setTokenCookie(accessTokenCookieName, accessToken, exp, c)
setUserCookie(user, exp, c)
// We generate here a new refresh token and saving it to the cookie.
refreshToken, exp, err := generateRefreshToken(user)
if err != nil {
return err
}
setTokenCookie(refreshTokenCookieName, refreshToken, exp, c)
return nil
}
func generateRefreshToken(user *user.User) (string, time.Time, error) {
// Declare the expiration time of the token - 24 hours.
expirationTime := time.Now().Add(24 * time.Hour)
return generateToken(user, expirationTime, []byte(GetRefreshJWTSecret()))
}
At this point, when the user is signing-in, we generate 2 tokens: access and refresh. We still need to add logic for updating the access token, if the user is still active. For that, we can add a middleware, where we can check how much time is left for the user’s access token, and if this time is less than some period of time (in this example it’s 15 mins) we can generate the new tokens, by providing a valid refresh token.
// TokenRefresherMiddleware middleware, which refreshes JWT tokens if the access token is about to expire.
func TokenRefresherMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// If the user is not authenticated (no user token data in the context), don't do anything.
if c.Get("user") == nil {
return next(c)
}
// Gets user token from the context.
u := c.Get("user").(*jwt.Token)
claims := u.Claims.(*Claims)
// We ensure that a new token is not issued until enough time has elapsed.
// In this case, a new token will only be issued if the old token is within
// 15 mins of expiry.
if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) < 15*time.Minute {
// Gets the refresh token from the cookie.
rc, err := c.Cookie(refreshTokenCookieName)
if err == nil && rc != nil {
// Parses token and checks if it valid.
tkn, err := jwt.ParseWithClaims(rc.Value, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(GetRefreshJWTSecret()), nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
c.Response().Writer.WriteHeader(http.StatusUnauthorized)
}
}
if tkn != nil && tkn.Valid {
// If everything is good, update tokens.
_ = GenerateTokensAndSetCookies(&user.User{
Name: claims.Name,
}, c)
}
}
}
return next(c)
}
}
And finally, we have to attach our middleware to the adminGroup
router inside the main()
function:
adminGroup.Use(auth.TokenRefresherMiddleware)
You can run the server again and experiment, how does it work. As an example, you can change the access token lifetime to 1min and investigate how the jwt cookies behave.
That was pretty much it. I hope this article was helpful for you.
The complete source code you can found here.
Hello! In this post, I will explain the cost-effective method I use to host my Go web applications with varying levels of complexity, all starting from as low as $5 per month. This method also allows to easy deploy and scale your golang application.
go hosting digitalocean dockerSince Go 1.19 we can use a new 103 (Early Hints)
http status code when we create web applications. Let’s figure out how and when this could help us.
We are going to create a simple golang web server that servers some html content. One html page will be served with 103
header and another one without.
After loading comparison we will see how early hints can improve page performance.
I guess that almost everyone in the go community was exciting when Go 1.18 was released, especially because of generics. Some days ago I decided to try generics in the real-world application, by refactoring some of its pieces, related to a caching logic.
go generics redis cacheThis time, I will show you how to work with the maps in go effectively and prevent the occurrence of the data race errors. Data races happen when several goroutines access the same resource concurrently and at least one of the accesses is a write.
go concurrent map data raceRecently, I discovered a surprisingly reliable memory caching solution, which I’m planning to use in all my further applications to increase performance. In this blog post, I will share some code examples of how you can integrate Ristretto caching library into your application.
go caching ristretto performance