Skip to content

JWT

JWT è diventato uno dei principali metodi di autenticazione per le comunicazioni server moderne con caratteristiche di leggerezza e statelessness.

Introduzione

JSON Web Tokens, per ulteriori informazioni su JWT è possibile visitare jwt.io. È un modo aperto, sicuro e compatto per trasmettere informazioni tra le parti del servizio utilizzando oggetti JSON come supporto. Le sue caratteristiche includono elevata sicurezza, prevenzione della manomissione dei contenuti e basso consumo.

Struttura

Nello standard RFC, JWT è composto dalle seguenti tre parti:

  • Header (intestazione)
  • Payload (carico utile)
  • Signature (firma)

Ogni parte è separata da un punto . per formare una stringa. Il formato è header.payload.signature, questa è la struttura standard di un token JWT. Di seguito verrà spiegata la funzione di ciascuna struttura.

TIP

Va notato che base64 e base64URL non sono lo stesso tipo di codifica; quest'ultima è compatibile con gli URL web e li converte.

L'header dichiara solo alcune informazioni di base, solitamente composte da due parti: il tipo di token e l'algoritmo di crittografia utilizzato per la firma, come segue:

json
{
  "alg": "HS256",
  "typ": "JWT"
}

Le informazioni sopra indicano approssimativamente che il tipo di token è JWT e l'algoritmo di crittografia utilizzato per la parte di firma è HS256. Infine, l'oggetto JSON viene codificato in una stringa tramite Base64Url, che costituisce l'header JWT.

Payload

La seconda parte di JWT è la parte payload, che contiene principalmente le dichiarazioni (claims). Le dichiarazioni sono solitamente dati su un'entità, come un utente. Esistono tre tipi di dichiarazioni:

  • registered: I Registered claims rappresentano alcune dichiarazioni predefinite. Non sono obbligatorie ma sono comunque raccomandate, ad esempio: iss (emittente), exp (tempo di scadenza), aud (destinatario).
  • public: I Public claims possono essere definiti liberamente dagli utenti di JWT, meglio evitare conflitti con altre dichiarazioni.
  • private claims: Questa parte delle dichiarazioni è anche personalizzata, solitamente utilizzata per condividere informazioni tra le parti del servizio.

Un esempio di payload è il seguente:

json
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

L'oggetto JSON verrà codificato in una stringa tramite Base64Url, formando così la seconda parte di JWT.

DANGER

Sebbene la parte payload sia anche protetta e abbia prevenzione di manomissione, questa parte è pubblicamente leggibile, quindi non inserire informazioni sensibili all'interno di JWT.

Signature

Dopo aver ottenuto l'header codificato e il payload codificato, è possibile utilizzare l'algoritmo di firma indicato nell'header per crittografare e firmare il contenuto in base alle prime due parti più una chiave segreta. Quindi, una volta che il contenuto di JWT subisce qualsiasi modifica, la firma ottenuta durante la decrittografia sarà diversa. Se si utilizza una chiave privata, è anche possibile verificare l'emittente di JWT.

sign = HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Ad esempio:

Header
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload
{
  "alg": "HS256",
  "typ": "JWT"
}

Verify Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your secret
)

L'output finale è una stringa composta da tre stringhe base64Url separate da ., approssimativamente come segue:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ

Come Funziona

Nell'autenticazione, quando un utente esegue correttamente l'accesso con le credenziali, verrà restituito un JSON Web Token. Poiché il token è una credenziale, deve essere gestito con molta attenzione per prevenire problemi di sicurezza. In generale, il token non dovrebbe essere conservato più a lungo del necessario. Quando l'utente desidera accedere a route e risorse protette, deve includere il token nella richiesta, solitamente nell'header Authorization con lo schema Bearer, come segue:

Authorization: Bearer <token>

Dopo aver ricevuto il JWT, il server ne verificherà la validità, ad esempio se il contenuto è stato manomesso o se il token è scaduto. Se la verifica ha successo, sarà possibile accedere alle risorse. Sebbene JWT possa trasportare alcune informazioni di base, si consiglia comunque di non rendere le informazioni troppo grandi.

