Go-JWT-RESTful Authentication Tutorial

1. What is JWT

JWT (JSON Web Token) is a very lightweight specification that allows us to use JWT to deliver secure and reliable information between users and servers.
A JWT consists of three parts, a Header Header, a Claims payload, and a Signature.

The principle of JWT is similar to the process of affixing official seals or handwritten signatures. There are many clauses written in the contract, not just a piece of paper, but some proof, such as signature, such as affixing a seal, which ensures that the information transmitted is true, not forged.

It encrypts the user information into the token. The server does not store any user information. The server verifies the correctness of the token by using the saved key.

2.JWT Composition

A JWT consists of three parts, a Header Header, a Claims payload, and a Signature.

  • Header Header: Header indicating the type and encryption algorithm
  • Claims load: Declaration, that is, load (what is loaded)
  • Signature: Signature. This part consists of the base64 transcoding of the header and claims and the salt (secret) of the encryption algorithm declared in the header, that is:
let tmpstr = base64(header)+base64(claims)
let signature = encrypt(tmpstr,secret)
//The last three are connected with "." that is:
let token = base64(header)+"."+base64(claims)+"."+signature

3.javascript extracts JWT string load information

In JWT, payload can contain many fields, the more fields you have, the longer your token string will be.
The more data you send for your HTTP request communication, the longer the interface response time will wait for it to return.

The code below is that front-end javascript gets the logged-in user information from payload.
Of course, back-end middleware can also directly parse payload to get user information, reducing queries to the database for user table data. Interfaces will be faster, and database pressure will be lower.
Backend checks JWT authentication to verify payload and ignature signatures are legitimate.

let tokenString = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Njc3Nzc5NjIsImp0aSI6IjUiLCJpYXQiOjE1Njc2OTE1NjIsImlzcyI6ImZlbGl4Lm1vam90di5jbiIsImlkIjo1LCJjcmVhdGVkX2F0IjoiMjAxOS0wOS0wNVQxMTo1Njo1OS41NjI1NDcwODYrMDg6MDAiLCJ1cGRhdGVkX2F0IjoiMjAxOS0wOS0wNVQxNjo1ODoyMC41NTYxNjAwOTIrMDg6MDAiLCJ1c2VybmFtZSI6ImVyaWMiLCJuaWNrX25hbWUiOiIiLCJlbWFpbCI6IjEyMzQ1NkBxcS5jb20iLCJtb2JpbGUiOiIiLCJyb2xlX2lkIjo4LCJzdGF0dXMiOjAsImF2YXRhciI6Ii8vdGVjaC5tb2pvdHYuY24vYXNzZXRzL2ltYWdlL2F2YXRhcl8zLnBuZyIsInJlbWFyayI6IiIsImZyaWVuZF9pZHMiOm51bGwsImthcm1hIjowLCJjb21tZW50X2lkcyI6bnVsbH0.tGjukvuE9JVjzDa42iGfh_5jIembO5YZBZDqLnaG6KQ'
function parseTokenGetUser(jwtTokenString) {
    let base64Url = jwtTokenString.split('.')[1];
    let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    let jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
    let user = JSON.parse(jsonPayload);

    localStorage.setItem("token", jwtTokenString);
    localStorage.setItem("expire_ts", user.exp);
    localStorage.setItem("user", jsonPayload);
    return user;
}
parseTokenGetUser(tokenString)

Copy the javascript code above and execute it in the browser console to parse the user information! Of course, you can use online tools to parse the payload load load of jwt token
JWT Online Parsing Tool

4. go Language Gin Framework for JWT User Authentication

Next I'll use the most popular gin-gonic/gin and dgrijalva/jwt-go
These two package s demonstrate how to use JWT authentication.

4.1 Logon Interface

4.1.1 Login-route

