Skip to content
Santekno.com | Level Up Your Engineering Skills
ID
📖 0%
21 Jan 2025 · 8 mnt baca ·Artikel 69 / 119
Go

10 Membuat Middleware Authentication Menggunakan Jwt

IH
Ihsan Arif
Penulis di Santekno · Backend Engineer

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:

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


Langkah 1: Membuat Struktur Proyek

Buat struktur folder berikut:

bash
1project-root/
2├── main.go
3├── handlers.go
4├── pkg/
5│   ├── middleware/
6│   │   └── auth.go
7│   └── utils/
8│       └── 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:

go
 1package main
 2
 3import (
 4	"embed"
 5	"encoding/base64"
 6	"fmt"
 7	"io/fs"
 8	"net/http"
 9
10	"github.com/julienschmidt/httprouter"
11	middleware "github.com/santekno/learn-golang-httprouter/pkg/middleware"
12	"github.com/santekno/learn-golang-httprouter/pkg/utils"
13)
14
15func main() {
16	router := httprouter.New()
17
18	// sample generate JWT untuk dipakai testing live
19	token, _ := utils.GenerateJWT("ihsan")
20	fmt.Println("generate jwt sample: " + token)
21
22	// Endpoint public tanpa autentikasi
23	router.GET("/public", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
24		fmt.Fprint(w, "Endpoint publik tidak memerlukan autentikasi!\n")
25	})
26
27	// Endpoint private menggunakan middleware autentikasi
28	router.GET("/private", middleware.JWTAuthentication(PrivateHandler))
29
30	log.Println("Server berjalan di http://localhost:8080")
31	log.Fatal(http.ListenAndServe(":8080", router))
32}
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:

go
 1package midleware
 2
 3import (
 4	"context"
 5	"net/http"
 6	"strings"
 7
 8	"github.com/julienschmidt/httprouter"
 9	"github.com/santekno/learn-golang-httprouter/pkg/utils"
10)
11
12func JWTAuthentication(next httprouter.Handle) httprouter.Handle {
13	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
14		tokenString := extractToken(r)
15		if tokenString == "" {
16			http.Error(w, "Authentication header missing", http.StatusUnauthorized)
17			return
18		}
19
20		// melakukan validasi token
21		claims, err := utils.ValidateJWT(tokenString)
22		if err != nil {
23			http.Error(w, "Token tidak valid", http.StatusUnauthorized)
24			return
25		}
26
27		// menambahkan data ke context
28		r = r.WithContext(context.WithValue(r.Context(), "user", claims))
29
30		// melanjutkan ke handler berikutnya
31		next(w, r, p)
32	}
33}
34
35func extractToken(r *http.Request) string {
36	bearer := r.Header.Get("Authorization")
37	if bearer == "" || !strings.HasPrefix(bearer, "Bearer ") {
38		return ""
39	}
40
41	return strings.TrimPrefix(bearer, "Bearer ")
42}
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.

go
  1package midleware
  2
  3import (
  4	"net/http"
  5	"net/http/httptest"
  6	"strings"
  7	"testing"
  8
  9	"github.com/julienschmidt/httprouter"
 10	"github.com/santekno/learn-golang-httprouter/pkg/utils"
 11)
 12
 13func TestJWTAuthentication(t *testing.T) {
 14	type args struct {
 15		username string
 16	}
 17	tests := []struct {
 18		name           string
 19		args           args
 20		mock           func(args args) string
 21		wantHTTPStatus int
 22		wantResponse   string
 23	}{
 24		{
 25			name: "valid token",
 26			args: args{
 27				username: "ihsan",
 28			},
 29			mock: func(args args) string {
 30				token, _ := utils.GenerateJWT(args.username)
 31				return "Bearer " + token
 32			},
 33			wantHTTPStatus: http.StatusOK,
 34			wantResponse:   "Hello, ihsan",
 35		},
 36		{
 37			name: "missing authentcation header",
 38			args: args{
 39				username: "ihsan",
 40			},
 41			mock: func(args args) string {
 42				return "Bearer "
 43			},
 44			wantHTTPStatus: http.StatusUnauthorized,
 45			wantResponse:   "Authentication header missing",
 46		},
 47		{
 48			name: "invalidate token",
 49			args: args{
 50				username: "ihsan",
 51			},
 52			mock: func(args args) string {
 53				return "Bearer invalid.token.string"
 54			},
 55			wantHTTPStatus: http.StatusUnauthorized,
 56			wantResponse:   "Token tidak valid",
 57		},
 58		{
 59			name: "not include bearer",
 60			args: args{
 61				username: "ihsan",
 62			},
 63			mock: func(args args) string {
 64				return ""
 65			},
 66			wantHTTPStatus: http.StatusUnauthorized,
 67			wantResponse:   "Authentication header missing",
 68		},
 69	}
 70	for _, tt := range tests {
 71		t.Run(tt.name, func(t *testing.T) {
 72			token := tt.mock(tt.args)
 73
 74			// membuat request dengan header yang valid
 75			request := httptest.NewRequest(http.MethodGet, "/", nil)
 76			request.Header.Set("Authorization", token)
 77
 78			// buat recorder untuk menangkap response
 79			recorder := httptest.NewRecorder()
 80
 81			handler := JWTAuthentication(mockHandler)
 82			handler(recorder, request, httprouter.Params{})
 83
 84			if recorder.Code != tt.wantHTTPStatus {
 85				t.Errorf("expected 200 ok, got %d", recorder.Code)
 86			}
 87
 88			if !strings.Contains(recorder.Body.String(), tt.wantResponse) {
 89				t.Errorf("unexpected response body %s", recorder.Body.String())
 90			}
 91		})
 92	}
 93}
 94
 95func mockHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 96	claims, ok := r.Context().Value("user").(*utils.CustomClaims)
 97	if !ok {
 98		http.Error(w, "calims not found", http.StatusInternalServerError)
 99		return
100	}
101	w.WriteHeader(http.StatusOK)
102	w.Write([]byte("Hello, " + claims.Username))
103}

