DEV Community

Cover image for Implementing JWT Authentication in a Golang Application
Ege Aytin for Permify

Posted on • Originally published at permify.co

Implementing JWT Authentication in a Golang Application

In this tutorial, we'll explore the fundamentals of JWT authentication, understanding its significance, and then transition into a hands-on implementation.

Throughout this guide, we'll cover:

  • A brief overview of JWT and its structure
  • Creating a Simple ToDo Application with Go
  • Examining the Golang-JWT package
  • Creating JWT Tokens and Adding Claims using Golang-JWT
  • Signing and Verifying JWTs

You can find the implementation source code in the following GitHub repository

A quick note before we start: We leave out some aspects that an actual application might need in terms of session management. We aim to keep things simple and focus on providing a straightforward approach to help you get started with JWT authentication in your Golang applications.

What is JWT

JWTs, or JSON Web Tokens, serve as compact and self-contained data structures for transmitting information securely between parties. JWTs specify the token type, contain claims about an entity, and ensure integrity through cryptographic signatures.

Structure of JWT

JWTs consist of three parts: the header, the payload, and the signature.

  1. Header

The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

Example:

{
  "alg": "HS256",
  "typ": "JWT"
}

Enter fullscreen mode Exit fullscreen mode
  1. Payload

The payload contains claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims. Registered claims include standard fields like issuer (iss), subject (sub), audience (aud), expiration time (exp), and issued at (iat).

Example:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
Enter fullscreen mode Exit fullscreen mode
  1. Signature

The signature is created by combining the encoded header, encoded payload, a secret, and the specified signing algorithm. It ensures the integrity and authenticity of the token.

Example (using HMAC SHA256):

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
Enter fullscreen mode Exit fullscreen mode

Now, we're ready to implement JWT to enhance the security of our application.

Creating a Simple ToDo Application

For the purpose of demonstrating JWT authentication, we will create a simple ToDo list application with Role-Based Access Control (RBAC). We will introduce two distinct roles: senior and employee. These roles will serve as the basis for managing user access and permissions within our application.

  • Senior: Senior users have elevated privileges and can perform actions such as adding ToDo items to the list.
  • Employee: Employees have more restricted access and cannot add new ToDo items.

Roles are typically assigned during user authentication and embedded in the JWT claims.

Now, let's dive into the fun part – creating our basic ToDo application using the powerful Gin framework. This section will walk you through the steps, breaking down the code into manageable snippets.

Step 1: Project Setup

Before we start, ensure you have Go installed and then initialize a new Go module and fetch the Gin package:

go mod init go-to-do
go get -u github.com/gin-gonic/gin
Enter fullscreen mode Exit fullscreen mode

This sets up a new Go module and grabs the necessary Gin dependencies.

Step 2: Project Structure

Now, let's structure our project. Create a main Go file, main.go, and a templates folder to hold our HTML files:

go-to-do
   - main.go
   - templates
       - index.html
Enter fullscreen mode Exit fullscreen mode

In main.go, start by importing necessary packages and defining our Todo struct:

package main

import (
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
)

type Todo struct {
    Text string
    Done bool
}

var todos []Todo
var loggedInUser string
Enter fullscreen mode Exit fullscreen mode

Here, we set up our basic ToDo structure and create a global variable for storing ToDo items (todos) and the currently logged-in user (loggedInUser).

*Step 3: Initialize Gin Router and Define Main Route*

Now, initialize our Gin router, set up static file serving, and define the main route to render the ToDo list:

// Add routes for the ToDo App
func main() {
    router := gin.Default()

    router.Static("/static", "./static")
    router.LoadHTMLGlob("templates/*")

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{
            "Todos":    todos,
            "LoggedIn": loggedInUser != "",
            "Username": loggedInUser,
        })
    })

    router.POST("/add", func(c *gin.Context) {
        text := c.PostForm("todo")
        todo := Todo{Text: text, Done: false}
        todos = append(todos, todo)
        c.Redirect(http.StatusSeeOther, "/")
    })

    router.POST("/toggle", func(c *gin.Context) {
        index := c.PostForm("index")
        toggleIndex(index)
        c.Redirect(http.StatusSeeOther, "/")
    })

    router.Run(":8080")
}

