Lesson 22 : How to Create and Verify JWT & PASETO Tokens in Golang

In the last lecture, we learned about token-based authentication and why PASETO is more secure and easier to use than JWT.
In this tutorial, we’ll implement both JWT and PASETO in Go and compare their simplicity

We’ll build a flexible token management system that showcases why PASETO is simpler and more secure than JWT, while maintaining compatibility through a common interface.

Table of Contents

1. Project Structure for Authentication

The core logic for token generation and verification is located in the token package. Let create a new package or folder token. The api package uses this logic to protect endpoints, and the util package manages the necessary configuration.

token/
├── maker.go           # Token maker interface
├── payload.go         # Token payload structure
├── jwt_maker.go       # JWT implementation
├── jwt_maker_test.go  # JWT tests
├── paseto_maker.go    # PASETO implementation
└── paseto_maker_test.go # PASETO tests
  • token/: Contains the interface and implementations for token makers.
    • maker.go: Defines the Maker interface.
    • paseto_maker.go: Implements the Maker interface using PASETO.
  • api/: The API layer.
    • server.go: Initializes the server and token maker.
    • middleware.go: Contains the authentication middleware that protects routes.
  • util/: Utility functions.
    • conif.go: Loads application configuration, including the token symmetric key.
  • app.env: The configuration file where the symmetric key is stored.

Step 1. Create token Maker Interface

Let create maker.go inside this package, idea is to create general token maker interface to manage the creation and verificaiton of the token. To support different token technologies and allow for easy switching, we use the Maker interface (token/maker.go). This defines a standard set of behaviors for any token implementation.

General token maker interface

This abstraction means we can swap out PASETO for JWT (or any other token tech) with minimal changes to the rest of the application, simply by creating a new type that satisfies the Maker interface. Flexibility: Switch between JWT and PASETO implementations seamlessly.

  1. The CreateToken method creates a new token for a specific username and valide duration, it returns a signed token string or an error. Basically, this method will create and sign a new token for a specific username and valid duration.
  2. The second method is VerifyToken, Which takes a token string as input, and returns a Payload object or an error. Role of this method is to check if the input token is valid or not. If it is valid, the method will return the payload data stored inside the body of the token.

Step 2: The Token Payload (payload.go)

The payload contains the data stored inside the token. It must implement the jwt.Claims interface for JWT compatibility

package token

import (
    "errors"
    "time"
    
    "github.com/google/uuid"
)

// Different types of error returned by the VerifyToken function
var (
    ErrInvalidToken = errors.New("token is invalid")
    ErrExpiredToken = errors.New("token has expired")
)

// Payload contains the payload data of the token
type Payload struct {
    ID        uuid.UUID `json:"id"`
    Username  string    `json:"username"`
    IssuedAt  time.Time `json:"issued_at"`
    ExpiredAt time.Time `json:"expired_at"`
}

// NewPayload creates a new token payload with a specific username and duration
func NewPayload(username string, duration time.Duration) (*Payload, error) {
    tokenID, err := uuid.NewRandom()
    if err != nil {
        return nil, err
    }

    payload := &Payload{
        ID:        tokenID,
        Username:  username,
        IssuedAt:  time.Now(),
        ExpiredAt: time.Now().Add(duration),
    }
    
    return payload, nil
}

The most important field is username, which is used to identify the token owner. Then an issued at field to know when the token is created. When using token based authentication, it’s crucial to make sure that each access token only has a short valid duration. We need to have a mechanism to invalidate some specific tokens in case they are leaked, So we need to add an ID field to uniquely identify each token

Payload Design ConsiderationsUUID for Token ID: Enables token revocation and tracking Timestamp Fields: Critical for security and debugging Validation Method: Required by JWT library and useful for PASETO.

This Go function, NewPayload, creates a new token payload with a unique ID, specified username, and expiration time based on the current time plus a given duration. It returns the payload and an error if one occurs during ID generation.

Now we’re gonna implement a JWT maker. We will need a JWT package for golang, So let’s open the browser and search for jwt golang. We also need other pacakges, we needs these packages.

Install Required Dependencies

# JWT library
go get github.com/dgrijalva/jwt-go

# PASETO library  
go get github.com/o1egl/paseto

# UUID library
go get github.com/google/uuid

# Clean up dependencies
go mod tidy

Step 3: Implement JWT Token Maker

In this tutorial, we will use symmetric key algorithm to sign the tokens, so this struct will have a field to store the secret key. This struct is a JSON web token maker, which implements the token maker interface.

