Skip to content

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 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:

json
{
  "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 claims mewakili 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 claims dapat 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:

json
{
  "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_xSJyQQ

Cara 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/v4

Impor

go
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 []byte untuk tanda tangan dan verifikasi. (HS256,HS384,HS512)
  • RSA: Enkripsi asimetris memerlukan nilai jenis *rsa.PrivateKey untuk tanda tangan dan nilai jenis *rsa.PublicKey untuk verifikasi.(RS256,RS384,RS512)
  • ECDSA: Enkripsi asimetris memerlukan nilai jenis *ecdsa.PrivateKey untuk tanda tangan dan nilai jenis *ecdsa.PublicKey untuk verifikasi.(ES256,ES384,ES512)
  • EdDSA: Enkripsi asimetris memerlukan nilai jenis ed25519.PrivateKey untuk tanda tangan dan nilai jenis ed25519.PublicKey untuk verifikasi.(Ed25519)

Contoh

Berikut akan ditampilkan beberapa contoh tentang pembuatan dan penandatanganan jwt serta parsing dan validasi.

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

go
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

go
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

go
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

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

  1. Standard Claims yang disematkan adalah jenis non-pointer

  2. Jika jenis pointer sebaiknya pastikan untuk mengalokasikan memori yang sesuai sebelum mengirimkannya jika tidak akan terjadi panic.

Contoh 4. Parsing dan Verifikasi Token HMAC

go
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

go
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 valid

Contoh 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.

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

  1. Pusat autentikasi membuat pasangan kunci menggunakan private key untuk menandatangani jwt jwt dikembalikan ke klien public key dipegang oleh layanan bisnis
  2. Klien membawa jwt untuk melakukan request ke layanan bisnis modul bisnis menggunakan public key untuk mengurai jwt tanpa perlu mengakses pusat autentikasi
  3. Jika autentikasi berhasil mengembalikan informasi bisnis
  4. Jika autentikasi gagal mengembalikan informasi kegagalan
go
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)
   }
}

Golang by www.golangdev.cn edit