Libreria JWT

Repository ufficiale: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go

Documentazione ufficiale: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages

Questa libreria supporta l'analisi, la verifica, la generazione e la firma di JWT. Gli algoritmi di firma attualmente supportati sono HMAC SHA, RSA, RSA-PSS e ECDSA, ma è anche possibile aggiungere i propri hook.

Installazione

go get -u github.com/golang-jwt/jwt/v4

Importazione

go
import "github.com/golang-jwt/jwt/v4"

Scelta dell'Algoritmo di Firma

Esistono diversi algoritmi di firma disponibili. Prima dell'uso, è necessario comprendere le differenze tra di essi per scegliere meglio l'algoritmo di firma. La differenza principale è tra crittografia simmetrica e crittografia asimmetrica.

L'algoritmo di crittografia simmetrica più semplice è HSA, che consente a qualsiasi []byte di essere utilizzato come chiave valida, quindi la velocità di calcolo è leggermente più veloce. L'efficienza degli algoritmi di crittografia simmetrica è massima quando entrambe le parti produttrici e consumatrici sono attendibili. Tuttavia, poiché la firma e la verifica utilizzano la stessa chiave, non è facile distribuire le chiavi per la verifica, dato che la chiave di firma è la stessa. Se la chiave di firma viene compromessa, la sicurezza di JWT diventa priva di senso.

I metodi di firma asimmetrica, come RSA, utilizzano chiavi diverse per firmare e verificare il token, rendendo possibile generare token con una chiave privata e consentendo a chiunque abbia la chiave pubblica di verificare l'accesso.

Diversi algoritmi di firma richiedono diversi tipi di chiavi. Di seguito sono riportati alcuni tipi di algoritmi di firma comuni:

  • HMAC: Crittografia simmetrica, richiede un valore di tipo []byte per la firma e la verifica. (HS256,HS384,HS512)
  • RSA: Crittografia asimmetrica, richiede un valore di tipo *rsa.PrivateKey per la firma e un valore di tipo *rsa.PublicKey per la verifica. (RS256,RS384,RS512)
  • ECDSA: Crittografia asimmetrica, richiede un valore di tipo *ecdsa.PrivateKey per la firma e un valore di tipo *ecdsa.PublicKey per la verifica. (ES256,ES384,ES512)
  • EdDSA: Crittografia asimmetrica, richiede un valore di tipo ed25519.PrivateKey per la firma e un valore di tipo ed25519.PublicKey per la verifica. (Ed25519)

Esempi

Di seguito verranno mostrati alcuni esempi sulla creazione e firma di jwt, nonché sull'analisi e verifica.

go
type Token struct {
  Raw       string                 // Stringa Token originale, questo campo viene popolato quando si inizia ad analizzare
  Method    SigningMethod          // Metodo utilizzato per la firma
  Header    map[string]interface{} // Parte header di JWT
  Claims    Claims                 // Parte payload di JWT
  Signature string                 // Parte firma di JWT, questo campo viene popolato quando si inizia ad analizzare
  Valid     bool                   // JWT è valido
}

La struttura Token rappresenta un Token JWT. L'uso dei campi dipende principalmente da come JWT viene creato/firmato o analizzato/verificato.

go
type RegisteredClaims struct {
   // la claim `iss` (Issuer). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
   Issuer string `json:"iss,omitempty"`

   // la claim `sub` (Subject). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
   Subject string `json:"sub,omitempty"`

   // la claim `aud` (Audience). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
   Audience ClaimStrings `json:"aud,omitempty"`

   // la claim `exp` (Expiration Time). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
   ExpiresAt *NumericDate `json:"exp,omitempty"`

   // la claim `nbf` (Not Before). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
   NotBefore *NumericDate `json:"nbf,omitempty"`

   // la claim `iat` (Issued At). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
   IssuedAt *NumericDate `json:"iat,omitempty"`

   // la claim `jti` (JWT ID). Vedi https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
   ID string `json:"jti,omitempty"`
}