File: token/jwt_maker.go This implementation uses the symmetric-key algorithm HS256. We added a function NewJWTMaker that takes a secret key string as input and returns a Maker interface or an error.
By returning the interface, we ensure that JWTMaker implements the token maker interface. Otherwise, the compiler will show a red underline at the return statement.
 To implment teh JWTMaker interface we need to add both methods from JWTMAker interface, CreateToken and VerifyToken methods.

package token

import ("errors" "fmt""time" "github.com/golang-jwt/jwt/v5")

const minSecretKeySize = 32

// JWTMaker is a JSON Web Token maker
type JWTMaker struct {
	secretKey string
}

// NewJWTMaker creates a new JWTMaker
func NewJWTMaker(secretKey string) (Maker, error) {
	if len(secretKey) < minSecretKeySize {
		return nil, fmt.Errorf("invalid key size: must be at least %d characters", minSecretKeySize)
	}
	return &JWTMaker{secretKey}, nil
}

// CreateToken creates a new token for a specific username & duration
func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, error) {
	payload, err := NewPayload(username, duration)
	if err != nil {
		return "", err
	}

	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
	return jwtToken.SignedString([]byte(maker.secretKey))
}

// VerifyToken checks if the token is valid or not
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
	keyFunc := func(token *jwt.Token) (interface{}, error) {
           // ✅ CRITICAL: Verify algorithm matches expected
		_, ok := token.Method.(*jwt.SigningMethodHMAC)
		if !ok {
			return nil, ErrInvalidToken
		}
		return []byte(maker.secretKey), nil
	}

	jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
	if err != nil {
               // Check if the error is a validation error		if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
			return nil, ErrExpiredToken
		}
		return nil, ErrInvalidToken
	}

//it checks token's claims (payload) of type Payload, assigns it to the payload variable if it is. 
	payload, ok := jwtToken.Claims.(*Payload)
	if !ok {
		return nil, ErrInvalidToken
	}

	return payload, nil
}

The createToken function generates a new JSON Web Token (JWT) for a given username and duration. It creates a token payload with the provided information, signs it with a secret key using the HS256 algorithm, and returns the signed token as a string. If an error occurs during payload creation, it returns an empty string and the error. This function is a method of the JWTMaker struct, which suggests it’s part of a token creation system.

In createToken at the end, to generate a token string, we call jwtToken.SignedString(), and pass in the secret key after converting it to byte slice. Here we have an error because our Payload struct doesn’t implement the jwt.Claims interface. By hovering on this method we can see the error, because our payload struct doesn’t implement the jwt.Claim Interface. It’s missing one method called Valid().The jwt-go package needs this method to check if the token payload is valid or not.

So let’s open the token/payload.go to add this method.

// existing code
...
var (
	ErrInvalidToken = errors.New("token is invalid")
	ErrExpiredToken = errors.New("token has expired")
)
func (payload Payload) Valid() error {
	if time.Now().After(payload.ExpiredAt) {
		return ErrExpiredToken
	}
	return nil
}

The VerifyToken, checks the validity of a JSON Web Token (JWT). It does the following:

  1. Checks the signing method: Ensures the token was signed with HMAC (Hash-based Message Authentication Code).
  2. Parses the token: Uses the provided secret key to verify the token’s signature.
  3. Checks for expiration: If the token is expired or not yet valid, returns an ErrExpiredToken error.
  4. Verifies the payload: Ensures the token’s payload is of type *Payload.
  5. Returns the payload or an error: If all checks pass, returns the payload; otherwise, returns an error.

JWT Implementation Details

Security Checks:

  • Minimum key size enforcement
  • Algorithm verification in keyFunc
  • Proper error handling and differentiation

Step 4: Create JWT token maker unit Tests

Now, let’s create a new file jwt_maker_test.go inside the token package. We need to tests in jwt_maker_test.go to verify that our JWTMaker works correctly. These tests cover valid tokensexpired tokens, and invalid tokens.

package token

import ("testing" "time" "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require" "github.com/ngodup/simplebank/util"
)

func TestJWTMaker(t *testing.T) {
	maker, err := NewJWTMaker(util.RandomString(32))
	require.NoError(t, err)

	username := util.RandomOwner()
	duration := time.Minute

	issuedAt := time.Now()
	expiredAt := issuedAt.Add(duration)

	token, err := maker.CreateToken(username, duration)
	require.NoError(t, err)
	require.NotEmpty(t, token)

	payload, err := maker.VerifyToken(token)
	require.NoError(t, err)
	require.NotEmpty(t, payload)

	require.NotZero(t, payload.ID)
	require.Equal(t, username, payload.Username)
	require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
	require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}


