Tag Archives: jwt

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.

Sign, Verify and decode JWT

  https://github.com/ToniNichev/tutorials-encodeDecodeJWT

Json Web Token become widely popular for creating data with optional signature and/or optional encryption and payload.

JWTs are a Base64 encoded string with a signature attached to it. JWT components are separated by . The components are:

  • Header: Contains metadata about the token, such as the signing algorithm used.
  • Payload: Contains the claims, which are statements about the subject of the token. For example, a JWT might contain claims about a user’s identity, such as their username and email address, or their authorization to access certain resources.
  • Signature: A digital signature that ensures the integrity of the header and payload. The signature is created using the header and payload and a secret key known only to the issuer.

Example token:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3IiOiJUb25pIFkgTmljaGV2IiwiaWF0IjoxNzA2MTEzNDc0LCJkYXRhIjoiTmV3IEpXVCBnZW5lcmF0ZWQgYXQgV2VkIEphbiAyNCAyMDI0IDExOjI0OjM0IEdNVC0wNTAwIChFYXN0ZXJuIFN0YW5kYXJkIFRpbWUpIiwiZXhwIjoxNzA2MTU2Njc0LCJhdWQiOiJodHRwczovL215c29mdHdhcmUtY29ycC5jb20iLCJpc3MiOiJUb25pIE5pY2hldiIsInN1YiI6InRvbmkubmljaGV2QGdtYWlsLmNvbSJ9.YVDqPvei911_KpPjywiZzzK4vNZAm0wiFC0jMV3qI8eUIuPsJC48GkhjNQFgG3GIqHvkwuWmmZEmpD6UrrxENtw9M8h-iLG9syWMJh1HqsyfpKzdATr3PY7fGE1W9If9v0ULWT7ogO_dMuquEf1vi1PcdW-YjrMqZtSnPbIrgaHogeFd3Hix2Bdmlf8v2TX9CWZHJYbgcTj9xDKFw92GkPgeuqYZ2I0C_2VbsWAjLWmdG5iOQakY7XS2I39qCCd87JLsxXHTfmK4mpMBIUgyOaBIy-o7kfQ1hU5wb-DA0H-GtG-WAgyfpIfw0kgULxV-paVVXQLurv78Lm7x6k5B1g

Let’s do base64 decode on each part of the token above:

Header

echo 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' |base64 -d
{"alg":"RS256","typ":"JWT"}%
  • “alg”: “RS256” – the encrypting algorithm
  • “typ”: “JWT” – type of the token

Payload

echo 'eyJhdXRob3IiOiJUb25pIFkgTmljaGV2IiwiaWF0IjoxNzA2MTEzNDc0LCJkYXRhIjoiTmV3IEpXVCBnZW5lcmF0ZWQgYXQgV2VkIEphbiAyNCAyMDI0IDExOjI0OjM0IEdNVC0wNTAwIChFYXN0ZXJuIFN0YW5kYXJkIFRpbWUpIiwiZXhwIjoxNzA2MTU2Njc0LCJhdWQiOiJodHRwczovL215c29mdHdhcmUtY29ycC5jb20iLCJpc3MiOiJUb25pIE5pY2hldiIsInN1YiI6InRvbmkubmljaGV2QGdtYWlsLmNvbSJ9' |base64 -d
{"author":"Toni Y Nichev","iat":1706113474,"data":"New JWT generated at Wed Jan 24 2024 11:24:34 GMT-0500 (Eastern Standard Time)","exp":1706156674,"aud":"https://mysoftware-corp.com","iss":"Toni Nichev","sub":"toni.nichev@gmail.com"}

Signature

echo 'YVDqPvei911_KpPjywiZzzK4vNZAm0wiFC0jMV3qI8eUIuPsJC48GkhjNQFgG3GIqHvkwuWmmZEmpD6UrrxENtw9M8h-iLG9syWMJh1HqsyfpKzdATr3PY7fGE1W9If9v0ULWT7ogO_dMuquEf1vi1PcdW-YjrMqZtSnPbIrgaHogeFd3Hix2Bdmlf8v2TX9CWZHJYbgcTj9xDKFw92GkPgeuqYZ2I0C_2VbsWAjLWmdG5iOQakY7XS2I39qCCd87JLsxXHTfmK4mpMBIUgyOaBIy-o7kfQ1hU5wb-DA0H-GtG-WAgyfpIfw0kgULxV-paVVXQLurv78Lm7x6k5B1g' |base64 -d
aP�>���]*����2���@�L"-#1]�#ǔ"��$.<Hc5`��{��妙�&�>���D6�=3�~����%�&G�̟���:�=��MV���E
                                                                                  Y>���2��o�S�uo���*fԧ=�+����]�x��f��/�5�       fG%��q8��2��݆����؍�e[�`#-i��A��t�#'|���q�~b���!H29�H��;��5�Npo�����o�
                                                                                                                                                                                                    �����H/~��U]���.n��NA%