Queste sono le Claims predefinite fornite dalla libreria, che possono essere utilizzate appropriatamente per raggiungere gli obiettivi desiderati.

Esempio 1. Creazione e Firma con HMAC

go
func TestHmac(t *testing.T) {
   // Il tipo di chiave per hmac è un array di byte
   secret := []byte("my secret")
   // Utilizzo dell'algoritmo HS256, jwt.MapClaims è il payload
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "id":   123456,
      "name": "jack",
   })
   fmt.Printf("%+v\n", *token)
   // Firma
   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>

Esempio 2. Utilizzo di Claims Predefinite

go
mySigningKey := []byte("AllYourBase")

// Creazione 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>

Esempio 3. Claims Personalizzate

go
type MyClaims struct {
   User string `json:"user"`
   jwt.RegisteredClaims
}

func TestCustomClaims(t *testing.T) {
   // Creazione chiave
   secret := []byte("my secret")
   // Creazione 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",
      },
   }
   // Creazione Token
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   // Firma
   signedString, err := token.SignedString(secret)
   fmt.Println(signedString, err)
}

Output:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>

TIP

Quando si incorporano Claims standard in Claims personalizzate, è necessario assicurarsi che:

  1. Le Claims standard incorporate siano di tipo non puntatore

  2. Se sono di tipo puntatore, è meglio assicurarsi di allocare memoria appropriata prima di passarle, altrimenti si verificherà un panic.

Esempio 4. Analisi e Verifica Token HMAC

go
func TestParse(t *testing.T) {
   secret := []byte("my secret")
   // Supponiamo che un token sia stato creato e firmato con l'algoritmo HS256
   tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"

   // Passa la stringa del token e la funzione hook di verifica, il valore di ritorno è una struttura Token
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
      // Verifica se l'algoritmo di firma corrisponde
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("Algoritmo di firma non corrispondente: %s", token.Header["alg"])
      }

      // Restituisce la chiave di verifica
      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]

Esempio 5. Gestione Errori

go
func TestProcess(t *testing.T) {
   secret := []byte("my secret")
   // Supponiamo che un token sia stato creato e firmato con l'algoritmo HS256
   tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"

   // Passa la stringa del token e la funzione hook di verifica, il valore di ritorno è una struttura Token
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
      // Verifica se l'algoritmo di firma corrisponde
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("Algoritmo di firma non corrispondente: %s", token.Header["alg"])
      }
      // Restituisce la chiave di verifica
      return secret, nil
   })

   if token.Valid {
      fmt.Println("token valido")
   } else if errors.Is(err, jwt.ErrTokenMalformed) {
      fmt.Println("La stringa passata non è nemmeno un token...")
   } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
      fmt.Println("Il token è scaduto o non è ancora valido")
   } else {
      fmt.Println("Elaborazione del token anomala...")
   }
}

Output:

token valido

Esempio 6. Analisi Claims Personalizzate

Se si utilizzano Claims personalizzate durante la creazione del Token, durante l'analisi, se si desidera che le Claims possano essere convertite direttamente in Claims personalizzate invece di map, è necessario passare Claims personalizzate.

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"})) // Utilizzo option per la verifica

    // 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 }}

Esempio 7. Firma e Analisi RSA

RSA è comunemente utilizzato nell'architettura distribuita. Il processo approssimativo è il seguente:

  1. Il centro di autenticazione crea una coppia di chiavi, utilizza la chiave privata per firmare il jwt, il jwt viene restituito al client, la chiave pubblica è detenuta dal servizio business
  2. Il client invia una richiesta al servizio business con il jwt, il modulo business analizza il jwt utilizzando la chiave pubblica senza bisogno di accedere al centro di autenticazione
  3. Se l'autenticazione ha successo, restituisce le informazioni business
  4. Se l'autenticazione fallisce, restituisce informazioni di fallimento
go
func TestRsa(t *testing.T) {

   // Creazione coppia di chiavi
   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)
   // Crittografia con chiave privata
   signedString, err := token.SignedString(privateKey)

   fmt.Println(signedString, err)

   // Decrittografia con chiave pubblica
   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