func TestExpiredJWTToken(t *testing.T) {
	maker, err := NewJWTMaker(util.RandomString(32))
	require.NoError(t, err)

	token, err := maker.CreateToken(util.RandomOwner(), -time.Minute)
	require.NoError(t, err)
	require.NotEmpty(t, token)

	payload, err := maker.VerifyToken(token)
	require.Error(t, err)
	require.EqualError(t, err, ErrExpiredToken.Error())
	require.Nil(t, payload)
}

func TestInvalidJWTTokenAlgNone(t *testing.T) {
	payload, err := NewPayload(util.RandomOwner(), time.Minute)
	require.NoError(t, err)

	jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload)
	token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
	require.NoError(t, err)

	maker, err := NewJWTMaker(util.RandomString(32))
	require.NoError(t, err)

	payload, err = maker.VerifyToken(token)
	require.Error(t, err)
	require.EqualError(t, err, ErrInvalidToken.Error())
	require.Nil(t, payload)
}

The function TestJWTMaker happy case,

  • Generate a new JWTMaker with a random 32-character key.
  • Create a token for a random user with a 1-minute duration.
  • Verify the token and extract its payload.

Assertions:

  • Token should not be empty.
  • Payload should not be nil.
  • payload.ID must exist.
  • payload.Username matches the input.
  • IssuedAt and ExpiredAt are within 1 second of expected times.

✅ This ensures tokens are created and verified correctly.

The TestExpiredJWTToken function

  • Create a token with negative duration → immediately expired.
  • Verify the token.

Assertions:

  • Token is generated successfully.
  • VerifyToken should return ErrExpiredToken.
  • Payload must be nil.

The func TestInvalidJWTTokenAlgNone

  • Manually create a token with alg: none (no signing).
  • Sign it with jwt.UnsafeAllowNoneSignatureType (only allowed in tests).
  • Verify the token with our JWTMaker.
  • Assertions:
    • Verification should fail with ErrInvalidToken.
    • Payload must be nil.

✅ This prevents the “alg: none” vulnerability that attackers exploit.

The func TestInvalidJWTTokenAlgNone: jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
require.NoError(t, err)
Normally, when we create a JWT, it must be signed with a secret (HS256) or a private key (RS256, etc.) so the server can verify that the token is authentic.

But JWT also defines an algorithm called none, which means “no signing at all.”

  • That’s very dangerous in production, because an attacker could just create their own fake tokens with no signature.
  • To prevent this, the jwt-go library blocks you from creating none-signed tokens by default.

However, for testing, we sometimes want to create such a token to simulate an attack.
That’s why jwt-go provides this special constant:

jwt.UnsafeAllowNoneSignatureType

If you pass this constant into SignedString(), the library will allow you to generate a none-signed token — but only because you are explicitly saying “Yes, I know this is unsafe, and I’m doing it on purpose for testing.”

⚠️ Important: This should never be used in production. It’s only for writing unit tests to confirm that your code correctly rejects invalid tokens with alg: none.


Step 5. PASETO Implementation: PasetoMaker

Now we are going to implement the same token maker interface but using PASETO instead. The PASETO are much easier and cleaner than JWT.

Lets create The PasetoMaker (token/paseto_maker.go) provides the concrete implementation of the Maker interface using PASETO v2 symmetric key encryption.

package token

import ( "fmt" "time" "github.com/o1egl/paseto" )

// PasetoMaker is a PASETO token maker
type PasetoMaker struct {
    paseto       *paseto.V2
    symmetricKey []byte
}

// NewPasetoMaker creates a new PasetoMaker
func NewPasetoMaker(symmetricKey string) (Maker, error) {
    if len(symmetricKey) != 32 {
        return nil, fmt.Errorf("invalid key size: must be exactly 32 characters")
    }

    maker := &PasetoMaker{
        paseto:       paseto.NewV2(),
        symmetricKey: []byte(symmetricKey),
    }

    return maker, nil
}

