JWT
JWT est devenu l'une des méthodes principales d'authentification de communication serveur moderne, avec des caractéristiques légères et sans état.
Introduction
JWT signifie JSON Web Tokens. Pour une présentation détaillée de JWT, vous pouvez consulter jwt.io. C'est une méthode ouverte, sécurisée et compacte pour transmettre des informations entre les parties serveur, utilisant des objets JSON comme support. Ses caractéristiques sont une sécurité élevée, un contenu protégé contre la falsification et une faible consommation.
Structure
Dans la norme RFC, JWT est composé des trois parties suivantes :
- Header (en-tête)
- Payload (charge utile)
- Signature (signature)
Chaque partie est séparée par un point ., formant finalement une chaîne. Le format est header.payload.signature, c'est la structure standard d'un token JWT. Expliquons maintenant le rôle de chaque structure.
TIP
Il est important de noter que base64 et base64URL ne sont pas les mêmes méthodes d'encodage. Ce dernier est compatible avec les URL web et a effectué des échappements.
En-tête
L'en-tête déclare simplement quelques informations de base, généralement composé de deux parties : le type de token et l'algorithme de cryptage utilisé pour la signature. Par exemple :
{
"alg": "HS256",
"typ": "JWT"
}Ces informations signifient essentiellement que le type de token est JWT et que l'algorithme de cryptage utilisé pour la signature est HS256. L'objet JSON est ensuite encodé en chaîne via Base64Url, cette chaîne constitue l'en-tête du JWT.
Charge utile
La deuxième partie du JWT est la charge utile, contenant principalement la partie des déclarations (claims). Les déclarations sont généralement des données sur une entité, comme un utilisateur. Il existe trois types de déclarations :
registered:Registered claimsreprésente certaines déclarations prédéfinies, qui ne sont pas obligatoires mais toujours recommandées, par exemple :iss(issuer, émetteur),exp(expiration time, temps d'expiration),aud(audience, public).public:Public claimspeuvent être librement définies par les utilisateurs de JWT, il est préférable d'éviter les conflits avec d'autres parties de déclaration.private claims: ces déclarations sont également personnalisées, généralement utilisées pour partager des informations entre les parties serveur.
Un exemple de charge utile :
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}Cet objet JSON sera encodé en chaîne via Base64Url, formant ainsi la deuxième partie du JWT.
DANGER
Bien que la partie charge utile soit également protégée contre la falsification, elle est publiquement lisible, donc ne stockez pas d'informations sensibles dans le JWT.
Signature
Après avoir obtenu l'en-tête et la charge utile encodés, vous pouvez utiliser l'algorithme de signature spécifié dans l'en-tête pour chiffrer et signer le contenu des deux premières parties avec une clé secrète. Ainsi, si le contenu du JWT change, la signature obtenue lors du déchiffrement sera différente. De plus, si une clé privée est utilisée, l'émetteur du JWT peut également être vérifié.
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)Par exemple :
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)Le résultat final est une chaîne composée de trois chaînes base64Url séparées par ., ressemblant à ceci :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQPrincipe de fonctionnement
Dans l'authentification, lorsqu'un utilisateur se connecte avec succès en utilisant ses identifiants, un JSON Web Token est retourné. Comme le token est un identifiant, il faut être très prudent pour éviter les problèmes de sécurité. En général, le token ne doit pas être conservé plus longtemps que nécessaire. Ensuite, chaque fois que l'utilisateur souhaite accéder à des routes ou ressources protégées, il doit porter le token lors de la requête, généralement dans le header Authorization avec le schéma Bearer, par exemple :
Authorization: Bearer <token>Après avoir reçu le JWT, le serveur vérifie sa validité, par exemple si le contenu a été falsifié, si le token a expiré, etc. Si la vérification réussit, l'accès aux ressources est autorisé. Bien que le JWT puisse contenir des informations de base, il est toujours recommandé de ne pas le rendre trop volumineux.
Bibliothèque JWT
Dépôt officiel : golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
Documentation officielle : jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
Cette bibliothèque supporte l'analyse et la vérification ainsi que la génération et la signature de JWT. Les algorithmes de signature actuellement supportés sont HMAC SHA, RSA, RSA-PSS et ECDSA, mais vous pouvez également ajouter vos propres hooks.
Installation
go get -u github.com/golang-jwt/jwt/v4Import
import "github.com/golang-jwt/jwt/v4"Choisir l'algorithme de signature
Plusieurs algorithmes de signature sont disponibles. Avant de les utiliser, vous devriez comprendre leurs différences pour mieux choisir. La principale différence réside dans le chiffrement symétrique et le chiffrement asymétrique.
L'algorithme de chiffrement symétrique le plus simple, HSA, permet à n'importe quel []byte d'être utilisé comme clé valide, donc le calcul est un peu plus rapide. L'efficacité des algorithmes symétriques est maximale lorsque le producteur et le consommateur sont tous deux de confiance. Cependant, comme la même clé est utilisée pour la signature et la vérification, il est difficile de distribuer la clé de vérification, car la clé de signature est la même. Si la signature fuite, la sécurité du JWT devient inutile.
Les méthodes de signature asymétrique, comme RSA, utilisent différentes clés pour signer et vérifier le token. Cela permet de générer des tokens avec une clé privée tout en permettant à quiconque possède la clé publique de vérifier normalement.
Les différents algorithmes de signature nécessitent des types de clés différents. Voici les types pour quelques algorithmes courants :
HMAC: chiffrement symétrique, nécessite une valeur de type[]bytepour la signature et la vérification. (HS256,HS384,HS512)RSA: chiffrement asymétrique, nécessite une valeur de type*rsa.PrivateKeypour la signature et*rsa.PublicKeypour la vérification. (RS256,RS384,RS512)ECDSA: chiffrement asymétrique, nécessite une valeur de type*ecdsa.PrivateKeypour la signature et*ecdsa.PublicKeypour la vérification. (ES256,ES384,ES512)EdDSA: chiffrement asymétrique, nécessite une valeur de typeed25519.PrivateKeypour la signature eted25519.PublicKeypour la vérification. (Ed25519)
Exemples
Voici quelques exemples démontrant la création et la signature de JWT, ainsi que l'analyse et la vérification.
type Token struct {
Raw string // Chaîne brute du token, remplie lors de l'analyse
Method SigningMethod // Méthode utilisée pour la signature
Header map[string]interface{} // Partie header du JWT
Claims Claims // Partie payload du JWT
Signature string // Partie signature du JWT, remplie lors de l'analyse
Valid bool // Si le JWT est valide
}La structure Token représente un JWT Token. L'utilisation des champs dépend principalement de la façon dont le JWT est créé/signé ou analysé/vérifié.
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"`
}Ce sont les Claims prédéfinis fournis par la bibliothèque, que vous pouvez utiliser selon vos besoins.
Exemple 1 : Création et signature HMAC
func TestHmac(t *testing.T) {
// Le type de clé hmac est un tableau d'octets
secret := []byte("my secret")
// Utilise l'algorithme HS256, jwt.MapClaims est le payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// Signature
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Sortie :
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>Exemple 2 : Utilisation des Claims prédéfinis
mySigningKey := []byte("AllYourBase")
// Créer les 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)Sortie :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>Exemple 3 : Claims personnalisés
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// Créer la clé
secret := []byte("my secret")
// Créer les 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",
},
}
// Créer le Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Signature
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}Sortie :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
Lorsque vous intégrez des Claims standard dans des Claims personnalisées, assurez-vous que :
Les Claims standard intégrées sont de type non-pointeur
Si c'est un type pointeur, il est préférable de s'assurer d'allouer une mémoire appropriée avant de passer, sinon il y aura une panic.
Exemple 4 : Analyse et vérification de Token HMAC
func TestParse(t *testing.T) {
secret := []byte("my secret")
// Supposons qu'un token a été créé et signé avec l'algorithme HS256
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Passe la chaîne token et la fonction hook de vérification, la valeur de retour est une structure Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Vérifie si l'algorithme de signature correspond
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Algorithme de signature non correspondant: %s", token.Header["alg"])
}
// Retourne la clé de vérification
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)
}
}Sortie :
map[id:123456 name:jack]Exemple 5 : Gestion des erreurs
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// Supposons qu'un token a été créé et signé avec l'algorithme HS256
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// Passe la chaîne token et la fonction hook de vérification, la valeur de retour est une structure Token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Vérifie si l'algorithme de signature correspond
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Algorithme de signature non correspondant: %s", token.Header["alg"])
}
// Retourne la clé de vérification
return secret, nil
})
if token.Valid {
fmt.Println("token valide")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("La chaîne passée n'est même pas un token...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("Le token a expiré ou n'est pas encore valide")
} else {
fmt.Println("Anomalie lors du traitement du token...")
}
}Sortie :
token valideExemple 6 : Analyse avec Claims personnalisées
Si vous avez utilisé des Claims personnalisées lors de la création du Token et que vous souhaitez que les Claims soient directement converties en Claims personnalisées plutôt qu'en map lors de l'analyse, vous devez passer les Claims personnalisées.
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"})) // Utilise l'option pour la vérification
// Assertion de type
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}Sortie :
&{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 }}Exemple 7 : Signature et analyse RSA
RSA est plus couramment utilisé dans les architectures distribuées. Le processus général est le suivant :
- Le centre d'authentification crée une paire de clés, utilise la clé privée pour signer le JWT, le JWT est retourné au client, la clé publique est détenue par les services métier
- Le client porte le JWT pour envoyer des requêtes aux services métier, les modules métier utilisent la clé publique pour analyser le JWT, sans avoir besoin d'accéder au centre d'authentification
- Si l'authentification réussit, les informations métier sont retournées
- Si l'authentification échoue, les informations d'échec sont retournées
func TestRsa(t *testing.T) {
// Créer une paire de clés
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)
// Chiffrement avec clé privée
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// Déchiffrement avec clé publique
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)
}
}