DEV Community

Cover image for JWT in Action: Secure Authentication & Authorization in Go
Leapcell
Leapcell

Posted on

1 1 1 1 1

JWT in Action: Secure Authentication & Authorization in Go

Image description

Leapcell: The Best Serverless Platform for Golang Hosting

In - depth Explanation of JWT: Principles, Format, Features, and Application in Go Projects

What is JWT

JWT is the abbreviation of JSON Web Token, which is a cross - domain authentication solution. It plays a vital role in web applications, enabling secure and convenient authentication and information transfer.

Problems Solved by Using JWT

Image description

Limitations of the Traditional User Authentication Process

Traditional authentication relies on client - side cookies and server - side sessions. This works well for single - server applications. However, when it comes to multi - server deployments, there is a problem of session sharing. For example, in a large - scale distributed system where multiple servers work together, each server maintains an independent session. When a user switches between servers, there may be inconsistent login states. At the same time, single - sign - on cannot be achieved across different domains through cookie + session because cookies are set based on domains, and different domains cannot directly share cookies, which restricts unified authentication and access control in multi - business systems.

Advantages of JWT

JWT makes applications stateless, avoiding the need for session sharing. It includes user information within its own structure. Servers do not need to store sessions. Instead, they can confirm the user's identity by verifying the validity of the JWT for each request. In a distributed system, JWT facilitates server expansion and is not affected by the number and distribution of servers.

Format of JWT

Image description

A correct JWT format is as follows:

eyJhbGciOiJIUzI1NiIsInR5c.eyJ1c2VybmFtZaYjiJ9._eCVNYFYnMXwpgGX9Iu412EQSOFuEGl2c
Enter fullscreen mode Exit fullscreen mode

As can be seen, a JWT string consists of three parts: Header, Payload, and Signature, connected by dots.

Header

The Header is a JSON object composed of two parts: the token type and the encryption algorithm. For example:

{
  "typ": "JWT",// Usually "JWT"
  "alg": "HS256"// Supports various encryption algorithms
}
Enter fullscreen mode Exit fullscreen mode

Convert the above JSON object into a string using the Base64URL algorithm to obtain the Header part of the JWT. It should be noted that JWT encoding does not use standard Base64 but Base64Url. This is because in the string generated by Base64, there may be three special symbols in URLs: +, /, and =. And we may pass the token on the URL (e.g., test.com?token = xxx). The Base64URL algorithm, on the basis of the string generated by the Base64 algorithm, omits the =, replaces + with -, and replaces / with _. This ensures that the generated string can be passed in the URL without problems.

Payload

The Payload part of the JWT, like the Header, is also a JSON object used to store the actual data we need. The JWT standard provides seven optional fields, namely:

  • iss(issuer): The issuer, whose value is a case - sensitive string or Uri.
  • sub(subject): The subject, used to identify a user.
  • exp(expiration time): The expiration time.
  • aud(audience): The audience.
  • iat(issued at): The issued time.
  • nbf(not before): The time before which the JWT is not valid.
  • jti(JWT ID): The identifier.

In addition to the standard fields, we can define private fields as needed to meet business requirements. For example:

{
    iss:"admin",// Standard field
    jti:"test",// Standard field
    username:"leapcell",// Custom field
    "gender":"male",
    "avatar":"https://avatar.leapcell.jpg"
}
Enter fullscreen mode Exit fullscreen mode

Convert the above JSON object into a string using the Base64URL algorithm to obtain the Payload part of the JWT.

Signature

The Signature is the signature of the JWT. The generation method is as follows: Encode the Header and Payload using the Base64URL algorithm, connect them with a dot, and then encrypt them using the secret key (secretKey) and the encryption method specified in the Header to finally generate the Signature. The role of the signature is to ensure that the JWT has not been tampered with during transmission. The server can verify the integrity and authenticity of the JWT by verifying the signature.

