pemrograman

13 Membuat Middleware Authentication Menggunakan Oauth Pada Golang

Di dunia pemrograman modern, keamanan aplikasi menjadi hal yang sangat penting, terutama ketika melibatkan otentikasi pengguna. Salah satu metode otentikasi yang banyak digunakan saat ini adalah OAuth 2.0, yang memungkinkan aplikasi untuk mengakses sumber daya pengguna tanpa membagikan kredensial mereka. Dalam artikel ini, kita akan membahas bagaimana cara membuat middleware otentikasi menggunakan OAuth 2.0 dengan menggunakan Go dan library httprouter.

Apa itu OAuth 2.0?

OAuth 2.0 adalah protokol otentikasi dan otorisasi yang memungkinkan aplikasi pihak ketiga untuk mendapatkan akses terbatas ke sumber daya pengguna tanpa membagikan informasi kredensial pengguna. Protokol ini banyak digunakan di layanan besar seperti Google, Facebook, dan GitHub untuk memungkinkan login pengguna menggunakan akun yang sudah ada.

Dalam implementasi ini, pengguna akan diajak untuk login melalui penyedia otentikasi (seperti Google atau GitHub), dan aplikasi kita akan mendapatkan token akses yang memungkinkan akses ke data pengguna.

Mengapa Menggunakan httprouter?

Library httprouter adalah router HTTP yang cepat dan ringan untuk aplikasi Go. Router ini sangat cocok digunakan dalam aplikasi Go karena performanya yang tinggi dan cara penggunaannya yang sederhana. Dengan httprouter, kita dapat menangani permintaan HTTP dengan sangat efisien.

Pada artikel ini, kita akan memanfaatkan httprouter untuk membuat middleware otentikasi OAuth 2.0.

Persyaratan

Sebelum memulai, pastikan Anda memiliki beberapa hal berikut:

  1. Go: Pastikan Anda telah menginstal Go pada sistem Anda.
  2. Library httprouter: Kita akan menggunakan library ini untuk menangani rute HTTP.
  3. OAuth 2.0: Anda perlu mendaftar aplikasi pada penyedia OAuth (seperti Google atau GitHub) untuk mendapatkan kredensial klien (client ID dan client secret).

Instalasi httprouter:

go get github.com/julienschmidt/httprouter

1. Menyiapkan Environment OAuth 2.0

Langkah pertama adalah membuat aplikasi di penyedia OAuth 2.0 yang Anda pilih (misalnya, Google). Anda perlu mendapatkan client ID dan client secret dari penyedia tersebut.

Jika Anda memilih Google, Anda dapat melakukannya dengan mengunjungi Google Developers Console dan membuat proyek baru. Setelah itu, buat kredensial OAuth 2.0 dan simpan client ID dan client secret.

2. Menggunakan Library OAuth 2.0 untuk Go

Go memiliki beberapa library untuk bekerja dengan OAuth 2.0. Salah satu yang paling populer adalah golang.org/x/oauth2.

Untuk menginstal library ini, jalankan perintah berikut:

go get golang.org/x/oauth2
go get golang.org/x/oauth2/google

3. Membuat Middleware Authentication

Sekarang kita akan membuat middleware yang akan memeriksa token akses OAuth 2.0 dalam setiap permintaan HTTP. Jika token valid, permintaan diteruskan; jika tidak, akan mengarahkan pengguna untuk login terlebih dahulu.

Berikut adalah contoh implementasi middleware otentikasi menggunakan OAuth 2.0:

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

var oauth2Config oauth2.Config

// OAuth2 middleware to check for valid token
func OAuth2(next httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
		// load token dari request header user
		token, err := loadTokenFromRequest(r)
		if err != nil || !token.Valid() {
			http.Redirect(w, r, OauthConfig.AuthCodeURL("", oauth2.AccessTypeOffline), http.StatusNotFound)
			return
		}

		// create token source using load token
		ts := OauthConfig.TokenSource(r.Context(), token)

		// optionally refresh token
		token, err = ts.Token()
		if err != nil || !token.Valid() {
			http.Redirect(w, r, OauthConfig.AuthCodeURL("", oauth2.AccessTypeOffline), http.StatusNotFound)
		}

		// continue to the next handler
		next(w, r, p)
	}
}

