pemrograman

11 Membuat Middleware Basic Authentication HTTP Router pada Golang

Basic Authentication adalah metode autentikasi sederhana di mana klien mengirimkan kredensial (username dan password) dalam header setiap permintaan HTTP. Pada artikel ini, kita akan mempelajari cara membuat middleware Basic Authentication di Golang menggunakan library httprouter.

Artikel ini ditujukan untuk programmer pemula dengan langkah-langkah detail dan penjelasan untuk mempermudah pemahaman.

Prasyarat

  1. Pemahaman dasar tentang Golang: Memahami fungsi dasar, struktur, dan cara kerja package.
  2. Golang terinstal: Pastikan Anda telah menginstal Go di komputer.
  3. Library pendukung: Instal library github.com/julienschmidt/httprouter dengan perintah berikut:
    go get github.com/julienschmidt/httprouter
    

1. Struktur Proyek

Buat struktur proyek seperti berikut:

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

File main.go adalah entry point aplikasi, sedangkan middleware/ dan handlers/ digunakan untuk memisahkan middleware dan fungsi endpoint.


2. Membuat File Utama

Buka file main.go dan tambahkan kode berikut:

package main

import (
	"fmt"
	"log"
	"net/http"
	"github.com/julienschmidt/httprouter"
	"project-root/middleware"
	"project-root/handlers"
)

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

		// sample testing generate basic authentication
	fmt.Println("generate basic auth: " + base64.StdEncoding.EncodeToString([]byte("ihsan:password")))

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

	// Endpoint privat menggunakan middleware Basic Authentication
	router.GET("/private", middleware.BasicAuth(handlers.PrivateHandler))

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

Kode di atas mendefinisikan endpoint publik dan endpoint privat yang dilindungi oleh middleware Basic Authentication.


3. Membuat Middleware Basic Authentication

3.1. Membuat Fungsi Middleware Basic Authentication

Buka file middleware/basic_auth.go dan tuliskan kode berikut:

package middleware

import (
	"encoding/base64"
	"net/http"
	"strings"
	"github.com/julienschmidt/httprouter"
)

var validUsers = map[string]string{
	"admin": "password123",
	"user":  "password456",
}

func BasicAuth(next httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
		authHeader := r.Header.Get("Authorization")
		if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// Decode Base64 dari kredensial
		payload, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic "))
		if err != nil {
			http.Error(w, "Invalid authentication token", http.StatusUnauthorized)
			return
		}

		cred := strings.SplitN(string(payload), ":", 2)
		if len(cred) != 2 || !validateUser(cred[0], cred[1]) {
			http.Error(w, "Invalid username or password", http.StatusUnauthorized)
			return
		}

		// Lanjutkan ke handler berikutnya jika valid
		next(w, r, ps)
	}
}

func validateUser(username, password string) bool {
	if pass, ok := validUsers[username]; ok {
		return pass == password
	}
	return false
}

Middleware ini membaca header Authorization, men-decode kredensial yang dienkripsi dalam Base64, dan memvalidasi username serta password menggunakan data yang disimpan di memori (dalam map validUsers).

3.2. Menambahkan pengujian fungsi dengan unit test

Berikut pengujian menggunakan unit test functionality dibawah ini.

package midleware

import (
	"encoding/base64"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/julienschmidt/httprouter"
)

func TestBasicAuth(t *testing.T) {
	type args struct {
		username string
		password string
	}
	tests := []struct {
		name           string
		args           args
		mock           func(args args) string
		wantHTTPStatus int
		wantResponse   string
	}{
		{
			name: "success valid basic authentication",
			args: args{
				username: "ihsan",
				password: "password",
			},
			mock: func(args args) string {
				return "Basic " + base64.StdEncoding.EncodeToString([]byte(args.username+":"+args.password))
			},
			wantHTTPStatus: http.StatusOK,
			wantResponse:   fmt.Sprintln("Selamat datang di endpoint private menggunakan basic authentication! Anda berhasil terautentikasi."),
		},
		{
			name: "invalid because authorization header not set",
			args: args{},
			mock: func(args args) string {
				return ""
			},
			wantHTTPStatus: http.StatusUnauthorized,
			wantResponse:   fmt.Sprintln("Unauthorized"),
		},
		{
			name: "invalid username or password",
			args: args{
				username: "santekno",
				password: "password",
			},
			mock: func(args args) string {
				return "Basic " + base64.StdEncoding.EncodeToString([]byte(args.username+":"+args.password))
			},
			wantHTTPStatus: http.StatusUnauthorized,
			wantResponse:   fmt.Sprintln("Invalid username or password"),
		},
	}
	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 := BasicAuth(MockBasicAuthHandler)
			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 MockBasicAuthHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Selamat datang di endpoint private menggunakan basic authentication! Anda berhasil terautentikasi.\n")
}

4. Membuat Handler untuk Endpoint Privat

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

package handlers

import (
	"fmt"
	"net/http"
	"github.com/julienschmidt/httprouter"
)

func PrivateHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Selamat datang di endpoint privat! Anda berhasil terautentikasi.\n")
}

Handler ini hanya akan diakses jika kredensial Basic Authentication yang diberikan valid.


5. Menguji Aplikasi

Jalankan aplikasi menggunakan perintah berikut:

go run main.go

Cobalah akses endpoint berikut menggunakan alat seperti curl atau Postman.

5.1. Endpoint Publik (Tanpa Autentikasi)

curl http://localhost:8080/public

Responsnya:

Endpoint publik tidak membutuhkan autentikasi!

5.2. Endpoint Privat (Dengan Autentikasi)

Untuk mengakses endpoint privat, tambahkan header Authorization dengan nilai Basic diikuti kredensial username dan password yang dienkode dalam Base64.

Misalnya, untuk username admin dan password password123:

curl -H "Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM=" http://localhost:8080/private

Jika valid, responsnya adalah:

Selamat datang di endpoint privat! Anda berhasil terautentikasi.

Jika kredensial salah, Anda akan menerima respons error 401 Unauthorized.


Kesimpulan

Dengan langkah-langkah di atas, Anda telah berhasil membuat middleware Basic Authentication di Golang menggunakan httprouter. Middleware ini adalah solusi sederhana untuk melindungi endpoint API, meskipun kurang aman untuk penggunaan di lingkungan produksi jika tidak dilengkapi protokol HTTPS.

Untuk pengamanan lebih baik, pertimbangkan menggunakan metode autentikasi yang lebih modern, seperti OAuth atau JWT, di aplikasi produksi Anda. Selamat mencoba!

comments powered by Disqus