func toggleIndex(index string) {
    i, _ := strconv.Atoi(index)
    if i >= 0 && i < len(todos) {
        todos[i].Done = !todos[i].Done
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we create a Gin router, configure it to serve static files and load HTML templates. The main route (/) renders the index.html template, passing along ToDo items, login status, and the username. Additionally, we handle the addition of new ToDo items and toggling their completion status.

*Step 4: Create HTML Template*

Create the index.html file in the templates folder:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/static/style.css">
    <title>ToDo List</title>
</head>
<body>
    <h1>Welcome, User</h1>

    <!-- Form for adding new ToDo items -->
    <form action="/add" method="post">
        <input type="text" name="todo" required>
        <button type="submit">Add ToDo</button>
    </form>

    <!-- ToDo list -->
    <ul>
        {{ range $index, $todo := .Todos }}
            <li>
                <form action="/toggle" method="post" style="display:inline;">
                    <input type="hidden" name="index" value="{{ $index }}">
                    <input type="checkbox" {{ if $todo.Done }}checked{{ end }} onchange="this.form.submit()">
                </form>
                {{ if $todo.Done }}
                    <del>{{ $todo.Text }}</del>
                {{ else }}
                    {{ $todo.Text }}
                {{ end }}
            </li>
        {{ end }}
    </ul>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This HTML template includes a form for adding new ToDo items and a list to display existing items. The template uses the Go template syntax to loop through the Todos slice and dynamically render each item.

That's it for now! You've set up the basic structure and added the functionality to display and add ToDo items. Run the application, and you should be able to see your ToDo list in action at http://localhost:8080.

simple todo

Now, with these additions, you have the basic functionality of adding new ToDo items and toggling their completion status. Your ToDo application is ready for further enhancements!

Getting Started With Golang-jwt

To add JWT authentication to our ToDo application, we'll be using the Golang-jwt library. The golang-jwt package simplifies the implementation of JWTs in Go applications, offering a suite of convenient functions that abstract away the complexities associated with token creation, verification, and management.

To incorporate Golang-jwt into our Go project, we can easily install it using the following command:

go get -u github.com/golang-jwt/jwt/v5
Enter fullscreen mode Exit fullscreen mode

This command fetches the necessary dependencies and makes Golang-jwt readily available for integration into our ToDo application. Now we are ready to dive into the creation of JWT tokens and the addition of claims to represent user information and roles.

Creating JWT Tokens and Adding Claims

Now, let's enhance our ToDo application by adding JWT authentication. In this section, we'll focus on creating JWT tokens and adding claims to represent user information and roles.

*Step 5: Set Up Secret Key*

To get started with JWT token creation, we'll first import the required packages and set up a secret key. The secret key is crucial for both signing and verifying JWTs. Here's the code:

// Import the required packages
import (
    "fmt"
    "time"
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)
// Add a new global variable for the secret key
var secretKey = []byte("your-secret-key")

// Function to create JWT tokens with claims
func createToken(username string) (string, error) {
    // ... (Code for creating JWT tokens and adding claims will go here)
}
Enter fullscreen mode Exit fullscreen mode

In this part, we import the necessary packages for JWT and Gin. We also create a global variable secretKey, which represents our secret cryptographic key used for token signing and verification.

*Step 6: Create Token with Claims*

Now, let's proceed with the code responsible for creating JWT tokens and adding claims:

// Function to create JWT tokens with claims
func createToken(username string) (string, error) {
    // Create a new JWT token with claims
    claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": username,                    // Subject (user identifier)
        "iss": "todo-app",                  // Issuer
        "aud": getRole(username),           // Audience (user role)
        "exp": time.Now().Add(time.Hour).Unix(), // Expiration time
        "iat": time.Now().Unix(),                 // Issued at
    })

  // Print information about the created token
    fmt.Printf("Token claims added: %+v\n", claims)
    return tokenString, nil
}
Enter fullscreen mode Exit fullscreen mode

Here, we've defined the createToken function. It takes a username as input and generates a JWT token with specific claims such as subject (sub), issuer (iss), audience (aud), expiration time (exp), and issued at (iat).

Now, let's add the getRole function, which helps determine the role of a user based on their username:

func getRole(username string) string {
    if username == "senior" {
        return "senior"
    }
    return "employee"
}
Enter fullscreen mode Exit fullscreen mode

This simple function checks if the username is senior and returns the role senior, otherwise, it defaults to employee. It will be used in the createToken function to set the audience claim of the JWT.

With these three parts, you've set up the foundation for creating JWT tokens with claims in your application. The createToken function can be utilized to generate tokens with specific user information.

Once tokens are created, the next step is to sign them when they are presented during authentication.

Signing JWT Tokens

To ensure the integrity and authenticity of the token, we need to sign it using our secret key. The signing process is handled internally by the jwt package. Here's the code:

// Sign the previously created token
func createToken(username string) (string, error) {
    // ... (previous code)

    tokenString, err := claims.SignedString(secretKey)
    if err != nil {
        return "", err
    }

    // ... (previous code)
}
Enter fullscreen mode Exit fullscreen mode

In this part, we've included the code to sign the JWT token with our secret key after adding claims. The signed token is then returned.

*Step 7: Update Login Route to Include Token Creation*

Now, let's modify the /login route in the main.go file to include token creation and sending:

// Create route for user authentication
router.POST("/login", func(c *gin.Context) {
        username := c.PostForm("username")
        password := c.PostForm("password")

        // Dummy credential check
        if (username == "employee" && password == "password") || (username == "senior" && password == "password") {
            tokenString, err := createToken(username)
            if err != nil {
                c.String(http.StatusInternalServerError, "Error creating token")
                return
            }

            loggedInUser = username
            fmt.Printf("Token created: %s\n", tokenString)
            c.SetCookie("token", tokenString, 3600, "/", "localhost", false, true)
            c.Redirect(http.StatusSeeOther, "/")
        } else {
            c.String(http.StatusUnauthorized, "Invalid credentials")
        }
})
Enter fullscreen mode Exit fullscreen mode

Here, we've updated the /login route to include the createToken function. After validating the username and password (in this case, using dummy credentials), if successful, we create a JWT token and set it as a cookie in the response.

*Step 8: Update HTML Template for Login Form*

Finally, update the login form in the index.html file to include the username and password fields:

{{ if not .LoggedIn }}
<h1>Login</h1>
<!-- Login form -->
<form action="/login" method="post">
    <input type="text" name="username" placeholder="Username" required>
    <input type="password" name="password" placeholder="Password" required>
    <button type="submit">Login</button>
</form>
{{ else }}
<!-- ... (previous code) -->
{{ end }}

Enter fullscreen mode Exit fullscreen mode

This modification includes the fields for entering a username and password in the login form. This information will be sent to the server for authentication.

token creation

In the next section, we'll explore token verification to ensure secure authentication.

Verifying JWT Tokens

In this section, we'll implement a middleware function to verify incoming JWT tokens. This verification step ensures the integrity and authenticity of the tokens before processing requests that require authentication.

Step 9: Add Middleware for Token Verification

Firstly, let's create a middleware function for verifying JWT tokens:

// Function to verify JWT tokens
func authenticateMiddleware(c *gin.Context) {
    // Retrieve the token from the cookie
    tokenString, err := c.Cookie("token")
    if err != nil {
        fmt.Println("Token missing in cookie")
        c.Redirect(http.StatusSeeOther, "/login")
        c.Abort()
        return
    }

    // Verify the token
    token, err := verifyToken(tokenString)
    if err != nil {
        fmt.Printf("Token verification failed: %v\\n", err)
        c.Redirect(http.StatusSeeOther, "/login")
        c.Abort()
        return
    }

    // Print information about the verified token
    fmt.Printf("Token verified successfully. Claims: %+v\\n", token.Claims)

    // Continue with the next middleware or route handler
    c.Next()
}
Enter fullscreen mode Exit fullscreen mode

Here, the authenticateMiddleware function retrieves the JWT token from the cookie, attempts to verify it using the verifyToken function, and redirects to the login page if the token is missing or verification fails.

If the token is successfully verified, information about the token's claims is printed, and the middleware allows the request to continue to the next handler.

Step 10: Add Middleware to Protected Routes

Next, let's apply the authenticateMiddleware to the routes that require authentication, such as adding and toggling ToDo items:

