JWT in Action: Secure Authentication & Authorization in Go
James Reed
Infrastructure Engineer · Leapcell
![Cover of "JWT in Action: Secure Authentication & Authorization in Go"](https://cdn1.leapcell.io/2144927599Group481.png)
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
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
A correct JWT format is as follows:
eyJhbGciOiJIUzI1NiIsInR5c.eyJ1c2VybmFtZaYjiJ9._eCVNYFYnMXwpgGX9Iu412EQSOFuEGl2c
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 }
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" }
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>
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) }
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) }
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 }
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) }
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"` }
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) }
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.", }) }
After running the server, send a login request through the curl command, such as:
curl http://localhost:8005/login -d "username=admin&password=admin"
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"}
Leapcell: The Best Serverless Platform for Golang Hosting
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.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