Tombol “Login dengan Google” bekerja di balik dua protokol: OAuth2 mengatur cara aplikasi meminta izin akses tanpa pernah memegang password pengguna, dan OpenID Connect (OIDC) menambahkan lapisan identitas di atasnya. Sebagai penulis aplikasi, kita berperan sebagai client yang mempercayai authorization server milik pihak lain. Chapter ini membedah peran, alur authorization code dengan PKCE, lalu membangun URL otorisasi seperti keluaran library resmi golang.org/x/oauth2 — tanpa koneksi eksternal, hanya perhitungan lokal.

Empat Peran dalam OAuth2 #

OAuth2 membagi tanggung jawab kepada empat pihak agar password tidak pernah berpindah tangan. Tabel berikut memetakannya pada skenario Budi login memakai akun Google:

PeranPihak dalam skenarioTugas
Resource ownerBudipengguna yang memiliki data dan memberi izin
Clientaplikasi kitameminta akses atas nama Budi
Authorization serverGooglemengautentikasi Budi dan menerbitkan token
Resource serverAPI Googlemenyajikan data yang dilindungi token

Alur Authorization Code #

Alur paling umum — dan satu-satunya yang layak dipakai aplikasi web — menukar authorization code menjadi token lewat backend. Diagram berikut melukiskan seluruh perjalanan:

Aplikasi hanya mengirim Budi ke authorization server; login terjadi sepenuhnya di sana sehingga aplikasi tidak pernah melihat kredensial Google. Setelah Budi menyetujui consent — halaman yang menampilkan scope seperti profile atau email — server mengalihkan browser ke redirect_uri dengan code sekali pakai. Backend lalu menukar code di endpoint token dengan client_id, redirect_uri yang sama, dan code_verifier; penukaran ini terjadi server-ke-server melalui HTTPS.

PKCE untuk Klien Publik #

PKCE (Proof Key for Code Exchange) menutup celah pencekatan authorization code: aplikasi membuat code_verifier acak, mengirim hash SHA-256-nya sebagai code_challenge di URL otorisasi, lalu menyertakan code_verifier asli saat menukar code. Server menolak penukaran bila hash-nya tidak cocok, sehingga code yang dicuri di tengah jalan tidak bisa ditukar. Standar OAuth 2.1 menjadikan PKCE wajib untuk semua client — SPA dan aplikasi mobile yang tak bisa menyimpan client_secret paling bergantung padanya.

Program: Membangun URL Otorisasi #

Program oauth-demo.go merakit URL otorisasi lengkap: parameter standar, state, dan PKCE. Nilai state dan verifier dibuat tetap agar output deterministik, sementara buatState dibuktikan menghasilkan nilai yang selalu berbeda.

go
package main

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"log"
	"net/url"
)

// buatState menghasilkan state acak 16 byte untuk melindungi
// alur otorisasi dari CSRF.
func buatState() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func main() {
	// Generator state: panjang tetap dan nilainya tidak pernah sama.
	statePertama, err := buatState()
	if err != nil {
		log.Fatal(err)
	}
	stateKedua, err := buatState()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("panjang state    :", len(statePertama), "karakter hex")
	fmt.Println("dua state berbeda:", statePertama != stateKedua)

	// PKCE: verifier dibuat tetap agar output deterministik; produksi
	// membuat verifier acak 43-128 karakter pada setiap proses login.
	verifier := "verifier-demo-0123456789abcdefghijklmnopqrstuv"
	jumlah := sha256.Sum256([]byte(verifier))
	challenge := base64.RawURLEncoding.EncodeToString(jumlah[:])
	fmt.Println("code_verifier    :", verifier)
	fmt.Println("code_challenge   :", challenge)

	// URL otorisasi memakai state contoh; produksi menggantinya
	// dengan hasil buatState() yang tersimpan di sesi aplikasi.
	nilai := url.Values{}
	nilai.Set("response_type", "code")
	nilai.Set("client_id", "YOUR_CLIENT_ID")
	nilai.Set("redirect_uri", "https://aplikasi.example.com/callback")
	nilai.Set("scope", "openid profile")
	nilai.Set("state", "statedemotetap0123456789")
	nilai.Set("code_challenge", challenge)
	nilai.Set("code_challenge_method", "S256")

	alamat := url.URL{
		Scheme:   "https",
		Host:     "auth.example.com",
		Path:     "/authorize",
		RawQuery: nilai.Encode(),
	}
	fmt.Println()
	fmt.Println("URL otorisasi:")
	fmt.Println(alamat.String())
}

Output yang Dihasilkan:

text
panjang state    : 32 karakter hex
dua state berbeda: true
code_verifier    : verifier-demo-0123456789abcdefghijklmnopqrstuv
code_challenge   : 9dJ6n5YrGdT4wuT7nCAE6khmsFKPloJM3x2eA2ou3h8

URL otorisasi:
https://auth.example.com/authorize?client_id=YOUR_CLIENT_ID&code_challenge=9dJ6n5YrGdT4wuT7nCAE6khmsFKPloJM3x2eA2ou3h8&code_challenge_method=S256&redirect_uri=https%3A%2F%2Faplikasi.example.com%2Fcallback&response_type=code&scope=openid+profile&state=statedemotetap0123456789

