Web Development JWT Practices

baize1998

baize

Posted on November 17, 2022

Web Development JWT Practices

Foreword

The previous post briefly introduced a high-performance Go HTTP framework - Hertz. This article is based on a demo from the Hertz open-source repository, which describes how to use Hertz to complete the JWT authentication and authorization process.

It should be noted here that hertz-jwt is one of many external extensions to Hertz, it is forked from gin-jwt and adapted to Hertz.

Hertz's rich extension ecosystem is a great convenience for developers and worth exploring beyond this article.

image-20221114215243547

Introduction

  • Use hz to generate code
  • Use JWT to complete login and authentication
  • Use Gorm and MySQL

Download

git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_jwt
Enter fullscreen mode Exit fullscreen mode

Architecture

hertz_jwt
├── Makefile # Generate hertz scaffolding code using the hz command line tool
├── biz
│   ├── dal
│   │   ├── init.go 
│   │   └── mysql
│   │       ├── init.go # Initialising database connections
│   │       └── user.go # Database operations
│   ├── handler
│   │   ├── ping.go
│   │   └── register.go # Register handler
│   ├── model
│   │   ├── sql
│   │   │   └── user.sql
│   │   └── user.go # Defining the database model
│   ├── mw
│   │   └── jwt.go # Initialising the hertz-jwt middleware
│   ├── router
│   │   └── register.go
│   └── utils
│       └── md5.go # MD5
├── docker-compose.yml # MySQL container
├── go.mod
├── go.sum
├── main.go # Hertz startup functions
├── readme.md
├── router.go # Routing Registration
└── router_gen.go
Enter fullscreen mode Exit fullscreen mode

Analysis

The list of interfaces for this demo is as follows.

// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
    r.POST("/register", handler.Register)
    r.POST("/login", mw.JwtMiddleware.LoginHandler)
    auth := r.Group("/auth", mw.JwtMiddleware.MiddlewareFunc())
    auth.GET("/ping", handler.Ping)
}
Enter fullscreen mode Exit fullscreen mode

User Registration

The user data of the current demo is persisted through mysql by gorm, so before logging in, the user needs to be registered. The registration process is as follows.

  1. get the username, password and email address
  2. determine if the user exists
  3. create the user

User log-in (Authentication)

The server needs to validate the user account and password and sign a jwt token when the user logs in for the first time.

JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    Key:           []byte("secret key"),
    Timeout:       time.Hour,
    MaxRefresh:    time.Hour,
    Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
        var loginStruct struct {
            Account  string `form:"account" json:"account" query:"account" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
            Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
        }
        if err := c.BindAndValidate(&loginStruct); err != nil {
            return nil, err
        }
        users, err := mysql.CheckUser(loginStruct.Account, utils2.MD5(loginStruct.Password))
        if err != nil {
            return nil, err
        }
        if len(users) == 0 {
            return nil, errors.New("user already exists or wrong password")
        }

        return users[0], nil
    },
    PayloadFunc: func(data interface{}) jwt.MapClaims {
        if v, ok := data.(*model.User); ok {
            return jwt.MapClaims{
                jwt.IdentityKey: v,
            }
        }
        return jwt.MapClaims{}
    },
})
Enter fullscreen mode Exit fullscreen mode
  • Authenticator: A function that sets up the user information to be authenticated when logging in. The demo defines a loginStruct structure that receives the user login information and authenticates the validity. The return value of this function, users[0], will provide the payload data source for the subsequent generation of the jwt token.
  • PayloadFunc: Its input is the return value of Authenticator, which is responsible for parsing users[0] and injecting the username into the payload part of the token.

  • Key: specifies the key used to encrypt the jwt token as "secret key".

  • Timeout: specifies that the token is valid for one hour.

  • MaxRefresh: sets the maximum token refresh time, allowing the client to refresh the token within TokenTime + MaxRefresh, appending a Timeout duration.

Return of Token

JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
        c.JSON(http.StatusOK, utils.H{
            "code":    code,
            "token":   token,
            "expire":  expire.Format(time.RFC3339),
            "message": "success",
        })
    },
})
Enter fullscreen mode Exit fullscreen mode
  • LoginResponse: After a successful login, jwt token information is returned with the response, you can customize the content of this part, but be careful not to change the function signature as it is strongly bound to LoginHandler.

Token Validation

When a client requests a route with a jwt middleware configured, the server verifies the jwt token it carries.

JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    TokenLookup:   "header: Authorization, query: token, cookie: jwt",
    TokenHeadName: "Bearer",
    HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
        hlog.CtxErrorf(ctx, "jwt biz err = %+v", e.Error())
        return e.Error()
    },
    Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
        c.JSON(http.StatusOK, utils.H{
            "code":    code,
            "message": message,
        })
    },
})
Enter fullscreen mode Exit fullscreen mode
  • TokenLookup: This is used to set the source of the token, you can choose header, query, cookie, or param, the default is header:Authorization, the first one read from the left side takes precedence. The current demo will use header as the data source, so when accessing the /ping interface, you will need to store the token information in the HTTP Header.
  • TokenHeadName: This is used to set the prefix used to retrieve the token from the header, the default is "Bearer".
  • HTTPStatusMessageFunc: This is used to set the error message that will be included in the response when an error occurs in the jwt validation process, you can wrap these yourself.
  • Unauthorized: used to set the response function for a failed jwt validation process, the current demo returns the error code and error message.

Extracting User Information

JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    IdentityKey: IdentityKey,
    IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
        claims := jwt.ExtractClaims(ctx, c)
        return &model.User{
            UserName: claims[IdentityKey].(string),
        }
    },
})

// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
    user, _ := c.Get(mw.IdentityKey)
    c.JSON(200, utils.H{
        "message": fmt.Sprintf("username:%v", user.(*model.User).UserName),
    })
}
Enter fullscreen mode Exit fullscreen mode
  • IdentityHandler: The function used to set the identity information to be retrieved. In the demo, here the payload of the token is extracted and the username is stored in the context information with the IdentityKey.
  • IdentityKey: sets the key used to retrieve the identity, the default is "identity".
  • Ping: Constructs the response. Retrieves the username information from the context information and returns it.

Other Components

Code Generation

Most of the code above is scaffolded code generated through the hz command line tool, so developers don't need to spend a lot of time building a good code structure and just focus on writing the business.

hz new -mod github.com/cloudwego/hertz-examples/bizdemo/hertz_jwt
Enter fullscreen mode Exit fullscreen mode

Most of the code above is scaffolded code generated through the hz command line tool, so developers don't need to spend a lot of time building a good code structure and just focus on writing the business.

Sample Code(Sourced from hz Official Document):

// idl/hello.thrift
namespace go hello.example

struct HelloReq {
    1: string Name (api.query="name"); // api annotations to facilitate parameter binding
}

struct HelloResp {
    1: string RespBody;
}

service HelloService {
    HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}

// execute under $GOPATH
hz new -idl idl/hello.thrift
Enter fullscreen mode Exit fullscreen mode

Parameter Binding

Hertz uses the open-source library go-tagexpr for parameter binding and validation, which the current demo uses for user registration and login.

// register
var registerStruct struct {
    // Binding and validation of parameters by declaring tags
    Username string `form:"username" json:"username" query:"username" vd:"(len($) > 0 && len($) < 128); msg:'Illegal format'"`
    Email    string `form:"email" json:"email" query:"email" vd:"(len($) > 0 && len($) < 128) && email($); msg:'Illegal format'"`
    Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 128); msg:'Illegal format'"`
}