https://github.com/libragen/felix/blob/master/ssh2ws/ssh2ws.go

    r := gin.New()
    r.MaxMultipartMemory = 32 << 20
    //sever static file in http's root path
    binStaticMiddleware, err := felixbin.NewGinStaticBinMiddleware("/")
    if err != nil {
        return err
    }
    //Supports cross-domain
    mwCORS := cors.New(cors.Config{
        AllowOrigins:     []string{"*"},
        AllowMethods:     []string{"PUT", "PATCH", "POST", "GET", "DELETE"},
        AllowHeaders:     []string{"Origin", "Authorization", "Content-Type"},
        ExposeHeaders:    []string{"Content-Type"},
        AllowCredentials: true,
        AllowOriginFunc: func(origin string) bool {
            return true
        },
        MaxAge: 2400 * time.Hour,
    })
    r.Use(binStaticMiddleware, mwCORS)


    {
        r.POST("comment-login", internal.LoginCommenter)       //Comment User Logon
        r.POST("comment-register", internal.RegisterCommenter) //Comment on user registration
    }

    api := r.Group("api")
    api.POST("admin-login", internal.LoginAdmin) //Manage Background Logon

The internal.LoginCommenter and internal.LoginAdmin methods are the same,
Just focus on one of them and we'll focus on internal.LoginCommenter

4.1.2 login handler

Write a login handler

https://github.com/libragen/felix/blob/master/ssh2ws/internal/h_login.go

func LoginCommenter(c *gin.Context) {
    var mdl model.User
    err := c.ShouldBind(&mdl)
    if handleError(c, err) {
        return
    }
    //Get ip
    ip := c.ClientIP()
    //roleId 8 is a user of the review system
    data, err := mdl.Login(ip, 8)
    if handleError(c, err) {
        return
    }
    jsonData(c, data)
}

The most critical is the function mdl.Login(ip, 8)
https://github.com/libragen/felix/blob/master/model/m_users.go

  • 1. Database Query User
  • 2. Verify user role_id
  • 3. Compare passwords
  • 4. Prevent password leaks (empty struct properties)
  • 5. Generate JWT-string
//Login
func (m *User) Login(ip string, roleId uint) (string, error) {
    m.Id = 0
    if m.Password == "" {
        return "", errors.New("password is required")
    }
    inputPassword := m.Password
    //Get logged-in users
    err := db.Where("username = ? or email = ?", m.Username, m.Username).First(&m).Error
    if err != nil {
        return "", err
    }
    //Verify user roles
    if (m.RoleId & roleId) != roleId {
        return "", fmt.Errorf("not role of %d", roleId)
    }
    //Verify password
    //password is set to bcrypt check
    if err := bcrypt.CompareHashAndPassword([]byte(m.HashedPassword), []byte(inputPassword)); err != nil {
        return "", err
    }
    //Prevent password disclosure
    m.Password = ""
    //Generate jwt-string
    return jwtGenerateToken(m, time.Hour*24*365)
}

4.1.2 Generating JWT-string (Core Code)

  • 1. Customizing the payload structure does not recommend using the dgrijalva/jwt-go jwt.StandardClaims structure directly. Because his payload contains too little user information.
  • 2. Implement the Valid() error method of type Claims interface, and customize the checks
  • 3. Generate JWT-string jwtGenerateToken(m *User,d time.Duration) (string, error)

https://github.com/libragen/felix/blob/master/model/m_jwt.go

package model

import (
    "errors"
    "fmt"
    "time"

    "github.com/dgrijalva/jwt-go"
    "github.com/sirupsen/logrus"
)

var AppSecret = ""//viper.GetString sets this value (32byte length)
var AppIss = "github.com/libragen/felix"//This value will be overridden by viper.GetString

//Customizing the payload structure does not recommend using the dgrijalva/jwt-go `jwt.StandardClaims'structure directly. Because his payload contains too little user information.
type userStdClaims struct {
    jwt.StandardClaims
    *User
}
//Implement the `Valid() error` method of `type Claims interface`, customize the check content
func (c userStdClaims) Valid() (err error) {
    if c.VerifyExpiresAt(time.Now().Unix(), true) == false {
        return  errors.New("token is expired")
    }
    if !c.VerifyIssuer(AppIss, true) {
        return  errors.New("token's issuer is wrong")
    }
    if c.User.Id < 1 {
        return errors.New("invalid user in jwt")
    }
    return
}

func jwtGenerateToken(m *User,d time.Duration) (string, error) {
    m.Password = ""
    expireTime := time.Now().Add(d)
    stdClaims := jwt.StandardClaims{
        ExpiresAt: expireTime.Unix(),
        IssuedAt:  time.Now().Unix(),
        Id:        fmt.Sprintf("%d", m.Id),
        Issuer:    AppIss,
    }

    uClaims := userStdClaims{
        StandardClaims: stdClaims,
        User:           m,
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, uClaims)
    // Sign and get the complete encoded token as a string using the secret
    tokenString, err := token.SignedString([]byte(AppSecret))
    if err != nil {
        logrus.WithError(err).Fatal("config is wrong, can not generate jwt")
    }
    return tokenString, err
}


//JwtParseUser parses the contents of payload to get user information
//gin-middleware uses this method
func JwtParseUser(tokenString string) (*User, error) {
    if tokenString == "" {
        return nil, errors.New("no token is found in Authorization Bearer")
    }
    claims := userStdClaims{}
    _, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(AppSecret), nil
    })
    if err != nil {
        return nil, err
    }
    return claims.User, err
}

4.2 JWT Middleware

  • 1. Get JWT-string from url-query_t or from request header Authorization
  • 2.model.JwtParseUser(token) parses JWT-string to get the User structure (reduces middleware query database operations and time)
  • 3. Set user information to gin.Context Other handler s get model.User structure by gin.Context.Get(contextKeyUserObj) in User Type Assert.
  • 4. Use handle s after jwt-middle s to get user information from gin.Context

https://github.com/libragen/felix/blob/master/ssh2ws/internal/mw_jwt.go

package internal

import (
    "net/http"
    "strings"

    "github.com/libragen/felix/model"
    "github.com/gin-gonic/gin"
)

const contextKeyUserObj = "authedUserObj"
const bearerLength = len("Bearer ")

func ctxTokenToUser(c *gin.Context, roleId uint) {
    token, ok := c.GetQuery("_t")
    if !ok {
        hToken := c.GetHeader("Authorization")
        if len(hToken) < bearerLength {
            c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"msg": "header Authorization has not Bearer token"})
            return
        }
        token = strings.TrimSpace(hToken[bearerLength:])
    }
    usr, err := model.JwtParseUser(token)
    if err != nil {
        c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"msg": err.Error()})
        return
    }
    if (usr.RoleId & roleId) != roleId {
        c.AbortWithStatusJSON(http.StatusPreconditionFailed, gin.H{"msg": "roleId No privileges"})
        return
    }

    //store the user Model in the context
    c.Set(contextKeyUserObj, *usr)
    c.Next()
    // after request
}

func MwUserAdmin(c *gin.Context) {
    ctxTokenToUser(c, 2)
}

func MwUserComment(c *gin.Context) {
    ctxTokenToUser(c, 8)
}

User information is retrieved from gin.Context using handle s after jwt-middle s.
https://github.com/libragen/felix/blob/master/ssh2ws/internal/helper.go

func mWuserId(c *gin.Context) (uint, error) {
    v,exist := c.Get(contextKeyUserObj)
    if !exist {
        return 0,errors.New(contextKeyUserObj + " not exist")
    }
    user, ok := v.(model.User)
    if ok {
        return user.Id, nil
    }
    return 0,errors.New("can't convert to user struct")
}

4.2 Using JWT Middleware

The following code has two uses of JWT Middleware

  • Interna.MwUserAdmin Manages Background User Middleware
  • internal.MwUserCommenter Comment User Middleware

https://github.com/libragen/felix/blob/master/ssh2ws/ssh2ws.go

package ssh2ws

import (
    "time"

    "github.com/libragen/felix/felixbin"
    "github.com/libragen/felix/model"
    "github.com/libragen/felix/ssh2ws/internal"
    "github.com/libragen/felix/wslog"
    "github.com/gin-contrib/cors"
    "github.com/gin-gonic/gin"
)