Mari kita telaah baris demi baris program di atas:

  1. Baris 13-21: buatState mengisi 16 byte dari crypto/rand dan menyandikannya menjadi 32 karakter hex — nilai yang tidak mungkin ditebak dan tidak pernah sama antar proses login.
  2. Baris 38-40: PKCE menghitung SHA-256 dari verifier lalu menyandikannya ke base64 URL; hasil inilah code_challenge yang aman dikirim di URL.
  3. Baris 46-53: url.Values mengumpulkan parameter standar — response_type=code menandai alur authorization code, client_id mengidentifikasi aplikasi, redirect_uri harus sama persis saat penukaran, dan scope meminta akses.
  4. Baris 55-60: struct url.URL merakit alamat lengkap; RawQuery: nilai.Encode() mengurutkan kunci dan meng-escape nilai — perhatikan scope=openid+profile yang menggantikan spasi.

Dalam kode produksi, penukaran code cukup memakai package golang.org/x/oauth2:

go
conf := &oauth2.Config{
	ClientID:     "YOUR_CLIENT_ID",
	ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"),
	RedirectURL:  "https://aplikasi.example.com/callback",
	Scopes:       []string{"openid", "profile"},
	Endpoint: oauth2.Endpoint{
		AuthURL:  "https://auth.example.com/authorize",
		TokenURL: "https://auth.example.com/token",
	},
}
// menukar authorization code menjadi token di handler callback
token, err := conf.Exchange(r.Context(), code)

Grant Lain dan Refresh Token #

Tidak semua pertukaran melibatkan pengguna. Client credentials grant memakai client_id dan client_secret aplikasi sendiri — cocok untuk komunikasi antar layanan. Refresh token yang dibawa pulang bersama access token memungkinkan aplikasi meminta token baru tanpa memaksa Budi login ulang; access token sengaja berumur pendek, dan refresh token dapat dicabut satu per satu. Implicit flow lama yang menitipkan token di fragment URL sudah dilarang.

OIDC: Identitas di Atas Otorisasi #

OAuth2 semula menjawab “bolehkah aplikasi mengakses data ini” — bukan “siapa penggunanya”. OIDC menambahkan scope openid sehingga server turut menerbitkan ID token: JWT bertanda tangan berisi klaim sub, email, dan name yang diverifikasi seperti token pada chapter sebelumnya. Authorization server pun menjadi penyedia login terpusat, dan atribut pengguna dapat dilengkapi lewat endpoint userinfo.

Kesalahan Umum #

Empat kekeliruan berikut paling sering menjatuhkan implementasi OAuth2:

  • redirect_uri tidak terdaftar atau beda satu karakter dengan yang dikirim saat penukaran — authorization server menolak dengan invalid_grant.
  • state tidak diperiksa di callback — celah CSRF pada alur login.
  • client_secret ditanam di SPA atau aplikasi mobile — rahasia itu dapat diekstrak siapa pun; ganti dengan PKCE.
  • Access token ditaruh di localStorage tanpa pertimbangan XSS — kembali ke trade-off chapter Token-Based Authentication.

Pendelegasian login ke pihak ketiga hanya separuh pekerjaan; permintaan sah pun bisa datang dari situs lain dengan memanfaatkan cookie pengguna — pertahanan CSRF itulah topik chapter penutup section ini.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

oauth-demo.go

Unduh berkas
go
package main

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"log"
	"net/url"
)

// buatState menghasilkan state acak 16 byte untuk melindungi
// alur otorisasi dari CSRF.
func buatState() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}

func main() {
	// Generator state: panjang tetap dan nilainya tidak pernah sama.
	statePertama, err := buatState()
	if err != nil {
		log.Fatal(err)
	}
	stateKedua, err := buatState()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("panjang state    :", len(statePertama), "karakter hex")
	fmt.Println("dua state berbeda:", statePertama != stateKedua)

	// PKCE: verifier dibuat tetap agar output deterministik; produksi
	// membuat verifier acak 43-128 karakter pada setiap proses login.
	verifier := "verifier-demo-0123456789abcdefghijklmnopqrstuv"
	jumlah := sha256.Sum256([]byte(verifier))
	challenge := base64.RawURLEncoding.EncodeToString(jumlah[:])
	fmt.Println("code_verifier    :", verifier)
	fmt.Println("code_challenge   :", challenge)

	// URL otorisasi memakai state contoh; produksi menggantinya
	// dengan hasil buatState() yang tersimpan di sesi aplikasi.
	nilai := url.Values{}
	nilai.Set("response_type", "code")
	nilai.Set("client_id", "YOUR_CLIENT_ID")
	nilai.Set("redirect_uri", "https://aplikasi.example.com/callback")
	nilai.Set("scope", "openid profile")
	nilai.Set("state", "statedemotetap0123456789")
	nilai.Set("code_challenge", challenge)
	nilai.Set("code_challenge_method", "S256")

	alamat := url.URL{
		Scheme:   "https",
		Host:     "auth.example.com",
		Path:     "/authorize",
		RawQuery: nilai.Encode(),
	}
	fmt.Println()
	fmt.Println("URL otorisasi:")
	fmt.Println(alamat.String())
}