Features of JWT

  • Security Recommendation: It is best to use the HTTPS protocol to prevent the possibility of JWT theft. Because under the HTTP protocol, data transmission is in plaintext, which is easy to be intercepted and tampered with. HTTPS can effectively protect the security of JWT through encrypted transmission.
  • Limitation of the Invalidation Mechanism: Except for the expiration of the JWT's issued time, there is no other way to invalidate an already - generated JWT, unless the server - side changes the algorithm. This means that once a JWT is issued, if it is stolen within the validity period, it may be maliciously used.
  • Storage of Sensitive Information: When the JWT is not encrypted, sensitive information should not be stored in it. If sensitive information needs to be stored, it is best to encrypt it again. Because the JWT itself can be decoded. If it contains sensitive information and is not encrypted, there will be a security risk.
  • Setting of Expiration Time: It is advisable to set a short expiration time for the JWT to prevent it from remaining valid if stolen, reducing potential losses. A short expiration time can reduce the risk after the JWT is stolen. Even if it is stolen, its valid time is limited.
  • Storage of Business Information: The Payload of the JWT can also store some business information, which can reduce database queries. For example, basic user information can be stored in the Payload. Each time a request is made, the server can directly obtain this information from the JWT without querying the database again, improving the performance and response speed of the system.

Usage of JWT

After the server issues the JWT, it sends it to the client. If the client is a browser, it can be stored in a cookie or localStorage. If it is an APP, it can be stored in an sqlite database. Then, for each interface request, the JWT is carried. There are many ways to carry it to the server - side, such as query, cookie, header, or body. In short, any way that can carry data to the server can be used. However, the more standardized approach is to upload it through the header Authorization, with the following format:

Authorization: Bearer <token>
Enter fullscreen mode Exit fullscreen mode

This way of passing the JWT in the HTTP request header conforms to common authentication specifications and is convenient for the server to perform unified authentication processing.

Using JWT in Go Projects

Generating JWT

Use the github.com/golang - jwt/jwt library to help us generate or parse JWT. We can use the NewWithClaims() method to generate a Token object and then use the method of the Token object to generate a JWT string. For example:

package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt"
)

func main() {
    hmacSampleSecret := []byte("123")// Secret key, must not be leaked
    // Generate a token object
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "foo": "bar",
    "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    })
    // Generate a jwt string
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}
Enter fullscreen mode Exit fullscreen mode

We can also use the New() method to generate a Token object and then generate a JWT string. For example:

package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt"
)

