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

13 Membuat Middleware Authentication Menggunakan Oauth Pada Golang

IH
Ihsan Arif
Penulis di Santekno · Backend Engineer

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:

bash
1go 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:

bash
1go get golang.org/x/oauth2
2go 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:

go
 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "net/http"
 7
 8    "github.com/julienschmidt/httprouter"
 9    "golang.org/x/oauth2"
10    "golang.org/x/oauth2/google"
11)
12
13var oauth2Config oauth2.Config
14
15// OAuth2 middleware to check for valid token
16func OAuth2(next httprouter.Handle) httprouter.Handle {
17	return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
18		// load token dari request header user
19		token, err := loadTokenFromRequest(r)
20		if err != nil || !token.Valid() {
21			http.Redirect(w, r, OauthConfig.AuthCodeURL("", oauth2.AccessTypeOffline), http.StatusNotFound)
22			return
23		}
24
25		// create token source using load token
26		ts := OauthConfig.TokenSource(r.Context(), token)
27
28		// optionally refresh token
29		token, err = ts.Token()
30		if err != nil || !token.Valid() {
31			http.Redirect(w, r, OauthConfig.AuthCodeURL("", oauth2.AccessTypeOffline), http.StatusNotFound)
32		}
33
34		// continue to the next handler
35		next(w, r, p)
36	}
37}
38
39func main() {
40    err := godotenv.Load()
41	if err != nil {
42		log.Fatal("error loading .env files")
43	}
44
45	middleware.OauthConfig = &oauth2.Config{
46		ClientID:     os.Getenv("OAUTH_GOOGLE_CLIENT_ID"),
47		ClientSecret: os.Getenv("OAUTH_GOOGLE_CLIENT_SECRET"),
48		RedirectURL:  "http://localhost:8080/callback",
49		Scopes:       []string{"openid", "profile", "email"},
50		Endpoint:     google.Endpoint,
51	}
52
53    router := httprouter.New()
54
55    // Protect /home route with OAuth2 middleware
56    router.GET("/home", oauth2Middleware(homeHandler))
57
58    log.Println("Starting server on :8080")
59    log.Fatal(http.ListenAndServe(":8080", router))
60}

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:

go
 1func HomeOauthHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 2	fmt.Fprint(w, "welcome, you are authenticated!\n")
 3}
 4
 5// callback handler after success login oauth authentication
 6func CallbackHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 7	code := r.URL.Query().Get("code")
 8	token, err := midleware.OauthConfig.Exchange(r.Context(), code)
 9	if err != nil {
10		http.Error(w, "Failed to get token"+err.Error(), http.StatusInternalServerError)
11		return
12	}
13
14	fmt.Fprintf(w, "You are authenticated! Token: %s", token.AccessToken)
15}

Jangan lupa untuk menambahkan route untuk callback:

go
1router.GET("/callback", callbackHandler)

5. Menambahkan Unit Test

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

