Endpoint yang kita bangun sejauh ini melayani siapa saja: semua permintaan diperlakukan sama tanpa identitas. API nyata harus lebih dulu menjawab siapa pemanggilnya sebelum memutuskan apa yang boleh dilakukan pemanggil tersebut — proses itu bernama authentication, sementara keputusan hak aksesnya menjadi chapter berikutnya. Pada chapter ini kita menerapkan Bearer token pada header Authorization, membedakan 401 dari 403, menyimpan token sebagai hash SHA-256, lalu menyediakan alternatif API key.
Bearer Token pada Header Authorization #
Mekanisme token paling umum memakai header standar Authorization dengan skema Bearer: nilai header berbunyi Bearer tok-budi-7f3a, dan bagian setelah kata Bearer adalah token itu sendiri. Kata Bearer berarti pembawa — siapa pun yang memegang token dianggap pemiliknya, sehingga token wajib dijaga kerahasiaannya dan hanya dikirim lewat HTTPS. Server memverifikasi token terhadap tabel kredensial, lalu melanjutkan permintaan atas nama pemegangnya.
Skema Basic pada header yang sama mengirim username dan password ter-encode base64. Skema ini kurang cocok untuk API: kredensial dikirim ulang pada setiap permintaan, tidak berkedaluwarsa, dan tidak bisa dicabut tanpa mengganti kata sandi. Token dapat diterbitkan, diberi masa berlaku, dan dihapus terpisah dari kata sandi pengguna.
401 atau 403 #
Kedua status ini sering tertukar padahal maknanya berbeda:
| Status | Makna sebenarnya | Tindakan klien |
|---|---|---|
| 401 Unauthorized | kredensial hilang atau salah — pemanggil belum diverifikasi | kirim ulang permintaan dengan kredensial yang benar |
| 403 Forbidden | pemanggil sudah diverifikasi, tetapi tidak berhak | jangan mengulang kredensial; masalahnya adalah hak akses |
Nama resmi 401 memang menyesatkan: Unauthorized sebenarnya berarti unauthenticated. Respons 401 semestinya menyertakan header WWW-Authenticate yang memberi tahu klien cara mengautentikasi — untuk skema token nilainya misalnya Bearer realm="api". Sebaliknya 403 tidak meminta kredensial baru: kredensial yang ada sudah cukup mengenali pemanggil, yang kurang adalah haknya.
Simpan Hash, Bukan Token Asli #
Server tidak perlu menyimpan token asli untuk memverifikasinya. Pola yang lebih aman: saat penerbitan, hitung sidik jari SHA-256 token lalu simpan hash-nya; saat verifikasi, hitung hash token yang masuk dan cari padanannya di tabel. Kebocoran memori atau cadangan database kemudian hanya membocorkan hash yang tidak bisa dipakai sebagai kredensial.
API Key pada X-API-Key #
Untuk integrasi antar server, alternatif yang lebih sederhana adalah API key: string panjang yang dikirim pada header custom X-API-Key. API key mudah diterbitkan per pelanggan dan mudah diverifikasi. Bearer token lebih cocok untuk pengguna akhir karena mendukung kedaluwarsa dan pencabutan bertingkat melalui OAuth 2.0.
Session, Token, dan Statelessness #
Session klasik menyimpan status login di server (peta ID session ke pengguna) dan menyerahkan ID itu kepada klien lewat cookie. Token bersifat stateless: klien menyimpan kredensial dan server memverifikasi tanpa menyimpan sesi — cocok dengan karakter REST. JWT membawa klaim dan tanda tangan di dalam tokennya; pembahasan mendalam tersedia pada chapter Token-based Authentication .
Program: Tujuh Skenario Autentikasi #
Program auth-demo.go memasang middleware autentikasi di depan dua endpoint. Tujuh permintaan menampilkan seluruh cabang: tanpa kredensial, token sah, token palsu, skema keliru, API key, lalu 403 dan 200 pada endpoint khusus admin. Token demo berupa konstanta konfigurasi agar ringkas; produksi seharusnya menerbitkan token acak.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
// Identitas menjawab "siapa pemanggilnya" setelah autentikasi berhasil.
type Identitas struct {
Nama string
Peran string
}
// kunciKonteks memisahkan namespace nilai context milik program ini.
type kunciKonteks struct{}
// profilPublik adalah bentuk identitas yang dikirim ke klien.
type profilPublik struct {
Nama string `json:"nama"`
Peran string `json:"peran"`
}
type profilBody struct {
Data profilPublik `json:"data"`
}
// Autentikasi menyimpan hash token sebagai pengganti token asli,
// sehingga kebocoran isi memori server tidak langsung membocorkan kredensial.
type Autentikasi struct {
pemegang map[string]Identitas
}
// baruAutentikasi meng-hash seluruh token dari konfigurasi sekali saat startup.
func baruAutentikasi(kredensial map[string]Identitas) *Autentikasi {
a := &Autentikasi{pemegang: make(map[string]Identitas, len(kredensial))}
for token, id := range kredensial {
a.pemegang[hashToken(token)] = id
}
return a
}
// hashToken menghitung sidik jari SHA-256 dari sebuah token.
func hashToken(token string) string {
jumlah := sha256.Sum256([]byte(token))
return hex.EncodeToString(jumlah[:])
}
// middleware memverifikasi kredensial lalu menempelkan identitas
// pada request context sebelum handler berikutnya dijalankan.
func (a *Autentikasi) middleware(berikut http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, ok := a.periksa(r)
if !ok {
// 401: klien belum terautentikasi; header ini memintanya mengirim token.
w.Header().Set("WWW-Authenticate", `Bearer realm="api"`)
tulisGalat(w, http.StatusUnauthorized, "UNAUTHENTICATED",
"lampirkan Bearer token pada header Authorization")
return
}
ctx := context.WithValue(r.Context(), kunciKonteks{}, id)
berikut.ServeHTTP(w, r.WithContext(ctx))
})
}
// periksa menerima Bearer token di Authorization atau kunci di X-API-Key.
// Kedua jenis kredensial diverifikasi lewat tabel hash yang sama.
func (a *Autentikasi) periksa(r *http.Request) (Identitas, bool) {
if kepala, token, ada := strings.Cut(r.Header.Get("Authorization"), " "); ada {
if strings.EqualFold(kepala, "Bearer") {
id, ok := a.pemegang[hashToken(strings.TrimSpace(token))]
return id, ok
}
}
if kunci := r.Header.Get("X-API-Key"); kunci != "" {
id, ok := a.pemegang[hashToken(kunci)]
return id, ok
}
return Identitas{}, false
}
// profil mengembalikan identitas pemanggil yang sudah terautentikasi.
func profil(w http.ResponseWriter, r *http.Request) {
id := r.Context().Value(kunciKonteks{}).(Identitas)
tulisJSON(w, http.StatusOK, profilBody{Data: profilPublik{Nama: id.Nama, Peran: id.Peran}})
}
// panelAdmin menunjukkan beda 403: pemanggil dikenal, tetapi perannya kurang.
func panelAdmin(w http.ResponseWriter, r *http.Request) {
id := r.Context().Value(kunciKonteks{}).(Identitas)
if id.Peran != "admin" {
tulisGalat(w, http.StatusForbidden, "FORBIDDEN", "akses panel hanya untuk admin")
return
}
tulisJSON(w, http.StatusOK, profilBody{Data: profilPublik{Nama: id.Nama, Peran: id.Peran}})
}
type galatBody struct {
Error galatRincian `json:"error"`
}
type galatRincian struct {
Code string `json:"code"`
Message string `json:"message"`
}
func tulisGalat(w http.ResponseWriter, status int, kode, pesan string) {
tulisJSON(w, status, galatBody{Error: galatRincian{Code: kode, Message: pesan}})
}
func tulisJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func main() {
autentikasi := baruAutentikasi(map[string]Identitas{
"tok-budi-7f3a": {Nama: "budi", Peran: "user"},
"tok-siti-9c2e": {Nama: "siti", Peran: "admin"},
})
mux := http.NewServeMux()
mux.HandleFunc("GET /profil", profil)
mux.HandleFunc("GET /panel-admin", panelAdmin)
server := autentikasi.middleware(mux)
// 401: tidak ada kredensial sama sekali.
kirim(server, "GET", "/profil", "", nil)
// 200: Bearer token dikenal.
kirim(server, "GET", "/profil", "", map[string]string{
"Authorization": "Bearer tok-budi-7f3a",
})
// 401: token tidak dikenal.
kirim(server, "GET", "/profil", "", map[string]string{
"Authorization": "Bearer tok-palsu",
})
// 401: skema Authorization bukan Bearer (contoh salah pakai Basic).
kirim(server, "GET", "/profil", "", map[string]string{
"Authorization": "Basic dXNlcjpwYXNzd29yZA==",
})
// 200: API key pada X-API-Key sebagai alternatif token.
kirim(server, "GET", "/profil", "", map[string]string{
"X-API-Key": "tok-siti-9c2e",
})
// 403: budi terautentikasi, tetapi panel admin bukan haknya.
kirim(server, "GET", "/panel-admin", "", map[string]string{
"Authorization": "Bearer tok-budi-7f3a",
})
// 200: siti adalah admin.
kirim(server, "GET", "/panel-admin", "", map[string]string{
"Authorization": "Bearer tok-siti-9c2e",
})
}
// kirim mengeksekusi satu permintaan lalu mencetak status, header
// WWW-Authenticate, dan body — semuanya lewat httptest tanpa jaringan.
func kirim(server http.Handler, metode, target, body string, header map[string]string) {
var pembaca io.Reader
if body != "" {
pembaca = strings.NewReader(body)
}
req := httptest.NewRequest(metode, target, pembaca)
for k, v := range header {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
server.ServeHTTP(rec, req)
fmt.Printf("%-5s %-12s -> %d", metode, target, rec.Code)
if tanya := rec.Header().Get("WWW-Authenticate"); tanya != "" {
fmt.Printf(" (WWW-Authenticate: %s)", tanya)
}
fmt.Println()
if rec.Body.Len() > 0 {
fmt.Printf(" %s", rec.Body.String())
}
}Output yang Dihasilkan:
GET /profil -> 401 (WWW-Authenticate: Bearer realm="api")
{"error":{"code":"UNAUTHENTICATED","message":"lampirkan Bearer token pada header Authorization"}}
GET /profil -> 200
{"data":{"nama":"budi","peran":"user"}}
GET /profil -> 401 (WWW-Authenticate: Bearer realm="api")
{"error":{"code":"UNAUTHENTICATED","message":"lampirkan Bearer token pada header Authorization"}}
GET /profil -> 401 (WWW-Authenticate: Bearer realm="api")
{"error":{"code":"UNAUTHENTICATED","message":"lampirkan Bearer token pada header Authorization"}}
GET /profil -> 200
{"data":{"nama":"siti","peran":"admin"}}
GET /panel-admin -> 403
{"error":{"code":"FORBIDDEN","message":"akses panel hanya untuk admin"}}
GET /panel-admin -> 200
{"data":{"nama":"siti","peran":"admin"}}Mari kita telaah baris demi baris program di atas:
- Baris 34-38 (
type Autentikasi): hanya menyimpan peta hash token ke identitas — token asli tidak pernah tersimpan. - Baris 40-47 (
baruAutentikasi): seluruh token dari konfigurasi di-hash satu kali saat startup. - Baris 49-53 (
hashToken):sha256.Sum256menghasilkan 32 byte sidik jari yang di-encode heksadesimal agar layak menjadi kunci map. - Baris 57-70 (
middleware): kegagalan dijawab 401 dengan headerWWW-Authenticate; keberhasilan menempelkan identitas pada request context sebelum handler berikutnya. - Baris 74-86 (
periksa): dua pintu diverifikasi lewat tabel hash yang sama — skemaBearerpadaAuthorization(baris 75-79) atauX-API-Key(baris 81-84). - Baris 95-102 (
panelAdmin): 403 muncul setelah identitas sah tetapi peran kurang — bandingkan dengan 401 pada skenario pertama. - Baris 124-132 (
main): seluruh mux dibungkus middleware sehingga tidak ada endpoint yang lolos tanpa verifikasi. - Baris 134-165 (tujuh permintaan): tiap skenario memperagakan satu cabang; helper
kirimmencetak status, header, dan body secara deterministik.
Empat skenario pertama pada output memakai endpoint yang sama dengan jawaban berbeda — perbedaannya murni berasal dari kredensial pada header. Authentication kini memberi tahu server siapa pemanggilnya; pertanyaan apa saja yang boleh dilakukan pemanggil menjadi materi authorization pada chapter berikutnya.
Referensi #
- Dokumentasi package net/http — handler, middleware, dan status 401/403.
- Dokumentasi package crypto/sha256 — hash token satu arah.
- Dokumentasi package httptest — eksekusi permintaan tanpa jaringan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
auth-demo.go
Unduh berkaspackage main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
// Identitas menjawab "siapa pemanggilnya" setelah autentikasi berhasil.
type Identitas struct {
Nama string
Peran string
}
// kunciKonteks memisahkan namespace nilai context milik program ini.
type kunciKonteks struct{}
// profilPublik adalah bentuk identitas yang dikirim ke klien.
type profilPublik struct {
Nama string `json:"nama"`
Peran string `json:"peran"`
}
type profilBody struct {
Data profilPublik `json:"data"`
}
// Autentikasi menyimpan hash token sebagai pengganti token asli,
// sehingga kebocoran isi memori server tidak langsung membocorkan kredensial.
type Autentikasi struct {
pemegang map[string]Identitas
}
// baruAutentikasi meng-hash seluruh token dari konfigurasi sekali saat startup.
func baruAutentikasi(kredensial map[string]Identitas) *Autentikasi {
a := &Autentikasi{pemegang: make(map[string]Identitas, len(kredensial))}
for token, id := range kredensial {
a.pemegang[hashToken(token)] = id
}
return a
}
// hashToken menghitung sidik jari SHA-256 dari sebuah token.
func hashToken(token string) string {
jumlah := sha256.Sum256([]byte(token))
return hex.EncodeToString(jumlah[:])
}
// middleware memverifikasi kredensial lalu menempelkan identitas
// pada request context sebelum handler berikutnya dijalankan.
func (a *Autentikasi) middleware(berikut http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, ok := a.periksa(r)
if !ok {
// 401: klien belum terautentikasi; header ini memintanya mengirim token.
w.Header().Set("WWW-Authenticate", `Bearer realm="api"`)
tulisGalat(w, http.StatusUnauthorized, "UNAUTHENTICATED",
"lampirkan Bearer token pada header Authorization")
return
}
ctx := context.WithValue(r.Context(), kunciKonteks{}, id)
berikut.ServeHTTP(w, r.WithContext(ctx))
})
}
// periksa menerima Bearer token di Authorization atau kunci di X-API-Key.
// Kedua jenis kredensial diverifikasi lewat tabel hash yang sama.
func (a *Autentikasi) periksa(r *http.Request) (Identitas, bool) {
if kepala, token, ada := strings.Cut(r.Header.Get("Authorization"), " "); ada {
if strings.EqualFold(kepala, "Bearer") {
id, ok := a.pemegang[hashToken(strings.TrimSpace(token))]
return id, ok
}
}
if kunci := r.Header.Get("X-API-Key"); kunci != "" {
id, ok := a.pemegang[hashToken(kunci)]
return id, ok
}
return Identitas{}, false
}
// profil mengembalikan identitas pemanggil yang sudah terautentikasi.
func profil(w http.ResponseWriter, r *http.Request) {
id := r.Context().Value(kunciKonteks{}).(Identitas)
tulisJSON(w, http.StatusOK, profilBody{Data: profilPublik{Nama: id.Nama, Peran: id.Peran}})
}
// panelAdmin menunjukkan beda 403: pemanggil dikenal, tetapi perannya kurang.
func panelAdmin(w http.ResponseWriter, r *http.Request) {
id := r.Context().Value(kunciKonteks{}).(Identitas)
if id.Peran != "admin" {
tulisGalat(w, http.StatusForbidden, "FORBIDDEN", "akses panel hanya untuk admin")
return
}
tulisJSON(w, http.StatusOK, profilBody{Data: profilPublik{Nama: id.Nama, Peran: id.Peran}})
}
type galatBody struct {
Error galatRincian `json:"error"`
}
type galatRincian struct {
Code string `json:"code"`
Message string `json:"message"`
}
func tulisGalat(w http.ResponseWriter, status int, kode, pesan string) {
tulisJSON(w, status, galatBody{Error: galatRincian{Code: kode, Message: pesan}})
}
func tulisJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func main() {
autentikasi := baruAutentikasi(map[string]Identitas{
"tok-budi-7f3a": {Nama: "budi", Peran: "user"},
"tok-siti-9c2e": {Nama: "siti", Peran: "admin"},
})
mux := http.NewServeMux()
mux.HandleFunc("GET /profil", profil)
mux.HandleFunc("GET /panel-admin", panelAdmin)
server := autentikasi.middleware(mux)
// 401: tidak ada kredensial sama sekali.
kirim(server, "GET", "/profil", "", nil)
// 200: Bearer token dikenal.
kirim(server, "GET", "/profil", "", map[string]string{
"Authorization": "Bearer tok-budi-7f3a",
})
// 401: token tidak dikenal.
kirim(server, "GET", "/profil", "", map[string]string{
"Authorization": "Bearer tok-palsu",
})
// 401: skema Authorization bukan Bearer (contoh salah pakai Basic).
kirim(server, "GET", "/profil", "", map[string]string{
"Authorization": "Basic dXNlcjpwYXNzd29yZA==",
})
// 200: API key pada X-API-Key sebagai alternatif token.
kirim(server, "GET", "/profil", "", map[string]string{
"X-API-Key": "tok-siti-9c2e",
})
// 403: budi terautentikasi, tetapi panel admin bukan haknya.
kirim(server, "GET", "/panel-admin", "", map[string]string{
"Authorization": "Bearer tok-budi-7f3a",
})
// 200: siti adalah admin.
kirim(server, "GET", "/panel-admin", "", map[string]string{
"Authorization": "Bearer tok-siti-9c2e",
})
}
// kirim mengeksekusi satu permintaan lalu mencetak status, header
// WWW-Authenticate, dan body — semuanya lewat httptest tanpa jaringan.
func kirim(server http.Handler, metode, target, body string, header map[string]string) {
var pembaca io.Reader
if body != "" {
pembaca = strings.NewReader(body)
}
req := httptest.NewRequest(metode, target, pembaca)
for k, v := range header {
req.Header.Set(k, v)
}
rec := httptest.NewRecorder()
server.ServeHTTP(rec, req)
fmt.Printf("%-5s %-12s -> %d", metode, target, rec.Code)
if tanya := rec.Header().Get("WWW-Authenticate"); tanya != "" {
fmt.Printf(" (WWW-Authenticate: %s)", tanya)
}
fmt.Println()
if rec.Body.Len() > 0 {
fmt.Printf(" %s", rec.Body.String())
}
}