func main() {
    hmacSampleSecret := []byte("123")
    token := jwt.New(jwt.SigningMethodHS256)
    // Data cannot be carried when created through the New method, so data can be defined by assigning values to token.Claims
    token.Claims = jwt.MapClaims{
    "foo": "bar",
    "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    }
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}
Enter fullscreen mode Exit fullscreen mode

In the above examples, the data in the Payload of the JWT is defined through the jwt.MapClaims data structure. In addition to using jwt.MapClaims, we can also use a custom structure. However, this structure must implement the following interface:

type Claims interface {
    Valid() error
}
Enter fullscreen mode Exit fullscreen mode

The following is an example of implementing a custom data structure:

package main

import (
    "fmt"
    "github.com/golang-jwt/jwt"
)

type CustomerClaims struct {
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
}

func (c CustomerClaims) Valid() error {
return nil
}

func main() {
    // Secret key
    hmacSampleSecret := []byte("123")
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = CustomerClaims{
        Username: "Leapcell",
    Gender:   "male",
    Avatar:   "https://avatar.leapcell.jpg",
    Email:    "admin@test.org",
    }
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}
Enter fullscreen mode Exit fullscreen mode

If we want to use the fields defined in the JWT standard in the custom structure, we can do it like this:

type CustomerClaims struct {
    *jwt.StandardClaims// Standard fields
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
}
Enter fullscreen mode Exit fullscreen mode

Parsing JWT

Parsing is the reverse operation of generation. We parse a token to obtain its Header, Payload, and verify whether the data has been tampered with through the Signature. The following is the specific implementation:

package main

import (
    "fmt"

    "github.com/golang-jwt/jwt"
)

type CustomerClaims struct {
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
    jwt.StandardClaims
}

func main() {
    var hmacSampleSecret = []byte("111")
        // The token generated in the previous example
    tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuWwj-aYjiIsImdlbmRlciI6IueUtyIsImF2YXRhciI6Imh0dHBzOi8vMS5qcGciLCJlbWFpbCI6InRlc3RAMTYzLmNvbSJ9.mJlWv5lblREwgnP6wWg-P75VC1FqQTs8iOdOzX6Efqk"

    token, err := jwt.ParseWithClaims(tokenString, &CustomerClaims{}, func(t *jwt.Token) (interface{}, error) {
        return hmacSampleSecret, nil
    })

    if err!= nil {
        fmt.Println(err)
        return
    }
    claims := token.Claims.(*CustomerClaims)
    fmt.Println(claims)
}
Enter fullscreen mode Exit fullscreen mode

Using JWT in Gin Projects

In the Gin framework, login authentication is generally implemented through middleware. The github.com/appleboy/gin - jwt library has integrated the implementation of github.com/golang - jwt/jwt and defined corresponding middleware and controllers for us. The following is a specific example:

package main

import (
    "log"
    "net/http"
    "time"

    jwt "github.com/appleboy/gin-jwt/v2"
    "github.com/gin-gonic/gin"
)

// Used to receive the username and password for login
type login struct {
    Username string `form:"username" json:"username" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

var identityKey = "id"

// Data in the payload of jwt
type User struct {
    UserName  string
    FirstName string
    LastName  string
}

func main() {

    // Define a Gin middleware
    authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
    Realm:            "test zone",          // Identification
    SigningAlgorithm: "HS256",              // Encryption algorithm
    Key:              []byte("secret key"), // Secret key
    Timeout:          time.Hour,
    MaxRefresh:       time.Hour,   // Maximum refresh extension time
    IdentityKey:      identityKey, // Specify the id of the cookie
    PayloadFunc: func(data interface{}) jwt.MapClaims { // Payload, where the data in the payload of the returned jwt can be defined
            if v, ok := data.(*User); ok {
        return jwt.MapClaims{
                    identityKey: v.UserName,
        }
            }
            return jwt.MapClaims{}
    },
    IdentityHandler: func(c *gin.Context) interface{} {
            claims := jwt.ExtractClaims(c)
            return &User{
                UserName: claims[identityKey].(string),
            }
    },
    Authenticator: Authenticator, // Login verification logic can be written here
    Authorizator: func(data interface{}, c *gin.Context) bool { // When a user requests a restricted interface through a token, this logic will be executed
            if v, ok := data.(*User); ok && v.UserName == "admin" {
        return true
            }

            return false
    },
    Unauthorized: func(c *gin.Context, code int, message string) { // Response when there is an error
        c.JSON(code, gin.H{
                    "code":    code,
                    "message": message,
        })
    },
        // Specify where to get the token. The format is: "<source>:<name>". If there are multiple, separate them with commas
    TokenLookup:   "header: Authorization, query: token, cookie: jwt",
    TokenHeadName: "Bearer",
    TimeFunc:      time.Now,
})

    if err!= nil {
        log.Fatal("JWT Error:" + err.Error())
    }
    r := gin.Default()
    // Login interface
    r.POST("/login", authMiddleware.LoginHandler)
    auth := r.Group("/auth")
    // Logout
    auth.POST("/logout", authMiddleware.LogoutHandler)
    // Refresh token, extend the token's validity period
    auth.POST("/refresh_token", authMiddleware.RefreshHandler)
    auth.Use(authMiddleware.MiddlewareFunc()) // Apply the middleware
    {
        auth.GET("/hello", helloHandler)
    }

    if err := http.ListenAndServe(":8005", r); err!= nil {
    log.Fatal(err)
    }
}

func Authenticator(c *gin.Context) (interface{}, error) {
    var loginVals login
    if err := c.ShouldBind(&loginVals); err!= nil {
        return "", jwt.ErrMissingLoginValues
    }
    userID := loginVals.Username
    password := loginVals.Password

    if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
        return &User{
            UserName:  userID,
            LastName:  "Leapcell",
            FirstName: "Admin",
        }, nil
    }

    return nil, jwt.ErrFailedAuthentication
}

// Controller for handling the /hello route
func helloHandler(c *gin.Context) {
    claims := jwt.ExtractClaims(c)
    user, _ := c.Get(identityKey)
    c.JSON(200, gin.H{
        "userID":   claims[identityKey],
        "userName": user.(*User).UserName,
        "text":     "Hello World.",
    })
}
Enter fullscreen mode Exit fullscreen mode

After running the server, send a login request through the curl command, such as:

curl http://localhost:8005/login -d "username=admin&password=admin"
Enter fullscreen mode Exit fullscreen mode

The response result returns the token, such as:

{"code":200,"expire":"2021-12-16T17:33:39+08:00","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI"}
Enter fullscreen mode Exit fullscreen mode

Leapcell: The Best Serverless Platform for Golang Hosting

Image description

Finally, I would like to recommend a platform that is most suitable for deploying Golang services: Leapcell

1. Multi - Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • Pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay - as - you - go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real - time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto - scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Image description

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs