pemrograman

10 Membuat Middleware Authentication Menggunakan Jwt

Dalam pengembangan aplikasi modern, autentikasi adalah salah satu komponen paling penting. JSON Web Tokens (JWT) adalah metode populer untuk menangani autentikasi berbasis token. Pada artikel ini, kita akan membuat middleware autentikasi menggunakan JWT di Golang dengan library httprouter dari julienschmidt.

Artikel ini dirancang untuk programmer pemula dengan langkah-langkah terperinci dan disertai penjelasan sehingga mudah diikuti.

Prasyarat

  1. Pemahaman dasar tentang Golang: Sebaiknya Anda sudah memahami dasar-dasar Golang seperti fungsi, struktur, dan modul.
  2. Golang terinstal: Pastikan Anda sudah menginstal Go di sistem Anda.
  3. Library pendukung: Kita akan menggunakan beberapa library tambahan, yaitu:
    • github.com/golang-jwt/jwt/v4 untuk mengelola JWT.
    • github.com/julienschmidt/httprouter untuk routing.

Untuk menginstal library di atas, gunakan perintah:

go get github.com/golang-jwt/jwt/v4 github.com/julienschmidt/httprouter

Langkah 1: Membuat Struktur Proyek

Buat struktur folder berikut:

project-root/
├── main.go
├── handlers.go
├── pkg/
│   ├── middleware/
│   │   └── auth.go
│   └── utils/
│       └── jwt.go

File main.go adalah entry point aplikasi, sedangkan folder lainnya mengelompokkan berbagai fungsi dan middleware.


Langkah 2: Membuat Main File

Buka file main.go dan tuliskan kode berikut:

package main

import (
	"embed"
	"encoding/base64"
	"fmt"
	"io/fs"
	"net/http"

	"github.com/julienschmidt/httprouter"
	middleware "github.com/santekno/learn-golang-httprouter/pkg/middleware"
	"github.com/santekno/learn-golang-httprouter/pkg/utils"
)

func main() {
	router := httprouter.New()

	// sample generate JWT untuk dipakai testing live
	token, _ := utils.GenerateJWT("ihsan")
	fmt.Println("generate jwt sample: " + token)

	// Endpoint public tanpa autentikasi
	router.GET("/public", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
		fmt.Fprint(w, "Endpoint publik tidak memerlukan autentikasi!\n")
	})

	// Endpoint private menggunakan middleware autentikasi
	router.GET("/private", middleware.JWTAuthentication(PrivateHandler))

	log.Println("Server berjalan di http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", router))
}

Kode di atas adalah contoh minimal aplikasi yang menggunakan middleware untuk endpoint privat.


Langkah 3: Membuat Middleware JWT

3.1 Membuat fungsi Middleware handler

Buka file middleware/jwt.go dan tambahkan kode berikut:

package midleware

import (
	"context"
	"net/http"
	"strings"

	"github.com/julienschmidt/httprouter"
	"github.com/santekno/learn-golang-httprouter/pkg/utils"
)

func JWTAuthentication(next httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
		tokenString := extractToken(r)
		if tokenString == "" {
			http.Error(w, "Authentication header missing", http.StatusUnauthorized)
			return
		}

		// melakukan validasi token
		claims, err := utils.ValidateJWT(tokenString)
		if err != nil {
			http.Error(w, "Token tidak valid", http.StatusUnauthorized)
			return
		}

		// menambahkan data ke context
		r = r.WithContext(context.WithValue(r.Context(), "user", claims))

		// melanjutkan ke handler berikutnya
		next(w, r, p)
	}
}

func extractToken(r *http.Request) string {
	bearer := r.Header.Get("Authorization")
	if bearer == "" || !strings.HasPrefix(bearer, "Bearer ") {
		return ""
	}

	return strings.TrimPrefix(bearer, "Bearer ")
}

Middleware ini mengekstrak token dari header Authorization, memvalidasi tokennya, dan melanjutkan ke handler berikutnya jika token valid.

3.2 Membuat Unit test pada fungsi Middleware Handler

Untuk memastikan fungsi diatas berjalan dengan baik dan sesuai dengan kebutuhan, maka kita perlu membuat unit test agar memastikan semua logic yang kita butuhkan sesuai sebagai berikut.

package midleware

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/julienschmidt/httprouter"
	"github.com/santekno/learn-golang-httprouter/pkg/utils"
)

func TestJWTAuthentication(t *testing.T) {
	type args struct {
		username string
	}
	tests := []struct {
		name           string
		args           args
		mock           func(args args) string
		wantHTTPStatus int
		wantResponse   string
	}{
		{
			name: "valid token",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				token, _ := utils.GenerateJWT(args.username)
				return "Bearer " + token
			},
			wantHTTPStatus: http.StatusOK,
			wantResponse:   "Hello, ihsan",
		},
		{
			name: "missing authentcation header",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				return "Bearer "
			},
			wantHTTPStatus: http.StatusUnauthorized,
			wantResponse:   "Authentication header missing",
		},
		{
			name: "invalidate token",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				return "Bearer invalid.token.string"
			},
			wantHTTPStatus: http.StatusUnauthorized,
			wantResponse:   "Token tidak valid",
		},
		{
			name: "not include bearer",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				return ""
			},
			wantHTTPStatus: http.StatusUnauthorized,
			wantResponse:   "Authentication header missing",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			token := tt.mock(tt.args)

			// membuat request dengan header yang valid
			request := httptest.NewRequest(http.MethodGet, "/", nil)
			request.Header.Set("Authorization", token)

			// buat recorder untuk menangkap response
			recorder := httptest.NewRecorder()

			handler := JWTAuthentication(mockHandler)
			handler(recorder, request, httprouter.Params{})

			if recorder.Code != tt.wantHTTPStatus {
				t.Errorf("expected 200 ok, got %d", recorder.Code)
			}

			if !strings.Contains(recorder.Body.String(), tt.wantResponse) {
				t.Errorf("unexpected response body %s", recorder.Body.String())
			}
		})
	}
}

func mockHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	claims, ok := r.Context().Value("user").(*utils.CustomClaims)
	if !ok {
		http.Error(w, "calims not found", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Hello, " + claims.Username))
}

Langkah 4: Membuat Handler untuk Endpoint Privat

Buka file handlers/handlers.go dan tambahkan kode berikut:

func PrivateHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	claims, ok := r.Context().Value("user").(*utils.CustomClaims)
	if !ok {
		http.Error(w, "claims not found", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Hello, " + claims.Username))
}

Handler ini hanya dapat diakses jika middleware JWT berhasil memvalidasi tokennya.


Langkah 5: Membuat Utilitas untuk JWT

5.1 Membuat fungsi utility JWT

Buka file utils/jwt.go dan tambahkan kode berikut:

package utils

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

var secretKey = []byte("rahasia_super")

type CustomClaims struct {
	Username string `json:"username"`
	jwt.RegisteredClaims
}

func GenerateJWT(username string) (string, error) {
	claims := CustomClaims{
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(secretKey)
}

func ValidateJWT(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(t *jwt.Token) (interface{}, error) {
		return secretKey, nil
	})
	if err != nil {
		return nil, err
	}

	claims, ok := token.Claims.(*CustomClaims)
	if !ok || !token.Valid {
		return nil, errors.New("token tidak valid")
	}

	return claims, nil
}

File ini berisi fungsi untuk membuat dan memvalidasi JWT. GenerateJWT digunakan untuk membuat token, sedangkan ValidateJWT untuk memeriksa keabsahan token.

5.2 Membuat Unit Testing pada fungsi utility JWT

Berikut fungsi unit testing pada fungsi utility JWT untuk memastiakn fungsionalitas berjalan dengan baik.

package utils

import (
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

func TestGenerateJWT(t *testing.T) {
	type args struct {
		username string
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "got empty string",
			args: args{
				username: "",
			},
			wantErr: false,
		},
		{
			name: "success generate token",
			args: args{
				username: "ihsan",
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GenerateJWT(tt.args.username)
			if (err != nil) != tt.wantErr {
				t.Errorf("GenerateJWT() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got == "" {
				t.Error("expectged a token string, got emtpy string")
			}

			if !strings.Contains(got, ".") {
				t.Errorf("expected JWT token structure with dots got: %s", got)
			}
		})
	}
}

func TestValidateJWT(t *testing.T) {
	type args struct {
		username string
	}
	tests := []struct {
		name     string
		args     args
		mock     func(args args) string
		wantUser string
		wantErr  bool
	}{
		{
			name: "success validate jwt",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				tokenString, _ := GenerateJWT(args.username)
				return tokenString
			},
			wantUser: "ihsan",
			wantErr:  false,
		},
		{
			name: "invalid token",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				return "this.is.not.a.valid.token"
			},
			wantErr: true,
		},
		{
			name: "expired token",
			args: args{
				username: "ihsan",
			},
			mock: func(args args) string {
				expiredClaims := CustomClaims{
					Username: args.username,
					RegisteredClaims: jwt.RegisteredClaims{
						ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), // expired
					},
				}
				token := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims)
				expiredTokenString, _ := token.SignedString(secretKey)
				return expiredTokenString
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tokenString := tt.mock(tt.args)
			got, err := ValidateJWT(tokenString)
			if (err != nil) != tt.wantErr {
				t.Errorf("ValidateJWT() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != nil && !reflect.DeepEqual(got.Username, tt.wantUser) {
				t.Errorf("ValidateJWT() = %v, want %v", got.Username, tt.wantUser)
			}
		})
	}
}

Langkah 6: Menguji Aplikasi

Jalankan aplikasi dengan perintah berikut:

go run main.go

Cobalah endpoint berikut menggunakan alat seperti curl atau Postman.

1. Endpoint Public (Tanpa Token)

curl http://localhost:8080/public

Respons:

Endpoint publik tidak memerlukan autentikasi!

2. Endpoint Private (Dengan Token)

Pertama, buat token menggunakan GenerateJWT (implementasikan di aplikasi lain atau secara manual untuk tes awal). Lalu, gunakan token untuk mengakses endpoint privat:

curl -H "Authorization: Bearer <TOKEN_ANDA>" http://localhost:8080/private

Jika token valid, responsnya adalah:

Selamat datang di endpoint privat! Token Anda valid.

Kesimpulan

Anda telah berhasil membuat middleware autentikasi menggunakan JWT di Golang dengan httprouter. Dengan pendekatan ini, Anda dapat memastikan bahwa endpoint-endpoint sensitif hanya dapat diakses oleh pengguna yang telah terautentikasi. Middleware ini juga fleksibel untuk diterapkan di aplikasi produksi.

Semoga artikel ini membantu Anda memahami konsep dasar JWT di Golang. Jika ada pertanyaan, jangan ragu untuk bertanya atau eksplorasi lebih lanjut!

comments powered by Disqus