Langkah 4: Membuat Handler untuk Endpoint Privat

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

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

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:

go
 1package utils
 2
 3import (
 4	"errors"
 5	"time"
 6
 7	"github.com/golang-jwt/jwt/v4"
 8)
 9
10var secretKey = []byte("rahasia_super")
11
12type CustomClaims struct {
13	Username string `json:"username"`
14	jwt.RegisteredClaims
15}
16
17func GenerateJWT(username string) (string, error) {
18	claims := CustomClaims{
19		Username: username,
20		RegisteredClaims: jwt.RegisteredClaims{
21			ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
22		},
23	}
24
25	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
26	return token.SignedString(secretKey)
27}
28
29func ValidateJWT(tokenString string) (*CustomClaims, error) {
30	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(t *jwt.Token) (interface{}, error) {
31		return secretKey, nil
32	})
33	if err != nil {
34		return nil, err
35	}
36
37	claims, ok := token.Claims.(*CustomClaims)
38	if !ok || !token.Valid {
39		return nil, errors.New("token tidak valid")
40	}
41
42	return claims, nil
43}
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.

go
  1package utils
  2
  3import (
  4	"reflect"
  5	"strings"
  6	"testing"
  7	"time"
  8
  9	"github.com/golang-jwt/jwt/v4"
 10)
 11
 12func TestGenerateJWT(t *testing.T) {
 13	type args struct {
 14		username string
 15	}
 16	tests := []struct {
 17		name    string
 18		args    args
 19		wantErr bool
 20	}{
 21		{
 22			name: "got empty string",
 23			args: args{
 24				username: "",
 25			},
 26			wantErr: false,
 27		},
 28		{
 29			name: "success generate token",
 30			args: args{
 31				username: "ihsan",
 32			},
 33			wantErr: false,
 34		},
 35	}
 36	for _, tt := range tests {
 37		t.Run(tt.name, func(t *testing.T) {
 38			got, err := GenerateJWT(tt.args.username)
 39			if (err != nil) != tt.wantErr {
 40				t.Errorf("GenerateJWT() error = %v, wantErr %v", err, tt.wantErr)
 41				return
 42			}
 43			if got == "" {
 44				t.Error("expectged a token string, got emtpy string")
 45			}
 46
 47			if !strings.Contains(got, ".") {
 48				t.Errorf("expected JWT token structure with dots got: %s", got)
 49			}
 50		})
 51	}
 52}
 53
 54func TestValidateJWT(t *testing.T) {
 55	type args struct {
 56		username string
 57	}
 58	tests := []struct {
 59		name     string
 60		args     args
 61		mock     func(args args) string
 62		wantUser string
 63		wantErr  bool
 64	}{
 65		{
 66			name: "success validate jwt",
 67			args: args{
 68				username: "ihsan",
 69			},
 70			mock: func(args args) string {
 71				tokenString, _ := GenerateJWT(args.username)
 72				return tokenString
 73			},
 74			wantUser: "ihsan",
 75			wantErr:  false,
 76		},
 77		{
 78			name: "invalid token",
 79			args: args{
 80				username: "ihsan",
 81			},
 82			mock: func(args args) string {
 83				return "this.is.not.a.valid.token"
 84			},
 85			wantErr: true,
 86		},
 87		{
 88			name: "expired token",
 89			args: args{
 90				username: "ihsan",
 91			},
 92			mock: func(args args) string {
 93				expiredClaims := CustomClaims{
 94					Username: args.username,
 95					RegisteredClaims: jwt.RegisteredClaims{
 96						ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), // expired
 97					},
 98				}
 99				token := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims)
100				expiredTokenString, _ := token.SignedString(secretKey)
101				return expiredTokenString
102			},
103			wantErr: true,
104		},
105	}
106	for _, tt := range tests {
107		t.Run(tt.name, func(t *testing.T) {
108			tokenString := tt.mock(tt.args)
109			got, err := ValidateJWT(tokenString)
110			if (err != nil) != tt.wantErr {
111				t.Errorf("ValidateJWT() error = %v, wantErr %v", err, tt.wantErr)
112				return
113			}
114			if got != nil && !reflect.DeepEqual(got.Username, tt.wantUser) {
115				t.Errorf("ValidateJWT() = %v, want %v", got.Username, tt.wantUser)
116			}
117		})
118	}
119}

Langkah 6: Menguji Aplikasi

Jalankan aplikasi dengan perintah berikut:

bash
1go run main.go
Cobalah endpoint berikut menggunakan alat seperti curl atau Postman.

1. Endpoint Public (Tanpa Token)

bash
1curl http://localhost:8080/public
Respons:

text
1Endpoint 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:

bash
1curl -H "Authorization: Bearer <TOKEN_ANDA>" http://localhost:8080/private
Jika token valid, responsnya adalah:

text
1Selamat 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!

Artikel Terkait

💬 Komentar