JWT
JWT telah menjadi salah satu metode utama autentikasi komunikasi server modern dengan karakteristik ringan dan stateless.
Pengenalan
JWT kepanjangan dari JSON Web Tokens untuk detail lebih lanjut tentang JWT dapat dilihat di jwt.io ini adalah cara yang terbuka aman dan kompak untuk mentransmisikan informasi antara dua pihak dengan objek JSON sebagai media karakteristiknya adalah keamanan tinggi anti-tampering konten dan konsumsi rendah.
Struktur
Dalam standar RFC JWT terdiri dari tiga bagian berikut:
- Header (Kepala)
- Payload (Beban)
- Signature (Tanda Tangan)
Kemudian setiap bagian dipisahkan dengan titik . dan akhirnya membentuk sebuah string dengan format header.payload.signature ini adalah struktur standar token JWT selanjutnya akan dijelaskan fungsi dari setiap struktur.
TIP
Perlu dicatat bahwa base64 dan base64URL bukan jenis encoding yang sama yang terakhir kompatibel dengan URL web dan melakukan escape terhadapnya.
Header
Header hanya mendeklarasikan beberapa informasi dasar biasanya terdiri dari dua bagian jenis token dan algoritma enkripsi yang digunakan untuk tanda tangan misalnya seperti di bawah ini:
{
"alg": "HS256",
"typ": "JWT"
}Informasi di atas kira-kira adalah jenis token adalah JWT algoritma enkripsi yang digunakan untuk bagian tanda tangan adalah HS256 kemudian objek JSON di-encoding menjadi string melalui Base64Url string ini adalah header JWT.
Payload
Bagian kedua JWT adalah bagian payload yang terutama berisi bagian claims claims biasanya adalah data tentang entitas seperti pengguna. Ada tiga jenis claims:
registered:Registered claimsmewakili beberapa claims yang telah ditentukan sebelumnya beberapa tidak wajib digunakan tetapi tetap direkomendasikan misalnya:iss(issuer penerbit)exp(expiration time waktu kedaluwarsa)aud(audience audiens).public:Public claimsdapat didefinisikan secara bebas oleh pengguna JWT yang terbaik adalah menghindari konflik dengan claims lainnya.private claims: Bagian claims ini juga didefinisikan secara kustom biasanya digunakan untuk berbagi informasi antara dua pihak server.
Contoh payload adalah sebagai berikut:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}Objek JSON ini akan di-encoding menjadi string melalui Base64Url sehingga membentuk bagian kedua JWT.
DANGER
Meskipun bagian payload juga dilindungi dan memiliki anti-tampering bagian ini dapat dibaca secara publik jadi jangan menyimpan informasi sensitif di dalam JWT.
Signature
Setelah mendapatkan header dan payload yang telah di-encoding dapat melakukan enkripsi tanda tangan melalui algoritma tanda tangan yang ditunjukkan oleh header sesuai dengan konten dari dua bagian pertama ditambah dengan secret key jadi sekali konten JWT berubah tanda tangan yang diperoleh saat dekripsi akan berbeda dan jika menggunakan private key juga dapat memverifikasi penerbit JWT.
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)Misalnya contoh berikut:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)Hasil akhir yang diperoleh adalah string yang terdiri dari tiga string base64Url yang dipisahkan oleh . kira-kira seperti ini:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQCara Kerja
Dalam autentikasi ketika pengguna berhasil login dengan kredensial akan mengembalikan JSON Web token. Karena token adalah kredensial harus sangat berhati-hati untuk mencegah masalah keamanan. Secara umum token tidak boleh disimpan lebih lama dari yang diperlukan. Kemudian kapan pun pengguna ingin mengakses route dan resource yang dilindungi saat melakukan request harus membawa token biasanya di header Authorization dalam Bearer schema misalnya seperti di bawah ini:
Authorization: Bearer <token>Server setelah menerima JWT akan memverifikasi validitasnya misalnya konten telah diubah token telah kedaluwarsa dll jika verifikasi berhasil dapat mengakses resource dengan lancar. Meskipun JWT dapat membawa beberapa informasi dasar tetap disarankan agar informasi tidak terlalu besar.
Library JWT
Repositori Resmi: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
Dokumentasi Resmi: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
Library ini mendukung parsing dan validasi serta pembuatan dan penandatanganan JWT. Algoritma tanda tangan yang saat ini didukung adalah HMAC SHA RSA RSA-PSS dan ECDSA namun juga dapat menambahkan hook sendiri.
Instalasi
go get -u github.com/golang-jwt/jwt/v4Impor
import "github.com/golang-jwt/jwt/v4"Memilih Algoritma Tanda Tangan
Ada beberapa algoritma tanda tangan yang tersedia sebelum digunakan sebaiknya pahami perbedaan di antara mereka agar dapat memilih algoritma tanda tangan yang lebih baik perbedaan terbesar di antara mereka adalah enkripsi simetris dan enkripsi asimetris.
Algoritma enkripsi simetris paling sederhana HSA memungkinkan setiap []byte dapat digunakan sebagai kunci valid sehingga kecepatan komputasi sedikit lebih cepat. Ketika produsen dan konsumen keduanya dapat dipercaya efisiensi algoritma enkripsi simetris adalah yang tertinggi. Namun karena tanda tangan dan verifikasi menggunakan kunci yang sama tidak mudah untuk mendistribusikan kunci yang digunakan untuk verifikasi lagipula kunci untuk tanda tangan juga sama jika kunci tanda tangan bocor maka keamanan JWT menjadi tidak berarti.
Metode tanda tangan enkripsi asimetris seperti RSA menggunakan kunci yang berbeda untuk menandatangani dan memverifikasi token ini memungkinkan pembuatan token dengan private key dan juga memungkinkan siapa pun yang memiliki public key untuk melakukan verifikasi akses.
Berbagai algoritma tanda tangan memerlukan jenis kunci yang berbeda berikut adalah beberapa jenis algoritma tanda tangan umum:
HMAC: Enkripsi simetris memerlukan nilai jenis[]byteuntuk tanda tangan dan verifikasi. (HS256,HS384,HS512)RSA: Enkripsi asimetris memerlukan nilai jenis*rsa.PrivateKeyuntuk tanda tangan dan nilai jenis*rsa.PublicKeyuntuk verifikasi.(RS256,RS384,RS512)ECDSA: Enkripsi asimetris memerlukan nilai jenis*ecdsa.PrivateKeyuntuk tanda tangan dan nilai jenis*ecdsa.PublicKeyuntuk verifikasi.(ES256,ES384,ES512)EdDSA: Enkripsi asimetris memerlukan nilai jenised25519.PrivateKeyuntuk tanda tangan dan nilai jenised25519.PublicKeyuntuk verifikasi.(Ed25519)
Contoh
Berikut akan ditampilkan beberapa contoh tentang pembuatan dan penandatanganan jwt serta parsing dan validasi.
type Token struct {
Raw string // String Token asli field ini diisi saat mulai parsing
Method SigningMethod // Metode yang digunakan untuk tanda tangan
Header map[string]interface{} // Bagian header JWT
Claims Claims // Bagian payload JWT
Signature string // Bagian tanda tangan JWT field ini diisi saat mulai parsing
Valid bool // Apakah JWT valid
}Struktur Token mewakili JWT Token penggunaan field terutama tergantung pada bagaimana JWT dibuat/ditandatangani atau di-parsing/diverifikasi.
type RegisteredClaims struct {
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience ClaimStrings `json:"aud,omitempty"`
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt *NumericDate `json:"exp,omitempty"`
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore *NumericDate `json:"nbf,omitempty"`
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt *NumericDate `json:"iat,omitempty"`
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
ID string `json:"jti,omitempty"`
}Ini adalah Claims yang telah ditentukan sebelumnya yang disediakan oleh library dapat digunakan sesuai kebutuhan untuk mencapai tujuan.
Contoh 1. Pembuatan dan Tanda Tangan HMAC
func TestHmac(t *testing.T) {
// Jenis kunci hmac adalah array byte
secret := []byte("my secret")
// Menggunakan algoritma HS256 jwt.MapClaims adalah payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// Tanda tangan
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Output:
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>Contoh 2. Menggunakan Registered Claims
mySigningKey := []byte("AllYourBase")
// Membuat Claims
claims := &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)Output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>Contoh 3. Custom Claims
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// Membuat kunci
secret := []byte("my secret")
// Membuat Claims
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
// Membuat Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Tanda tangan
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Output:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
Ketika menyematkan Standard Claims dalam Custom Claims perlu memastikan:
Standard Claims yang disematkan adalah jenis non-pointer
Jika jenis pointer sebaiknya pastikan untuk mengalokasikan memori yang sesuai sebelum mengirimkannya jika tidak akan terjadi panic.
Contoh 4. Parsing dan Verifikasi Token HMAC
func TestParse(t *testing.T) {
secret := []byte("my secret")
// Asumsikan token dibuat dan ditandatangani menggunakan algoritma HS256
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Masukkan string token dan fungsi hook verifikasi nilai kembalian adalah struktur Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifikasi apakah algoritma tanda tangan cocok
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Algoritma tanda tangan tidak cocok: %s", token.Header["alg"])
}
// Mengembalikan kunci verifikasi
return secret, nil
})
if err != nil {
fmt.Println(token, err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Output:
map[id:123456 name:jack]Contoh 5. Penanganan Error
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// Asumsikan token dibuat dan ditandatangani menggunakan algoritma HS256
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Masukkan string token dan fungsi hook verifikasi nilai kembalian adalah struktur Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verifikasi apakah algoritma tanda tangan cocok
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Algoritma tanda tangan tidak cocok: %s", token.Header["alg"])
}
// Mengembalikan kunci verifikasi
return secret, nil
})
if token.Valid {
fmt.Println("token valid")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("String yang dimasukkan bahkan bukan token...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("token telah kedaluwarsa atau belum berlaku")
} else {
fmt.Println("Pemrosesan token abnormal...")
}
}Output:
token validContoh 6. Parsing Custom Claims
Jika menggunakan Custom Claims saat membuat Token maka saat parsing jika ingin Claims dapat langsung dikonversi ke Custom Claims bukan map perlu mengirimkan Custom Claims.
func TestCustomClaimsParse(t *testing.T) {
secret := []byte("my secret")
tokenstring := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg4MDU2LCJuYmYiOjE2NzMwODQ0NTYsImlhdCI6MTY3MzA4NDQ1Nn0.T245aoDeL2x19X8_JZde0EmZ2TDyIgr1u3ddKFjQmgw"
token, err := jwt.ParseWithClaims(tokenstring, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"})) // Menggunakan option untuk verifikasi
// Type assertion
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Output:
&{114514 {Server [] 2023-01-07 18:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST }}Contoh 7. Tanda Tangan dan Parsing RSA
RSA banyak digunakan dalam arsitektur terdistribusi prosesnya kira-kira sebagai berikut:
- Pusat autentikasi membuat pasangan kunci menggunakan private key untuk menandatangani jwt jwt dikembalikan ke klien public key dipegang oleh layanan bisnis
- Klien membawa jwt untuk melakukan request ke layanan bisnis modul bisnis menggunakan public key untuk mengurai jwt tanpa perlu mengakses pusat autentikasi
- Jika autentikasi berhasil mengembalikan informasi bisnis
- Jika autentikasi gagal mengembalikan informasi kegagalan
func TestRsa(t *testing.T) {
// Membuat pasangan kunci
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
if err != nil {
fmt.Println(err)
return
}
// claims
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Enkripsi private key
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// Dekripsi public key
token, err = jwt.ParseWithClaims(signedString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
fmt.Println(err)
} else if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
}
}