Obviously there is no readable text here.

As we see JWT payload is not encrypted and could be decoded with any base64 decoder so never store sensitive data there. The purpose of signing with our private key is to make sure that ‘audience’ (who ever is going to use the token) will be able to verify the authenticity of this token with shared public key.

Claims to Verify

When code is presented with a JWT, it should verify certain claims. At a minimum, these claims should be checked out:

  • iss identifies the issuer of the JWT. (UUID, domain name, URL or something else)
  • aud identifies the audience of the token, that is, who should be consuming it. aud may be a scalar or an array value.
  • nbf and exp. These claims determine the timeframe for which the token is valid.

It doesn’t matter exactly what this strings are as long as the issuer and consumer of the JWT agree on the values.

JWT signing algorithms.

The default algorithm used is (HS256) which is symmetric: meaning that the same ‘secret’ is used for signing and verifying. In the example below  `itsasecret123`

Symmetric algorithm

jwt-services-symmetric.js
import jwt from "jsonwebtoken";
import fs from "fs";

const now = Math.round(new Date().getTime() / 1000);
const expirationTime = now + 500; // Set to 15 minutes (900 seconds)

const secret = 'itsasecret123';

const sign = async (signData, payload) => {
  // Create the JWT header and payload
  const header = {
    'alg': 'RS256',
    'typ': 'JWT'
  };


  const token = jwt.sign(payload, secret);
  return token;
}

const verify = async (token, signData) => {

  try {
    return jwt.verify(token, secret);
  } catch (err) {
    console.log("Error: ", err);
    return false;
  }

}

const decode = async (token) => {
  return jwt.decode(token, {complete: true});
}
export default {
  sign,
  verify,
  decode,
}

 

Asymmetric algorithm

With asymmetric algorithms like (RS256) we use private key to sign the token, and public key to verify the signature. 

How to create public/private key pair:

using opensssl:

  1. Generate the Key Pair:
    openssl genrsa -out private_key.pem 2048
  2. Extract the Public Key
    openssl rsa -in private_key.pem -pubout -out public_key.pem
  3. Secure the Private Key with passphrase (optional but highly recommended)
    openssl rsa -aes256 -in private_key.pem -out private_key_protected.pem

jwt-services-asymmetric.js

import jwt from "jsonwebtoken";
import fs from "fs";

const now = Math.round(new Date().getTime() / 1000);
const expirationTime = now + 500; // Set to 15 minutes (900 seconds)

const privateKey = fs.readFileSync('./keys/private_key.pem');
const publicKey = fs.readFileSync('./keys/public_key.pem');

const sign = async (signData, payload) => {
  // Create the JWT header and payload
  const header = {
    'alg': 'RS256',
    'typ': 'JWT'
  };


  // SIGNING OPTIONS
  const signOptions = {
    issuer: signData.issuer,
    subject: signData.subject,
    audience: signData.audience,
    expiresIn: signData.expiresIn,
    algorithm: signData.algorithm,
  };


  const token = jwt.sign(payload, privateKey, signOptions);
  return token;
}

const verify = async (token, signData) => {
  // VERIFY OPTIONS
  const verifyOptions = {
    issuer: signData.issuer,
    subject: signData.subject,
    audience: signData.audience,
    expiresIn: signData.expiresIn,
    algorithm: signData.algorithm,
  };

  try {
    return jwt.verify(token, publicKey, verifyOptions);
  } catch (err) {
    console.log("Error: ", err);
    return false;
  }

}

const decode = async (token) => {
  return jwt.decode(token, {complete: true});
}
export default {
  sign,
  verify,
  decode,
}

Calling the services. Uncomment jwt-services-symmetric .js and comment the other one if you want to test the symmetric JWT sign.

index.js

import jwt from "./jwt-services-asymmetric.js";
//import jwt from "./jwt-services-symmetric.js";

const now = Math.round(new Date().getTime() / 1000);
const secret = "12345";

const signData = {
    issuer: 'Toni Nichev',
    subject: 'toni.nichev@gmail.com',
    audience: 'https://mysoftware-corp.com',
    expiresIn: "12h",
    algorithm: "RS256"
}

const date = new Date();
let dateStr = date.toString();

const payload = {
    "author": "Toni Y Nichev",
    "iat": now,
    "data": `New JWT generated at ${dateStr}`,
};


const token = await jwt.sign(signData, payload);
console.log(`\n==================\nSIGN JWT\n==================\n ${token}`);



const v = await jwt.verify(token, signData);
console.log(`\n==================\nVERIFY SIGNATURE\n==================\n`, v);


const d = await jwt.decode(token);
console.log(`\n==================\nDECODE\n==================\n`, d);