func main() {
    err := godotenv.Load()
	if err != nil {
		log.Fatal("error loading .env files")
	}

	middleware.OauthConfig = &oauth2.Config{
		ClientID:     os.Getenv("OAUTH_GOOGLE_CLIENT_ID"),
		ClientSecret: os.Getenv("OAUTH_GOOGLE_CLIENT_SECRET"),
		RedirectURL:  "http://localhost:8080/callback",
		Scopes:       []string{"openid", "profile", "email"},
		Endpoint:     google.Endpoint,
	}

    router := httprouter.New()

    // Protect /home route with OAuth2 middleware
    router.GET("/home", oauth2Middleware(homeHandler))

    log.Println("Starting server on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

Penjelasan Kode

  • oauth2.Config: Menyimpan konfigurasi OAuth 2.0, termasuk clientID, clientSecret, dan redirectURL.
  • oauth2Middleware: Middleware ini memeriksa apakah token OAuth 2.0 yang diteruskan bersama permintaan valid. Jika tidak valid, pengguna akan diarahkan untuk login.
  • homeHandler: Handler untuk route /home, yang hanya dapat diakses jika pengguna telah berhasil diautentikasi.

4. Menambahkan Route Callback

Setelah pengguna berhasil login, penyedia OAuth 2.0 akan mengalihkan mereka kembali ke aplikasi Anda ke URL callback yang telah Anda tentukan dalam konfigurasi (RedirectURL). Berikut adalah handler untuk menangani callback:

func HomeOauthHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "welcome, you are authenticated!\n")
}

// callback handler after success login oauth authentication
func CallbackHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	code := r.URL.Query().Get("code")
	token, err := midleware.OauthConfig.Exchange(r.Context(), code)
	if err != nil {
		http.Error(w, "Failed to get token"+err.Error(), http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "You are authenticated! Token: %s", token.AccessToken)
}

Jangan lupa untuk menambahkan route untuk callback:

router.GET("/callback", callbackHandler)

5. Menambahkan Unit Test

Berikut adalah unit test untuk loadTokenFromRequest dan OAuth2, yang memastikan bahwa middleware berfungsi dengan benar:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/stretchr/testify/assert"
)