func RunSsh2ws(bindAddress, user, password, secret string, expire time.Duration, verbose bool) error {
    err := model.CreateGodUser(user, password)
    if err != nil {
        return err
    }
    //config jwt variables
    model.AppSecret = secret
    model.ExpireTime = expire
    model.AppIss = "felix.mojotv.cn"
    if !verbose {
        gin.SetMode(gin.ReleaseMode)
    }
    r := gin.New()
    r.MaxMultipartMemory = 32 << 20
    //sever static file in http's root path
    binStaticMiddleware, err := felixbin.NewGinStaticBinMiddleware("/")
    if err != nil {
        return err
    }

    mwCORS := cors.New(cors.Config{
        AllowOrigins:     []string{"*"},
        AllowMethods:     []string{"PUT", "PATCH", "POST", "GET", "DELETE"},
        AllowHeaders:     []string{"Origin", "Authorization", "Content-Type"},
        ExposeHeaders:    []string{"Content-Type"},
        AllowCredentials: true,
        AllowOriginFunc: func(origin string) bool {
            return true
        },
        MaxAge: 2400 * time.Hour,
    })
    r.Use(binStaticMiddleware, mwCORS)


    {
        r.POST("comment-login", internal.LoginCommenter)       //Comment User Logon
        r.POST("comment-register", internal.RegisterCommenter) //Comment on user registration
    }

    api := r.Group("api")
    api.POST("admin-login", internal.LoginAdmin) //Manage Background Logon
    api.GET("meta", internal.Meta)

    //terminal log
    hub := wslog.NewHub()
    go hub.Run()

    {
        //websocket
        r.GET("ws/hook", internal.MwUserAdmin, internal.Wslog(hub))
        r.GET("ws/ssh/:id", internal.MwUserAdmin, internal.WsSsh)
    }
    //Call Externally
    {
        api.POST("wslog/hook-api", internal.JwtMiddlewareWslog, internal.WsLogHookApi(hub))
        api.GET("wslog/hook", internal.MwUserAdmin, internal.WslogHookAll)
        api.POST("wslog/hook", internal.MwUserAdmin, internal.WslogHookCreate)
        api.PATCH("wslog/hook", internal.MwUserAdmin, internal.WslogHookUpdate)
        api.DELETE("wslog/hook/:id", internal.MwUserAdmin, internal.WslogHookDelete)

        api.GET("wslog/msg", internal.MwUserAdmin, internal.WslogMsgAll)
        api.POST("wslog/msg-rm", internal.MwUserAdmin, internal.WslogMsgDelete)
    }

    //comment
    {
        api.GET("comment", internal.CommentAll)
        api.GET("comment/:id/:action", internal.MwUserComment, internal.CommentAction)
        api.POST("comment", internal.MwUserComment, internal.CommentCreate)
        api.DELETE("comment/:id", internal.MwUserAdmin, internal.CommentDelete)
    }
    {
        api.GET("hacknews",internal.MwUserAdmin, internal.HackNewAll)
        api.PATCH("hacknews", internal.HackNewUpdate)
        api.POST("hacknews-rm", internal.HackNewRm)
    }

    authG := api.Use(internal.MwUserAdmin)
    {

        //create wslog hook

        authG.GET("ssh", internal.SshAll)
        authG.POST("ssh", internal.SshCreate)
        authG.GET("ssh/:id", internal.SshOne)
        authG.PATCH("ssh", internal.SshUpdate)
        authG.DELETE("ssh/:id", internal.SshDelete)

        authG.GET("sftp/:id", internal.SftpLs)
        authG.GET("sftp/:id/dl", internal.SftpDl)
        authG.GET("sftp/:id/cat", internal.SftpCat)
        authG.GET("sftp/:id/rm", internal.SftpRm)
        authG.GET("sftp/:id/rename", internal.SftpRename)
        authG.GET("sftp/:id/mkdir", internal.SftpMkdir)
        authG.POST("sftp/:id/up", internal.SftpUp)

        authG.POST("ginbro/gen", internal.GinbroGen)
        authG.POST("ginbro/db", internal.GinbroDb)
        authG.GET("ginbro/dl", internal.GinbroDownload)

        authG.GET("ssh-log", internal.SshLogAll)
        authG.DELETE("ssh-log/:id", internal.SshLogDelete)
        authG.PATCH("ssh-log", internal.SshLogUpdate)

        authG.GET("user", internal.UserAll)
        authG.POST("user", internal.RegisterCommenter)
        //api.GET("user/:id", internal.SshAll)
        authG.DELETE("user/:id", internal.UserDelete)
        authG.PATCH("user", internal.UserUpdate)

    }

    if err := r.Run(bindAddress); err != nil {
        return err
    }
    return nil
}

