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
- Pemahaman dasar tentang Golang: Sebaiknya Anda sudah memahami dasar-dasar Golang seperti fungsi, struktur, dan modul.
- Golang terinstal: Pastikan Anda sudah menginstal Go di sistem Anda.
- Library pendukung: Kita akan menggunakan beberapa library tambahan, yaitu:
github.com/golang-jwt/jwt/v4untuk mengelola JWT.github.com/julienschmidt/httprouteruntuk routing.
Untuk menginstal library di atas, gunakan perintah:
1go get github.com/golang-jwt/jwt/v4 github.com/julienschmidt/httprouterLangkah 1: Membuat Struktur Proyek
Buat struktur folder berikut:
1project-root/
2├── main.go
3├── handlers.go
4├── pkg/
5│ ├── middleware/
6│ │ └── auth.go
7│ └── utils/
8│ └── jwt.gomain.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:
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}Langkah 3: Membuat Middleware JWT
3.1 Membuat fungsi Middleware handler
Buka file middleware/jwt.go dan tambahkan kode berikut:
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}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.
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:
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:
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}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.
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:
1go run main.gocurl atau Postman.1. Endpoint Public (Tanpa Token)
1curl http://localhost:8080/public1Endpoint 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:
1curl -H "Authorization: Bearer <TOKEN_ANDA>" http://localhost:8080/private1Selamat 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!