// Apply middleware to routes that require authentication
router.POST("/add", authenticateMiddleware, func(c *gin.Context) {
    // ... (previous code)
})

router.POST("/toggle", authenticateMiddleware, func(c *gin.Context) {
    // ... (previous code)
})
Enter fullscreen mode Exit fullscreen mode

By applying the middleware to these routes, we ensure that requests to add or toggle ToDo items will only be processed if the incoming token is successfully verified.

Step 11: Verify Token

Now, let's define the verifyToken function for actual token verification:

// Function to verify JWT tokens
func verifyToken(tokenString string) (*jwt.Token, error) {
    // Parse the token with the secret key
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return secretKey, nil
    })

    // Check for verification errors
    if err != nil {
        return nil, err
    }

    // Check if the token is valid
    if !token.Valid {
        return nil, fmt.Errorf("invalid token")
    }

    // Return the verified token
    return token, nil
}
Enter fullscreen mode Exit fullscreen mode

This function parses the JWT token using the jwt.Parse method, with our secret key used for verification. It checks for verification errors and token validity. If the token is valid, it returns the verified token.

token verification

With these modifications, you've successfully implemented token verification middleware and applied it to routes that require authentication in your ToDo application. Now you can add role-based permissions to enhance the security and functionality of your application.

Role-Based Permissions

In this section, we'll introduce Role-Based Access Control (RBAC) to our ToDo application, enhancing user interactions based on assigned roles. Our ToDo application will now have two distinct roles: senior and employee.

Seniors will enjoy elevated privileges, including the ability to add new ToDo items to the list. On the other hand, employees will have more restricted access and won't be able to add new ToDo items.

This implementation ensures a tailored user experience, with specific permissions assigned to each role, making our application more versatile and secure.

Update the main.go file to include the necessary changes for role-based permissions:

router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{
            "Todos":    todos,
            "LoggedIn": loggedInUser != "",
            "Username": loggedInUser,
            "Role":     getRole(loggedInUser),
        })
    })

router.GET("/logout", func(c *gin.Context) {
    loggedInUser = ""
    c.SetCookie("token", "", -1, "/", "localhost", false, true)
    c.Redirect(http.StatusSeeOther, "/")
    })
Enter fullscreen mode Exit fullscreen mode

In the above code, we've updated the main route (/) to include the user's role in the HTML template data. Additionally, we've added a new route (/logout) for logging out, which clears the logged-in user and redirects to the main page.

Update the index.html file to conditionally display the form for adding ToDo items based on the user's role:

{{ if .LoggedIn }}
<h1>Welcome, {{ .Username }}</h1>
<a href="/logout">Logout</a>

{{ if eq .Role "senior" }}
<!-- Form for seniors to add new ToDo items -->
<form action="/add" method="post">
    <input type="text" name="todo" required>
    <button type="submit">Add ToDo</button>
</form>
{{ else }}
<p>Employees can't add todos.</p>
{{ end }}

<!-- ToDo list -->
<ul>
        {{ range $index, $todo := .Todos }}
            <li>
                <form action="/toggle" method="post" style="display:inline;">
                    <input type="hidden" name="index" value="{{ $index }}">
                    <input type="checkbox" {{ if $todo.Done }}checked{{ end }} onchange="this.form.submit()">
                </form>
                {{ if $todo.Done }}
                    <del>{{ $todo.Text }}</del>
                {{ else }}
                    {{ $todo.Text }}
                {{ end }}
            </li>
        {{ end }}
    </ul>

{{ end }}
Enter fullscreen mode Exit fullscreen mode

Now, seniors will see the form, while employees will see a message indicating that they can't add ToDo items. The user's role is retrieved from the data provided by the Golang code.

role based permissions

With these updates, the ToDo application enforces role-based permissions, allowing only seniors to add new ToDo items.

Closing

We've successfully implemented JWT authentication in a Golang ToDo application. We covered the basics of JWT, role-based authorization, token creation, signing and verification.

Understanding these concepts is crucial for building secure and scalable applications with user authentication.

As you continue to refine your application's security and scalability, you'll find that managing granular permissions becomes increasingly challenging.

At that point consider exploring our solution, Permify. It's a Google Zanzibar-based open-source authorization service that helps to build scalable authorization systems.

Top comments (0)