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:
- Go: Pastikan Anda telah menginstal Go pada sistem Anda.
- Library
httprouter: Kita akan menggunakan library ini untuk menangani rute HTTP. - 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:
1go get github.com/julienschmidt/httprouter1. 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:
1go get golang.org/x/oauth2
2go get golang.org/x/oauth2/google3. 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:
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, danredirectURL. - 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:
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:
1router.GET("/callback", callbackHandler)5. Menambahkan Unit Test
Berikut adalah unit test untuk loadTokenFromRequest dan OAuth2, yang memastikan bahwa middleware berfungsi dengan benar:
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:
1go test -vKesimpulan
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 .