func Test_loadTokenFromRequest(t *testing.T) {
	type args struct {
		r *http.Request
	}

	// Helper to create a request with a cookie
	createRequestWithToken := func(token oauth2.Token) *http.Request {
		tokenBytes, _ := json.Marshal(token)
		encodedToken := base64.StdEncoding.EncodeToString(tokenBytes)

		req := httptest.NewRequest(http.MethodGet, "/", nil)
		req.AddCookie(&http.Cookie{
			Name:  "oauth_token",
			Value: encodedToken,
		})
		return req
	}

	tests := []struct {
		name    string
		args    args
		want    *oauth2.Token
		wantErr bool
	}{
		{
			name: "successfully load token",
			args: args{
				r: createRequestWithToken(oauth2.Token{
					AccessToken: "test_access_token",
					TokenType:   "Bearer",
				}),
			},
			want: &oauth2.Token{
				AccessToken: "test_access_token",
				TokenType:   "Bearer",
			},
			wantErr: false,
		},
		{
			name: "missing cookie",
			args: args{
				r: httptest.NewRequest(http.MethodGet, "/", nil),
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "invalid base64",
			args: args{
				r: func() *http.Request {
					req := httptest.NewRequest(http.MethodGet, "/", nil)
					req.AddCookie(&http.Cookie{
						Name:  "oauth_token",
						Value: "invalid-base64-@@@",
					})
					return req
				}(),
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "invalid json after base64 decoding",
			args: args{
				r: func() *http.Request {
					req := httptest.NewRequest(http.MethodGet, "/", nil)
					invalidJSON := base64.StdEncoding.EncodeToString([]byte("not a json"))
					req.AddCookie(&http.Cookie{
						Name:  "oauth_token",
						Value: invalidJSON,
					})
					return req
				}(),
			},
			want:    nil,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := loadTokenFromRequest(tt.args.r)
			if (err != nil) != tt.wantErr {
				t.Errorf("loadTokenFromRequest() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("loadTokenFromRequest() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_OAuth2(t *testing.T) {
	type args struct {
		setupRequest func() *http.Request
	}
	tests := []struct {
		name           string
		args           args
		wantRedirect   bool
		wantStatusCode int
	}{
		{
			name: "valid token proceeds to next handler",
			args: args{
				setupRequest: func() *http.Request {
					token := oauth2.Token{
						AccessToken: "valid_token",
						TokenType:   "Bearer",
					}
					tokenBytes, _ := json.Marshal(token)
					encodedToken := base64.StdEncoding.EncodeToString(tokenBytes)

					req := httptest.NewRequest(http.MethodGet, "/", nil)
					req.AddCookie(&http.Cookie{
						Name:  "oauth_token",
						Value: encodedToken,
					})
					return req
				},
			},
			wantRedirect:   false,
			wantStatusCode: http.StatusOK,
		},
		{
			name: "missing token redirects",
			args: args{
				setupRequest: func() *http.Request {
					return httptest.NewRequest(http.MethodGet, "/", nil)
				},
			},
			wantRedirect:   true,
			wantStatusCode: http.StatusNotFound,
		},
		{
			name: "invalid token redirects",
			args: args{
				setupRequest: func() *http.Request {
					req := httptest.NewRequest(http.MethodGet, "/", nil)
					req.AddCookie(&http.Cookie{
						Name:  "oauth_token",
						Value: "invalid-base64-@@@",
					})
					return req
				},
			},
			wantRedirect:   true,
			wantStatusCode: http.StatusNotFound,
		},
	}

	// Mock OauthConfig
	OauthConfig = &oauth2.Config{
		ClientID:     "client_id",
		ClientSecret: "client_secret",
		Endpoint:     oauth2.Endpoint{},
		RedirectURL:  "http://localhost",
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Prepare recorder and router params
			rr := httptest.NewRecorder()
			req := tt.args.setupRequest()
			params := httprouter.Params{}

			// Define a dummy next handler
			nextCalled := false
			next := func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
				nextCalled = true
				w.WriteHeader(http.StatusOK)
			}

			// Wrap middleware
			handler := OAuth2(next)
			handler(rr, req, params)

			if tt.wantRedirect {
				if rr.Code != tt.wantStatusCode {
					t.Errorf("expected redirect with status %d, got %d", tt.wantStatusCode, rr.Code)
				}
			} else {
				if !nextCalled {
					t.Errorf("expected next handler to be called, but it was not")
				}
				if rr.Code != tt.wantStatusCode {
					t.Errorf("expected status %d, got %d", tt.wantStatusCode, rr.Code)
				}
			}
		})
	}
}

Untuk menjalankan unit test:

go test -v

Kesimpulan

Membuat middleware otentikasi menggunakan OAuth 2.0 dengan Go dan httprouter adalah cara yang efektif untuk mengamankan aplikasi Anda dan memberikan akses kepada pengguna yang sudah terautentikasi. Dengan mengikuti tutorial ini, Anda telah mempelajari bagaimana cara menyiapkan OAuth 2.0, membuat middleware, serta melindungi rute di aplikasi Go Anda.

Untuk lebih banyak tutorial terkait Go, Anda dapat mengunjungi Tutorial Golang.

Juga, jika Anda tertarik untuk mempelajari lebih lanjut tentang routing HTTP di Go, silakan baca Web HTTP Router pada Golang.

comments powered by Disqus