go
  1package main
  2
  3import (
  4    "net/http"
  5    "net/http/httptest"
  6    "testing"
  7    "github.com/stretchr/testify/assert"
  8)
  9
 10func Test_loadTokenFromRequest(t *testing.T) {
 11	type args struct {
 12		r *http.Request
 13	}
 14
 15	// Helper to create a request with a cookie
 16	createRequestWithToken := func(token oauth2.Token) *http.Request {
 17		tokenBytes, _ := json.Marshal(token)
 18		encodedToken := base64.StdEncoding.EncodeToString(tokenBytes)
 19
 20		req := httptest.NewRequest(http.MethodGet, "/", nil)
 21		req.AddCookie(&http.Cookie{
 22			Name:  "oauth_token",
 23			Value: encodedToken,
 24		})
 25		return req
 26	}
 27
 28	tests := []struct {
 29		name    string
 30		args    args
 31		want    *oauth2.Token
 32		wantErr bool
 33	}{
 34		{
 35			name: "successfully load token",
 36			args: args{
 37				r: createRequestWithToken(oauth2.Token{
 38					AccessToken: "test_access_token",
 39					TokenType:   "Bearer",
 40				}),
 41			},
 42			want: &oauth2.Token{
 43				AccessToken: "test_access_token",
 44				TokenType:   "Bearer",
 45			},
 46			wantErr: false,
 47		},
 48		{
 49			name: "missing cookie",
 50			args: args{
 51				r: httptest.NewRequest(http.MethodGet, "/", nil),
 52			},
 53			want:    nil,
 54			wantErr: true,
 55		},
 56		{
 57			name: "invalid base64",
 58			args: args{
 59				r: func() *http.Request {
 60					req := httptest.NewRequest(http.MethodGet, "/", nil)
 61					req.AddCookie(&http.Cookie{
 62						Name:  "oauth_token",
 63						Value: "invalid-base64-@@@",
 64					})
 65					return req
 66				}(),
 67			},
 68			want:    nil,
 69			wantErr: true,
 70		},
 71		{
 72			name: "invalid json after base64 decoding",
 73			args: args{
 74				r: func() *http.Request {
 75					req := httptest.NewRequest(http.MethodGet, "/", nil)
 76					invalidJSON := base64.StdEncoding.EncodeToString([]byte("not a json"))
 77					req.AddCookie(&http.Cookie{
 78						Name:  "oauth_token",
 79						Value: invalidJSON,
 80					})
 81					return req
 82				}(),
 83			},
 84			want:    nil,
 85			wantErr: true,
 86		},
 87	}
 88
 89	for _, tt := range tests {
 90		t.Run(tt.name, func(t *testing.T) {
 91			got, err := loadTokenFromRequest(tt.args.r)
 92			if (err != nil) != tt.wantErr {
 93				t.Errorf("loadTokenFromRequest() error = %v, wantErr %v", err, tt.wantErr)
 94				return
 95			}
 96			if !reflect.DeepEqual(got, tt.want) {
 97				t.Errorf("loadTokenFromRequest() = %v, want %v", got, tt.want)
 98			}
 99		})
100	}
101}
102
103func Test_OAuth2(t *testing.T) {
104	type args struct {
105		setupRequest func() *http.Request
106	}
107	tests := []struct {
108		name           string
109		args           args
110		wantRedirect   bool
111		wantStatusCode int
112	}{
113		{
114			name: "valid token proceeds to next handler",
115			args: args{
116				setupRequest: func() *http.Request {
117					token := oauth2.Token{
118						AccessToken: "valid_token",
119						TokenType:   "Bearer",
120					}
121					tokenBytes, _ := json.Marshal(token)
122					encodedToken := base64.StdEncoding.EncodeToString(tokenBytes)
123
124					req := httptest.NewRequest(http.MethodGet, "/", nil)
125					req.AddCookie(&http.Cookie{
126						Name:  "oauth_token",
127						Value: encodedToken,
128					})
129					return req
130				},
131			},
132			wantRedirect:   false,
133			wantStatusCode: http.StatusOK,
134		},
135		{
136			name: "missing token redirects",
137			args: args{
138				setupRequest: func() *http.Request {
139					return httptest.NewRequest(http.MethodGet, "/", nil)
140				},
141			},
142			wantRedirect:   true,
143			wantStatusCode: http.StatusNotFound,
144		},
145		{
146			name: "invalid token redirects",
147			args: args{
148				setupRequest: func() *http.Request {
149					req := httptest.NewRequest(http.MethodGet, "/", nil)
150					req.AddCookie(&http.Cookie{
151						Name:  "oauth_token",
152						Value: "invalid-base64-@@@",
153					})
154					return req
155				},
156			},
157			wantRedirect:   true,
158			wantStatusCode: http.StatusNotFound,
159		},
160	}
161
162	// Mock OauthConfig
163	OauthConfig = &oauth2.Config{
164		ClientID:     "client_id",
165		ClientSecret: "client_secret",
166		Endpoint:     oauth2.Endpoint{},
167		RedirectURL:  "http://localhost",
168	}
169
170	for _, tt := range tests {
171		t.Run(tt.name, func(t *testing.T) {
172			// Prepare recorder and router params
173			rr := httptest.NewRecorder()
174			req := tt.args.setupRequest()
175			params := httprouter.Params{}
176
177			// Define a dummy next handler
178			nextCalled := false
179			next := func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
180				nextCalled = true
181				w.WriteHeader(http.StatusOK)
182			}
183
184			// Wrap middleware
185			handler := OAuth2(next)
186			handler(rr, req, params)
187
188			if tt.wantRedirect {
189				if rr.Code != tt.wantStatusCode {
190					t.Errorf("expected redirect with status %d, got %d", tt.wantStatusCode, rr.Code)
191				}
192			} else {
193				if !nextCalled {
194					t.Errorf("expected next handler to be called, but it was not")
195				}
196				if rr.Code != tt.wantStatusCode {
197					t.Errorf("expected status %d, got %d", tt.wantStatusCode, rr.Code)
198				}
199			}
200		})
201	}
202}

Untuk menjalankan unit test:

bash
1go 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 .

Artikel Terkait

💬 Komentar