// CreateToken creates a new token for a specific username and duration
func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {
   ... We had implemented below

// VerifyToken checks if the token is valid or not
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {

Simplicity: Much shorter and cleaner than JWT implementation Security: No algorithm confusion possible – algorithm is fixed per version Key Requirements: ChaCha20-Poly1305 requires exactly 32-byte keys.

Comparison with JWT:

  • JWT VerifyToken: ~30 lines with complex error handling
  • PASETO VerifyToken: ~10 lines with simple logic

Key Advantages of the PASETO Implementation:

  • Simplicity: The Encrypt and Decrypt methods handle everything—signing, verification, and encryption—in a single call.
  • Security: The algorithm is fixed (v2.local), eliminating the risk of algorithm confusion attacks. The payload is encrypted, not just encoded.
  • No Complex Error Handling: Unlike JWT, we don’t need to parse special error types to check for expiration.

1. Initialization

The NewPasetoMaker function creates a new PasetoMaker. It requires a symmetric key of a specific length (chacha20poly1305.KeySize, which is 32 characters) to ensure strong encryption.

2. Token Creation

The CreateToken method generates a new PASETO token. It first creates a new payload containing the username and expiration, then uses the paseto.V2.Encrypt method to create a secure, encrypted token.

func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, error) {
    payload, err := NewPayload(username, duration)
    if err != nil {
        return "", err
    }

    return maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
}

Unlike JWT, the payload here is fully encrypted, not just Base64 encoded.

3. Token Verification

The VerifyToken method decrypts and verifies a token. It uses paseto.V2.Decrypt, which handles both decryption and integrity checking. If the token is malformed, has an invalid signature, or is otherwise tampered with, the function will return an error. After successful decryption, it checks if the token has expired.

func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
    payload := &Payload{}

    err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil)
    if err != nil {
        return nil, ErrInvalidToken
    }

    err = payload.Valid()
    if err != nil {
        return nil, err
    }

    return payload, nil
}

Step 6: Create JWT Unit Tests

Create a file token/jwt_maker_test.go:

package token

import ( "testing" "time" "github.com/ngodup/simplebank/util"
	"github.com/stretchr/testify/require")

func TestPasetoMaker(t *testing.T) {
	maker, err := NewPasetoMaker(util.RandomString(32))
	require.NoError(t, err)

	username := util.RandomOwner()
	duration := time.Minute

	issuedAt := time.Now()
	expiredAt := issuedAt.Add(duration)

	token, err := maker.CreateToken(username, duration)
	require.NoError(t, err)
	require.NotEmpty(t, token)

	payload, err := maker.VerifyToken(token)
	require.NoError(t, err)
	require.NotEmpty(t, payload)

	require.NotZero(t, payload.ID)
	require.Equal(t, username, payload.Username)
	require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
	require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
}

func TestExpiredPasetoToken(t *testing.T) {
	maker, err := NewPasetoMaker(util.RandomString(32))
	require.NoError(t, err)

	token, err := maker.CreateToken(util.RandomOwner(), -time.Minute)
	require.NoError(t, err)
	require.NotEmpty(t, token)

	payload, err := maker.VerifyToken(token)
	require.Error(t, err)
	require.EqualError(t, err, ErrExpiredToken.Error())
	require.Nil(t, payload)
}

The TestPasetoMaker function: tests the PasetoMaker struct. It creates a new PasetoMaker instance, generates a token for a random username with a 1-minute duration, verifies the token, and checks that the token’s payload matches the expected values.

The TestExpiredPasetoToken: This is a Go test function that checks the behavior of a PASETO token maker when creating and verifying an expired token.

Here’s a succinct breakdown:

  1. Create a new PASETO token maker with a random key.
  2. Create a token with a negative duration (-time.Minute), which means it’s already expired.
  3. Verify the token using the maker.
  4. Expect the verification to return an error (ErrExpiredToken) and a nil payload.

In summary, this test ensures that the PASETO token maker correctly identifies and rejects expired tokens.


Conclusion and Key Takeaways

  • Interchangeability: By using the Maker interface, you can easily swap between JWT and PASETO in your application (e.g., in your main.go or config) without changing the core logic that uses tokens.
  • Simplicity: The PASETO implementation is dramatically simpler and less error-prone than JWT. It handles security concerns internally.
  • Security: PASETO v2.local uses modern cryptography (XChaCha20-Poly1305) and encrypts the payload, providing confidentiality and integrity. It is immune to the algorithm confusion attack that JWT is susceptible to.
  • Recommendation: For new projects, prefer PASETO. It provides a safer and cleaner developer experience. Use this tutorial as a foundation to integrate token-based authentication into your Go APIs.
Scroll to Top