// login
var loginStruct struct {
    // Binding and validation of parameters by declaring tags
    Account  string `form:"account" json:"account" query:"account" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
    Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
}
if err := c.BindAndValidate(&loginStruct); err != nil {
    return nil, err
}
Enter fullscreen mode Exit fullscreen mode
  • vd: used to validate the data format, e.g. string length checks (128), email type checks (email)
  • form: binds the body content of the request. content-type -> multipart/form-data or application/x-www-form-urlencoded, binds the key-value of the form
  • json: bind the body content of the request content-type -> application/json, bind the json parameters
  • query: binds the query parameter of the request

The parameter bindings need to be in accordance with the priority.

path > form > query > cookie > header > json > raw_body
Enter fullscreen mode Exit fullscreen mode

Further usage can be found in Documentation

Gorm

For more information on Gorm's operation of MySQL, please refer to Gorm

Run

  • Running the mysql container
cd bizdemo/hertz_jwt && docker-compose up
Enter fullscreen mode Exit fullscreen mode
  • Creating a mysql database

After connecting to mysql, execute user.sql

  • run the demo
cd bizdemo/hertz_jwt && go run main.go
Enter fullscreen mode Exit fullscreen mode

API Requests

Register

# request
curl --location --request POST 'localhost:8888/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "Username": "admin",
    "Email": "admin@test.com",
    "Password": "admin"
}'
# response
{
    "code": 200,
    "message": "success"
}
Enter fullscreen mode Exit fullscreen mode

Login

# request
curl --location --request POST 'localhost:8888/login' \
--header 'Content-Type: application/json' \
--data-raw '{
    "Account": "admin",
    "Password": "admin"
}'
# response
{
    "code": 200,
    "expire": "2022-11-16T11:05:24+08:00",
    "message": "success",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg1Njc5MjQsImlkIjoyLCJvcmlnX2lhdCI6MTY2ODU2NDMyNH0.qzbDJLQv4se6dOHN51p21Rp3DjV1Lf131l_5k4cK6Wk"
}
Enter fullscreen mode Exit fullscreen mode

Routing Access

# request
curl --location --request GET 'localhost:8888/auth/ping' \
--header 'Authorization: Bearer ${token}'
# response
{
    "message": "username:admin"
}
Enter fullscreen mode Exit fullscreen mode

References

💖 💪 🙅 🚩
baize1998
baize

Posted on November 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Web Development JWT Practices
beginners Web Development JWT Practices

November 17, 2022