5. Cookie-Session VS JWT

JWT differs from session in that sessions need to be generated on the server side, the server saves the session and returns it only to the client session ID. The client can bring the session ID with it the next time it requests, because sessions are stored on the server. There will be some troubles when there are multiple servers, and it is necessary to synchronize the information of multiple hosts, otherwise it will appear in the invitationRequest A server to obtain information, but request B server identity information can not be passed, JWT can solve this problem well. The server side does not need to save jwt, just need to save the secret used for encryption, generate and send JWT encryption to the client when the user logs on, store by the client, and then the client's request is taken to the server for solution.Analyze the JWT and verify that the server does not waste space to store login information and time to synchronize.

5.1 What is a cookie

cookie-based authentication is stateful, which means that the authenticated record or session must be saved on both the server and the client, and the server needs to keep track of the record sessions and store them in the database.
At the same time, the front end needs to save a session ID in the cookie as the unique identifier of the session, which can be regarded as the session's "identity card".

Cookies, in short, store some historical information about user actions (including login information, of course) on the client (browser, etc.) and then, when the user visits the site again, the browser sends the local cookie content to the server via the HTTP protocol to complete the validation or continue with the previous step.

5.2 What is session

Session, session, in short, is to store historical information about user actions on the server. After the user logs on, the server stores information about user sessions and assigns an access credential to the client. If a client makes a request with this credential, the user-related login information is taken out of the information stored on the server.
And the credentials returned by the server are often stored in cookies, or you can override the URL to put the id in the url, which is generally the SessionID.

5.3 Process of cookie-session authentication mechanism

Sessions and cookie s are designed to overcome the stateless flaw of the http protocol, but they are done differently.
Sessions can be accomplished through cookies, which save session IDs on the client side and other session messages from users in the session object on the server side. In contrast, cookies need to save all information on the client side.
Therefore, cookies have certain security risks, such as the password of the user name stored in the local cookie is deciphered, or the cookie is collected by other websites (e.g., 1. appA actively sets the domain B cookie to get the domain B cookie; 2. XSS, obtains the document.cookie through javascript on the appA and passes it to its own appB).

  1. User Enter Logon Information
  2. The server verifies that the login information is correct, creates a session if correct, and stores the session in the database
  3. Server-side returns cookie s with sessionID to client
  4. In the next request, the server will match the sessionID to the database and process the request if it is valid
  5. If the user logs out of the app,session will be destroyed on both client and server sides

5.4 Cookie-session and JWT usage scenarios

Backend rendering of HTML pages using Cookie-session authentication is recommended

It is very convenient to write/clear cookie s to the browser by pressing the rendering page after, and the permission control is very convenient. It is rarely necessary to consider cross-domain AJAX authentication.

App,web single page application, APIs recommend JWT authentication

With the rise of App and web APIs, token-based authentication became popular.
When we talk about using token for authentication, we generally mean using JSON Web Tokens (JWTs) for authentication, although there are different ways to implement token.
In fact, JWTs have become standard, so token and JWTs will be exchanged in this article.

This is the whole content of this article. I hope the content of this article will help you in your study or work. If you have any questions, you can leave a message to communicate. Thank you for your support for mojotv.cn. I like this website to add troublesome help to my favorites and add my WeChat friends: felixarebest Weibo account: MojoTech asked me a question.

Original address: Go Advanced 24:Go-jwt RESTful Authentication Tutorial

Tags: Go Session github ssh Database

Posted on Sun, 08 Sep 2019 20:22:08 -0700 by daniel1988