Authenticate user with JWT in GoLang

  https://ToniNichev@github.com/ToniNichev/tutorials-golang-authanticate-with-jwt.git

 

Generating JWT for testing

Sign, Verify and decode JWT

Setting up the project

we are going to use Gin Web framework to create simple HTTP server that we could query against, passing JWT in the header and then using secret or public key to validate the signature.

package main

import (
    "github.com/gin-gonic/gin"
)

func AuthMiddleware() gin.HandlerFunc {
    // In a real-world application, you would perform proper authentication here.
    // For the sake of this example, we'll just check if an API key is present.
    return func(c *gin.Context) {
        apiKey := c.GetHeader("X-Auth-Token")
        if apiKey == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
            return
        }
        c.Next()
    }
}

func main() {
    // Create a new Gin router
    router := gin.Default()

    // Public routes (no authentication required)
    public := router.Group("/public")
    {
        public.GET("/info", func(c *gin.Context) {
            c.String(200, "Public information")
        })
        public.GET("/products", func(c *gin.Context) {
            c.String(200, "Public product list")
        })
    }

    // Private routes (require authentication)
    private := router.Group("/private")
    private.Use(AuthMiddleware())
    {
        private.GET("/data", func(c *gin.Context) {
            c.String(200, "Private data accessible after authentication")
        })
        private.POST("/create", func(c *gin.Context) {
            c.String(200, "Create a new resource")
        })
    }

    router.POST("query", AuthMiddleware(), validateSession, returnData)

    // Run the server on port 8080
    router.Run(":8080")
}

We added validateSession middleware that will decode the token and verify the signature.

 

Creating JWT services to decode and validate signature

We are using jwt GoLang library to decode the token, and validate the signature.

There are two ways to encode JWT: using symmetric encryption (meaning that the same secret is used to sign and validate the signature. This is done in retreiveTokenWithSymmetrikKey and retreiveTokenWithAsymmetrikKey is used to validate signature using the public key from the private/public key pair used to sign the token.

 

package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

type User struct {
    name  string
    email string
}

var user User

type requestBody struct {
    OperationName *string     `json:"operationName"`
    Query         *string     `json:"query"`
    Variables     interface{} `json:"variables"`
}

func validateSession(c *gin.Context) {
    user.name = ""
    user.email = ""
    if c.Request.Body != nil {
        bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
        c.Request.Body.Close()
        c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

        body := requestBody{}
        if err := json.Unmarshal(bodyBytes, &body); err != nil {
            return
        }

        // extract the token from the headers
        tokenStr := c.Request.Header.Get("X-Auth-Token")

        product := body.Variables.(map[string]interface{})["product"]

        var payload string
        var err error
        if product == "web" {
            payload, err = retreiveTokenWithSymmetrikKey(c, tokenStr)
        } else {
            payload, err = retreiveTokenWithAsymmetrikKey(c, tokenStr)
        }

        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "Session token signature can't be confirmed!"})
        }

        if payload == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token"})
            return
        }
        c.Next()
    }

}

func retreiveTokenWithSymmetrikKey(c *gin.Context, tokenStr string) (string, error) {
    fmt.Println("retreive Token With Symmetric Key ...")

    tknStr := c.Request.Header.Get("X-Auth-Token")
    secretKey := "itsasecret123"

    token, err := jwt.Parse(tknStr, func(token *jwt.Token) (interface{}, error) {
        return []byte(secretKey), nil
    })

    if err != nil {
        c.AbortWithStatusJSON(401, gin.H{"error": "Session token signature can't be confirmed!"})
        return "", errors.New("session token signature can't be confirmed!")
    } else {
        claims := token.Claims.(jwt.MapClaims)
        fmt.Println("======================================")
        fmt.Println(claims)
        fmt.Println(claims["author"])
        fmt.Println(claims["data"])
        fmt.Println("======================================")
        user.name = claims["author"].(string)
    }
    return "token valid", nil
}

func retreiveTokenWithAsymmetrikKey(c *gin.Context, tokenStr string) (string, error) {

    fmt.Println("retreive Token With Asymmetric Key ...")

    publicKeyPath := "key/public_key.pem"
    keyData, err := ioutil.ReadFile(publicKeyPath)
    if err != nil {
        c.AbortWithStatusJSON(401, gin.H{"error": "Error reading public key"})
        return "", errors.New("error reading public key")
    }

    var parsedToken *jwt.Token

    // parse token
    state, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {

        // ensure signing method is correct
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            c.AbortWithStatusJSON(401, gin.H{"error": "Session token signature can't be confirmed!"})
            return nil, errors.New("unknown signing method")
        }

        parsedToken = token

        // verify
        key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(keyData))
        if err != nil {
            return nil, errors.New("parsing key failed")
        }

        return key, nil
    })

    claims := state.Claims.(jwt.MapClaims)

    fmt.Println("======================================")
    fmt.Println("Header [alg]:", parsedToken.Header["alg"])
    fmt.Println("Header [expiresIn]:", parsedToken.Header["expiresIn"])
    fmt.Println("Claims [author]:", claims["author"])
    fmt.Println("Claims [data]:", claims["data"])
    fmt.Println("======================================")
    user.name = claims["author"].(string)

    if !state.Valid {
        return "", errors.New("verification failed")
    }

    if err != nil {
        return "", errors.New("unknown signing error")
    }

    return "token valid", nil
}

func returnData(c *gin.Context) {
    fmt.Println("Returning data ...")
    c.String(200, user.name)
}

 

 

Making requests with JWT

We are going to use Postman to make a new POST request passing the JWT

Generate JWT

Open the second project: Sign, Verify and decode JWT

Make sure that you comment and uncomment the right type of token that you want to use: asymmetric vs symmetric.
Run the project yarn start end copy the long string printed right after SIGNED JWT.

Create new postman POST request

Open Postman and create new POST request. In the url put http://localhost:8080/query this is where our Gin Web server running.

Add X-Auth-Token JWT

Open header section, and add X-Auth-Token key with the value the JWT copied from Sign, Verify and decode JWT

 

Add query parameters and variables.

We are going to pass dummy parameters just for testing except for product
We are going to use product parameter to distinguish between symmetric and asymmetric tokens.
Let’s assume that our app will except symmetric tokens for web and asymmetric for app so make sure that you will pass the right JWT.
Navigate to the GraphQL section of the request, and add the query and the variables.

query

query GetCustomerReccomendations($customerId: String!, $organization: organization!, $product: String!) {
    getCustomer(customerId: $customerId) {
        customerId
        zipCode
        }
    }
}

variables

{
    "customerId": "2b59f049-04d1-43d5-ac87-8ac62069d932",
    "organization": "nbcnews",
    "product": "app"
}

 

Make the request and check the response

If everything works good, you will see the user name printed in the response.

Leave a Reply