https://d226lax1qjow5r.cloudfront.net/blog/blogposts/usando-jwt-para-autenticação-em-uma-aplicação-golang/blog_jwt-golang_authentification_1200x600-2.png

Utilisation de JWT pour l'authentification dans une application Golang

Publié le August 20, 2021

Temps de lecture : 16 minutes

Introduction

Un jeton Web JSON (JWT) est une forme compacte et indépendante de transmission d'informations de manière sécurisée entre les parties sous la forme d'un objet JSON, et il est couramment utilisé par les personnes non impliquées dans leurs API. Les JWT sont populaires pour les raisons suivantes :

  1. Un JWT n'a pas d'état. En d'autres termes, il n'a pas besoin d'être stocké dans une banque de données (carte de persistance), contrairement aux jetons opaques.

  2. La structure d'un JWT n'est jamais décodée une fois formée, ce qui garantit la sécurité et la protection du jeton.

  3. Un JWT peut être configuré pour être invisible après une certaine période de temps. Cela permet de minimiser ou d'éliminer totalement tout risque de piratage, au cas où le jeton serait piraté.

Dans ce tutoriel, nous allons démontrer la création, l'utilisation et l'invalidation d'un JWT avec une API RESTful simple en utilisant Golang et l'API de Mensagens de Vonage.

Conta API Vonage

Pour terminer ce tutoriel, vous devez disposer d'un contact Vonage API. Si vous n'en avez pas, vous pouvez vous inscrire dès maintenant et commencer à construire en utilisant des crédits gratuits. Une fois que vous avez un compte, vous pouvez trouver votre clé API et votre secret API dans la partie supérieure du tableau de l'API de Vonage.

Ce tutoriel utilise également un numéro de téléphone virtuel. Pour en acquérir un, rendez-vous dans la rubrique Numéros > Acheter des numéros et procurez-vous-en un qui corresponde à vos besoins. Si vous parvenez à vous inscrire, l'achat initial d'un numéro sera facilement couvert par le crédit dont vous disposez.

Sign Up

Qu'est-ce qu'un JWT ?

Un JWT se compose de trois parties :

  • En-tête : le type de jeton et l'algorithme d'évaluation utilisés. Le type de jeton peut être "JWT" et l'algorithme d'évaluation peut être HMAC ou SHA256.

  • Charge utile : deuxième partie du jeton qui contient les données de validation. Ces informations comprennent des données spécifiques à l'application (par exemple, l'identité de l'utilisateur, le nom de l'utilisateur), la durée d'expiration du jeton (expiration), le(s) émetteur(s), le(s) objet(s) et ainsi de suite.

  • Signature : l'"en-tête" codifié, la "charge utile" codifiée et une signature que vous créez sont utilisés pour créer une signature.

Nous allons utiliser un jeton simple pour comprendre les concepts qui suivent.

Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtM

Ne vous inquiétez pas, le jeton est invisible et ne fonctionne donc dans aucune application de production.

Você pode navegar o site jwt.to et tester l'intégrité du jeton, qu'il soit vérifié ou non. Utilisez l'algorithme "HS512". Vous recevrez le message "Signature Verify" (Assinatura verificada) :

[JWT.IO Example]

Pour ce faire, l'application doit fournir une clé. Cette dernière permet d'assurer la sécurité du traitement - même si le JWT est décodé, le traitement reste crypté. Il est vivement recommandé d'utiliser toujours une fenêtre lors de la création d'un JWT.

Types de jetons

Chaque fois qu'un JWT peut être défini pour expirer (être invalidé) après une période de temps déterminée, des jetons sont pris en compte dans cette demande :

  • Jeton d'accès : Un jeton d'accès est utilisé pour les demandes nécessitant une autorisation. Il est normalement ajouté au dossier de la demande. Il est recommandé que le jeton d'accès ait une durée de vie courte, de l'ordre de 15 minutes. Le fait de donner à un jeton d'accès une durée de vie courte permet d'éviter tout problème grave si le jeton d'un utilisateur est falsifié, au cas où le jeton serait piraté. Le pirate informatique dispose de 15 minutes ou moins pour effectuer ses opérations avant que le jeton ne soit invalidé.

  • Jeton de rafraîchissement : Un jeton de mise à jour a une durée de vie utile plus longue, généralement de 7 jours. Ce jeton est utilisé pour générer de nouveaux jetons d'accès et de mise à jour. Si le jeton d'accès expire, de nouvelles combinaisons de jetons d'accès et de mise à jour sont créées lorsque la rotation du jeton de mise à jour est terminée (à partir de notre application).

Comment acquérir un JWT ?

Pour une application à forte production, il est fortement recommandé de stocker les JWT dans un cookie. HttpOnly. Pour ce faire, lors de l'envoi du cookie généré par le backend au frontend (client), un drapeau HttpOnly est envoyé à la fin du cookie, indiquant au navigateur qu'il ne doit pas l'envoyer par l'intermédiaire des scripts du client. Cela permet d'éviter les attaques de type XSS (Cross Site Scripting). Le JWT peut également être enregistré lors de l'enregistrement local du navigateur ou lors de l'enregistrement de la session. L'enregistrement d'un JWT sous cette forme peut l'exposer à de nombreuses attaques, comme le XSS mentionné ci-dessus, de sorte qu'il est généralement moins sûr que l'utilisation de la technique "HttpOnly cookie".

L'application

Envisageons une API Restful ToDo.

Créez un dossier de candidature jwt-todo, après avoir initialisé go.mod pour la gestion des dépendances. O go.mod est initialisé à l'aide de :

go mod init jwt-todo

Envoyez maintenant un message main.go dentro do diretório raiz /jwt-todoet l'ajoute à sa liste :

package main

func main() {}

Nous utilisons Gin pour le traitement des requêtes HTTP. Le cadre Gin permet de réduire le nombre de modèles de documents et est très efficace pour la construction d'APIs évolutives.

Vous pouvez installer le gin, même si vous ne l'avez pas encore fait, en utilisant le logiciel :

go get github.com/gin-gonic

En suite, actualisez l'arquivo main.go:

package main

Import (
    "github.com/gin-gonic/gin"
)

var (
  router = gin.Default()
)

func main() {
  router.POST("/login", Login)
  log.Fatal(router.Run(":8080"))
}

Dans une situation idéale, une banque /login vérifie les références d'un utilisateur, les compare avec celles d'une banque de données et enregistre si les références sont correctes. Avec cette API, nous n'utilisons qu'un seul profil d'utilisateur que nous définissons en mémoire. Créez un profil d'utilisateur dans une "structure". Ajoutez ceci à l'article main.go:

type User struct {
ID uint64            `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}
//A sample use
var user = User{
    ID:             1,
    Username: "username",
    Password: "password",
}

Demande de connexion

Lorsque les coordonnées d'un utilisateur sont vérifiées, celui-ci est enregistré et un JWT est créé pour son nom. C'est ce que permet la fonction Login() définie ci-dessus :

func Login(c *gin.Context) {
  var u User
  if err := c.ShouldBindJSON(&u); err != nil {
     c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
     return
  }
  //compare the user from the request, with the one we defined:
  if user.Username != u.Username || user.Password != u.Password {
     c.JSON(http.StatusUnauthorized, "Please provide valid login details")
     return
  }
  token, err := CreateToken(user.ID)
  if err != nil {
     c.JSON(http.StatusUnprocessableEntity, err.Error())
     return
  }
  c.JSON(http.StatusOK, token)
}

Nous recevons la demande de l'utilisateur et, par la suite, nous la convertissons en "structure" de l'utilisateur. Nous comparons ensuite l'utilisateur de l'entrée avec celui que nous avons défini en mémoire. Si nous utilisons un banc de données, nous le comparons à un registre du banc de données.

Pour que la fonction de connexion ne soit pas inutilisée, la logique de création d'un JWT a été modifiée par la société CreateToken. Observez que l'identification de l'utilisateur est transmise à cette fonction. Elle est utilisée comme un rappel à l'ordre lors de la création d'un JWT.

La fonction CreateToken fait usage de l'interrupteur dgrijalva/jwt-goNous pouvons l'installer de la même manière :

go get github.com/dgrijalva/jwt-go

Nous allons définir une fonction CreateToken:

func CreateToken(userid uint64) (string, error) {
  var err error
  //Creating Access Token
  os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
  atClaims := jwt.MapClaims{}
  atClaims["authorized"] = true
  atClaims["user_id"] = userid
  atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix()
  at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
  token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
  if err != nil {
     return "", err
  }
  return token, nil
}

Nous avons défini que le jeton n'est valide que pendant 15 minutes, et qu'une fois invalidé, il ne peut être utilisé pour aucune demande d'authentification. Observez également que nous associons le JWT à l'aide d'un mot de passe (ACCESS_SECRET) obtenu à partir de notre variante de l'environnement. Il est vivement recommandé de ne pas exposer cette clé dans sa base de données, mais de la placer dans l'environnement comme nous le faisons ici. Você pode salvá-lo em um .env`.yml ou de la façon dont il fonctionne pour vous.

Até agora, nosso arquivo main.go se parece com isto :

package main

import (
  "github.com/dgrijalva/jwt-go"
  "github.com/gin-gonic/gin"
  "log"
  "net/http"
  "os"
  "time"
)

var (
  router = gin.Default()
)

func main() {
  router.POST("/login", Login)
  log.Fatal(router.Run(":8080"))
}
type User struct {
  ID uint64            `json:"id"`
  Username string `json:"username"`
  Password string `json:"password"`
  Phone string `json:"phone"`
}
var user = User{
  ID:            1,
  Username: "username",
  Password: "password",
  Phone: "49123454322", //this is a random number
}
func Login(c *gin.Context) {
  var u User
  if err := c.ShouldBindJSON(&u); err != nil {
     c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
     return
  }
  //compare the user from the request, with the one we defined:
  if user.Username != u.Username || user.Password != u.Password {
     c.JSON(http.StatusUnauthorized, "Please provide valid login details")
     return
  }
  token, err := CreateToken(user.ID)
  if err != nil {
     c.JSON(http.StatusUnprocessableEntity, err.Error())
     return
  }
  c.JSON(http.StatusOK, token)
}
func CreateToken(userId uint64) (string, error) {
  var err error
  //Creating Access Token
  os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
  atClaims := jwt.MapClaims{}
  atClaims["authorized"] = true
  atClaims["user_id"] = userId
  atClaims["exp"] = time.Now().Add(time.Minute * 15).Unix()
  at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
  token, err := at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
  if err != nil {
     return "", err
  }
  return token, nil
}

Nous pouvons à présent exécuter l'application :

go run main.go

Nous allons maintenant pouvoir l'expérimenter et voir ce que nous avons obtenu ! Utilisez votre interface API préférée et cliquez sur le point de connexion :

[result]

Comme vous pouvez le voir ci-contre, nous avons créé un JWT d'une durée de 15 minutes.

Failles dans la mise en œuvre

Sim, nous pouvons nous connecter à un utilisateur et créer un JWT, mais il y a beaucoup d'erreurs dans la mise en œuvre :

  1. Le JWT ne peut pas être invalidé lorsqu'il expire. La limite la plus importante est la suivante : un utilisateur peut se connecter, puis décider d'abandonner immédiatement, mais le JWT de l'utilisateur reste valide jusqu'à ce que le délai d'expiration soit écoulé.

  2. Le JWT peut être utilisé par un pirate informatique sans que l'utilisateur ne fasse quoi que ce soit à ce sujet, jusqu'à ce que le jeton expire.

  3. L'utilisateur doit s'enregistrer à nouveau après l'expiration du jeton, ce qui lui permettra d'avoir une expérience plus longue.

Nous pouvons résoudre les problèmes mentionnés de deux manières :

  1. Utilisation d'une caméra de stockage de données persistantes pour stocker les métadonnées JWT. Cela permet d'invalider un logo JWT dès que l'utilisateur se déconnecte, améliorant ainsi la sécurité.

  2. En utilisant le concept de mise à jour du jeton pour générer un nouveau jeton d'accès en cas d'expiration du jeton d'accès, l'expérience de l'utilisateur s'en trouve améliorée.

Utiliser Redis pour stocker les métadonnées de JWT

L'une des solutions que nous proposons aujourd'hui est de supprimer les métadonnées JWT dans un champ de persistance. Il est possible de le faire avec n'importe quelle carte de persistance, mais il est vivement recommandé de le faire. Lorsque les JWT que nous gérons ont une durée d'expiration, le redis a pour caractéristique d'éliminer automatiquement les données dont la durée d'expiration a été dépassée. Le redis permet également de manipuler plusieurs textes et de les faire défiler horizontalement.

Comme le redis est un système de stockage de type clé-valeur, ses chaînes doivent être uniques. Pour ce faire, nous utilisons uuid comme chaîne et l'identifiant de l'utilisateur comme valeur.

En outre, nous allons installer deux ordinateurs pour l'utilisation :

go get github.com/go-redis/redis/v7
go get github.com/twinj/uuid

Nous importons également ce qui se trouve dans l'archive main.go également :

import (

  "github.com/go-redis/redis/v7"
  "github.com/twinj/uuid"

)

Nota : Nous espérons que vous avez installé des disques durs sur votre ordinateur local. Si ce n'est pas le cas, vous pouvez le faire avant de continuer.

Nous lançons aujourd'hui la procédure d'inscription :

var  client *redis.Client

func init() {
  //Initializing redis
  dsn := os.Getenv("REDIS_DSN")
  if len(dsn) == 0 {
     dsn = "localhost:6379"
  }
  client = redis.NewClient(&redis.Options{
     Addr: dsn, //redis port
  })
  _, err := client.Ping().Result()
  if err != nil {
     panic(err)
  }
}

Le client redis est lancé dans la fonction init(). Cela garantit que chaque fois que l'on exécute l'article main.go, le redis est automatiquement connecté.

Lorsque nous créons un jeton à partir de ce point, nous créons un identifiant qui sera utilisé comme l'une des revendications du jeton, tout comme nous avons utilisé l'identifiant de l'utilisateur comme une revendication lors de la mise en œuvre précédente.

Définir les métadonnées=

Dans notre solution proposée, au lieu de créer uniquement un jeton, nous devons créer deux JWT :

  1. Jeton d'accès

  2. O Rafraîchissement du jeton

Pour ce faire, il est nécessaire de définir une structure qui englobe ces définitions de jetons, leurs conditions de validité et l'UUIDS :

type TokenDetails struct {
  AccessToken  string
  RefreshToken string
  AccessUuid   string
  RefreshUuid  string
  AtExpires    int64
  RtExpires    int64
}

La durée de validité et les outils sont très importants, car ils sont utilisés pour récupérer les métadonnées simulées dans les bases de données.

Nous allons maintenant actualiser la fonction CreateToken pour cet aspect :

func CreateToken(userid uint64) (*TokenDetails, error) {
  td := &TokenDetails{}
  td.AtExpires = time.Now().Add(time.Minute * 15).Unix()
  td.AccessUuid = uuid.NewV4().String()

  td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()
  td.RefreshUuid = uuid.NewV4().String()

  var err error
  //Creating Access Token
  os.Setenv("ACCESS_SECRET", "jdnfksdmfksd") //this should be in an env file
  atClaims := jwt.MapClaims{}
  atClaims["authorized"] = true
  atClaims["access_uuid"] = td.AccessUuid
  atClaims["user_id"] = userid
  atClaims["exp"] = td.AtExpires
  at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
  td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
  if err != nil {
     return nil, err
  }
  //Creating Refresh Token
  os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf") //this should be in an env file
  rtClaims := jwt.MapClaims{}
  rtClaims["refresh_uuid"] = td.RefreshUuid
  rtClaims["user_id"] = userid
  rtClaims["exp"] = td.RtExpires
  rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
  td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET")))
  if err != nil {
     return nil, err
  }
  return td, nil
}

Dans la fonction suivante, le jeton d'accès expire au bout de 15 minutes et le jeton de rafraîchissement expire au bout de 7 jours. Vous pouvez également observer que nous avons ajouté un identifiant comme claim à chaque jeton.

Comme l'identifiant est unique à chaque fois qu'il est créé, un utilisateur peut créer plus d'un jeton. C'est le cas lorsqu'un utilisateur est connecté à différents dispositifs. L'utilisateur peut également se déconnecter de n'importe lequel de ses appareils sans que ceux-ci soient déconnectés de tous les appareils. C'est légal !

Sauver les métadonnées des JWTs

Nous allons à présent définir la fonction qui sera utilisée pour récupérer les métadonnées des JWT :

func CreateAuth(userid uint64, td *TokenDetails) error {
    at := time.Unix(td.AtExpires, 0) //converting Unix to UTC(to Time object)
    rt := time.Unix(td.RtExpires, 0)
    now := time.Now()

    errAccess := client.Set(td.AccessUuid, strconv.Itoa(int(userid)), at.Sub(now)).Err()
    if errAccess != nil {
        return errAccess
    }
    errRefresh := client.Set(td.RefreshUuid, strconv.Itoa(int(userid)), rt.Sub(now)).Err()
    if errRefresh != nil {
        return errRefresh
    }
    return nil
}

Nous vous invitons à consulter le site TokenDetails qui contient des informations sur le délai d'expiration des JWT et sur les identifiants utilisés pour la création des JWT. Si le délai d'expiration est atteint tant pour le jeton de rafraîchissement que pour le jeton d'accès, le JWT est automatiquement exclu du réseau.

J'utilise personnellement Redily, une interface graphique pour la gestion des données. C'est un excellent outil. Vous pouvez jeter un coup d'œil ci-dessus pour voir comment les métadonnées du JWT sont stockées dans la partie clé-valeur.

[results]

Avant de tester le login pour la première fois, il est nécessaire de modifier la fonction CreateAuth() dans la fonction Login(). Mettre à jour la fonction Login :

func Login(c *gin.Context) {
  var u User
  if err := c.ShouldBindJSON(&u); err != nil {
     c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
     return
  }
  //compare the user from the request, with the one we defined:
  if user.Username != u.Username || user.Password != u.Password {
     c.JSON(http.StatusUnauthorized, "Please provide valid login details")
     return
  }
  ts, err := CreateToken(user.ID)
 if err != nil {
 c.JSON(http.StatusUnprocessableEntity, err.Error())
   return
}
 saveErr := CreateAuth(user.ID, ts)
  if saveErr != nil {
     c.JSON(http.StatusUnprocessableEntity, saveErr.Error())
  }
  tokens := map[string]string{
     "access_token":  ts.AccessToken,
     "refresh_token": ts.RefreshToken,
  }
  c.JSON(http.StatusOK, tokens)
}

Nous pouvons essayer d'entrer à nouveau dans le système. Salve o arquivo main.go et l'exécuter. Lorsque le login est demandé par Postman, il faut le faire :

[postman result]

Excellent ! Nous avons aussi bien le access_token que le refresh_token, et nous avons également des métadonnées simbólicos persistantes sur le redis.

Criando um Todo

Nous pouvons à présent traiter les demandes qui requièrent une autorisation en utilisant le JWT.

L'une des requêtes non autorisées dans cette API est la création de toute requête.

En premier lieu, nous allons définir une structure Todo:

type Todo struct {
  UserID uint64 `json:"user_id"`
  Title string `json:"title"`
}

Lors de l'exécution d'une demande d'authentification, il convient de valider le jeton passé dans la boîte d'authentification pour vérifier s'il est valide. Nous devons définir quelques fonctions d'aide avec eux.

Il faut d'abord extraire le jeton du numéro de téléphone de la demande en utilisant la fonction ExtractToken:

func ExtractToken(r *http.Request) string {
  bearToken := r.Header.Get("Authorization")
  //normally Authorization the_token_xxx
  strArr := strings.Split(bearToken, " ")
  if len(strArr) == 2 {
     return strArr[1]
  }
  return ""
}

Ensuite, nous vérifions le jeton :

func VerifyToken(r *http.Request) (*jwt.Token, error) {
  tokenString := ExtractToken(r)
  token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
     //Make sure that the token method conform to "SigningMethodHMAC"
     if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
     }
     return []byte(os.Getenv("ACCESS_SECRET")), nil
  })
  if err != nil {
     return nil, err
  }
  return token, nil
}

Nous avons ExtractToken dans la fonction VerifyToken pour obtenir la chaîne de jetons, après quoi nous procédons à la vérification de la méthode d'évaluation.

Ensuite, nous vérifions la validité du jeton, s'il est toujours utile ou s'il a expiré, à l'aide de la fonction TokenValid:

func TokenValid(r *http.Request) error {
  token, err := VerifyToken(r)
  if err != nil {
     return err
  }
  if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
     return err
  }
  return nil
}

Nous allons également extraire les métadonnées du jeton qui seront fournies dans notre magasin rouge que nous avons montré précédemment. Pour extraire le jeton, nous avons défini la fonction suivante ExtractTokenMetadata:

func ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
  token, err := VerifyToken(r)
  if err != nil {
     return nil, err
  }
  claims, ok := token.Claims.(jwt.MapClaims)
  if ok && token.Valid {
     accessUuid, ok := claims["access_uuid"].(string)
     if !ok {
        return nil, err
     }
     userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
     if err != nil {
        return nil, err
     }
     return &AccessDetails{
        AccessUuid: accessUuid,
        UserId:   userId,
     }, nil
  }
  return nil, err
}

La fonction ExtractTokenMetadata renvoie un AccessDetails (qui est une structure). Cette structure contient des métadonnées (access_uuid e user_id) que nous devons rechercher dans la base de données. Si vous avez une raison quelconque de ne pas obtenir les métadonnées du jeton, votre demande sera interrompue par un message d'erreur.

O struct AccessDetails mencionado acima se parece com isto :

type AccessDetails struct {
    AccessUuid string
    UserId   uint64
}

Nous avons également évoqué la recherche des métadonnées du jeton dans les redis. Nous allons définir une fonction qui nous permettra de le faire :

func FetchAuth(authD *AccessDetails) (uint64, error) {
  userid, err := client.Get(authD.AccessUuid).Result()
  if err != nil {
     return 0, err
  }
  userID, _ := strconv.ParseUint(userid, 10, 64)
  return userID, nil
}

FetchAuth() aceita os AccessDetails de la fonction ExtractTokenMetadata, puis il s'inscrit sur la liste. Si aucun registre n'est trouvé, cela peut signifier que le jeton a expiré ou qu'une erreur s'est produite.

Nous allons finalement lier la fonction CreateTodo pour mieux comprendre la mise en œuvre des fonctions suivantes :

func CreateTodo(c *gin.Context) {
  var td *Todo
  if err := c.ShouldBindJSON(&td); err != nil {
     c.JSON(http.StatusUnprocessableEntity, "invalid json")
     return
  }
  tokenAuth, err := ExtractTokenMetadata(c.Request)
  if err != nil {
     c.JSON(http.StatusUnauthorized, "unauthorized")
     return
  }
 userId, err = FetchAuth(tokenAuth)
  if err != nil {
     c.JSON(http.StatusUnauthorized, "unauthorized")
     return
  }
td.UserID = userId

//you can proceed to save the Todo to a database
//but we will just return it to the caller here:
  c.JSON(http.StatusCreated, td)
}

Comme nous l'avons vu, nous avons utilisé la fonction ExtractTokenMetadata pour extraire les métadonnées du JWT qui est utilisé dans FetchAuth pour vérifier si les métadonnées existent toujours dans notre magasin redis. Si tout se passe bien, l'ensemble peut être conservé dans la base de données, mais nous avons choisi de le transférer à l'appelant.

Nous allons mettre à jour le main() pour y inclure une fonction CreateTodo:

func main() {
  router.POST("/login", Login)
  router.POST("/todo", CreateTodo)

  log.Fatal(router.Run(":8080"))
}

Pour tester l'application CreateTodo, il faut se connecter et copier le champ access_token et l'ajouter au champ "Bearer Token Field" comme ceci :

[bearer token]

Ensuite, ajoutez un titre au corps de la requête pour créer un ensemble et envoyez une requête POST au point de terminaison /todocomme indiqué ci-dessus :

[result]

La tentative de création d'un ToDo sans accès sera rejetée :

[denied]

Demande de déconnexion

Jusqu'à présent, nous avons vu comment un JWT est utilisé pour authentifier une requête. Lorsqu'un utilisateur se déconnecte, nous réactivons ou invalidons instantanément son JWT. Cela est possible en utilisant les métadonnées du JWT de notre magasin redis.

Nous allons maintenant définir une fonction qui nous permet d'exclure les métadonnées JWT du réseau :

func DeleteAuth(givenUuid string) (int64,error) {
  deleted, err := client.Del(givenUuid).Result()
  if err != nil {
     return 0, err
  }
  return deleted, nil
}

La fonction suivante permet d'enregistrer sur le disque la donnée correspondant à l'image passée. uuid passado como parâmetro.

La fonction Logout tem este aspecto :

func Logout(c *gin.Context) {
  au, err := ExtractTokenMetadata(c.Request)
  if err != nil {
     c.JSON(http.StatusUnauthorized, "unauthorized")
     return
  }
  deleted, delErr := DeleteAuth(au.AccessUuid)
  if delErr != nil || deleted == 0 { //if any goes wrong
     c.JSON(http.StatusUnauthorized, "unauthorized")
     return
  }
  c.JSON(http.StatusOK, "Successfully logged out")
}

En fonction LogoutNous extrayons d'abord les métadonnées du JWT. Si le résultat est satisfaisant, nous procédons à l'élimination de ces métadonnées, ce qui a pour effet d'invalider immédiatement le JWT.

Avant de tester, actualisez l'article main.go pour y inclure un point de terminaison de logout comme celui-ci :

func main() {
  router.POST("/login", Login)
  router.POST("/todo", CreateTodo)
  router.POST("/logout", Logout)

  log.Fatal(router.Run(":8080"))
}

Forneça um access_token Associer un mot de passe à un utilisateur et, ensuite, se déconnecter de l'utilisateur. Lancez l'ajout d'un access_token ao Authorization Bearer Token et, ensuite, accéder au point de déconnexion :

[logout endpoint]

À présent, l'utilisateur est déconnecté, et aucune autre demande ne peut être introduite avec ce JWT une fois qu'il est immédiatement invalidé. Cette implémentation est plus sûre que d'attendre qu'un JWT expire après que l'utilisateur a été déconnecté du système.

Protéger les rotations automatiques

Nous avons deux solutions qui requièrent une autorisation :/login e /logout. Aujourd'hui, avec ou sans autorisation, toute personne peut accéder à ces fonctions. Nous allons changer cela.

Nous allons définir une fonction TokenAuthMiddleware() pour assurer ces rotations :

func TokenAuthMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
     err := TokenValid(c.Request)
     if err != nil {
        c.JSON(http.StatusUnauthorized, err.Error())
        c.Abort()
        return
     }
     c.Next()
  }
}

Comme indiqué ci-dessus, nous utilisons la fonction TokenValid() (définie précédemment) pour vérifier si le jeton est toujours valide ou s'il a expiré. Cette fonction est utilisée pour protéger les serveurs authentifiés.

Nous allons maintenant mettre à jour l'application main.go pour inclure cet intergiciel :

func main() {
  router.POST("/login", Login)
  router.POST("/todo", TokenAuthMiddleware(), CreateTodo)
  router.POST("/logout", TokenAuthMiddleware(), Logout)

  log.Fatal(router.Run(":8080"))
}

Atulizando os Tokens

À l'heure actuelle, nous pouvons créer, utiliser et réviser les JWT. Dans une application qui comporte une interface utilisateur, que se passe-t-il si le jeton d'accès expire et que l'utilisateur doit faire une demande d'authentification ? L'utilisateur est-il désautorisé et doit-il se connecter à nouveau ? C'est malheureusement le cas. Mais cela peut être évité en utilisant le concept de jeton de rafraîchissement. L'utilisateur n'a pas besoin de se connecter une nouvelle fois.

Le jeton de rafraîchissement créé en même temps que le jeton d'accès est utilisé pour créer de nouveaux ensembles de jetons d'accès et de rafraîchissement.

En utilisant JavaScript pour utiliser nos terminaisons API, nous pouvons mettre à jour les JWTs facilement en utilisant des intercepteurs axios. Dans notre API, nous devons envoyer une demande de POST avec un refresh_token en tant que partie du point de terminaison /token/refresh.

Nous allons d'abord créer une fonction Refresh():

func Refresh(c *gin.Context) {
  mapToken := map[string]string{}
  if err := c.ShouldBindJSON(&mapToken); err != nil {
     c.JSON(http.StatusUnprocessableEntity, err.Error())
     return
  }
  refreshToken := mapToken["refresh_token"]

  //verify the token
  os.Setenv("REFRESH_SECRET", "mcmvmkmsdnfsdmfdsjf") //this should be in an env file
  token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
     //Make sure that the token method conform to "SigningMethodHMAC"
     if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
     }
     return []byte(os.Getenv("REFRESH_SECRET")), nil
  })
  //if there is an error, the token must have expired
  if err != nil {
     c.JSON(http.StatusUnauthorized, "Refresh token expired")
     return
  }
  //is token valid?
  if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
     c.JSON(http.StatusUnauthorized, err)
     return
  }
  //Since token is valid, get the uuid:
  claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims
  if ok && token.Valid {
     refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
     if !ok {
        c.JSON(http.StatusUnprocessableEntity, err)
        return
     }
     userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
     if err != nil {
        c.JSON(http.StatusUnprocessableEntity, "Error occurred")
        return
     }
     //Delete the previous Refresh Token
     deleted, delErr := DeleteAuth(refreshUuid)
     if delErr != nil || deleted == 0 { //if any goes wrong
        c.JSON(http.StatusUnauthorized, "unauthorized")
        return
     }
    //Create new pairs of refresh and access tokens
     ts, createErr := CreateToken(userId)
     if  createErr != nil {
        c.JSON(http.StatusForbidden, createErr.Error())
        return
     }
    //save the tokens metadata to redis
saveErr := CreateAuth(userId, ts)
 if saveErr != nil {
        c.JSON(http.StatusForbidden, saveErr.Error())
       return
}
 tokens := map[string]string{
       "access_token":  ts.AccessToken,
  "refresh_token": ts.RefreshToken,
}
     c.JSON(http.StatusCreated, tokens)
  } else {
     c.JSON(http.StatusUnauthorized, "refresh expired")
  }
}

Étant donné que beaucoup de choses se produisent dans cette fonction, nous allons essayer de comprendre le flux.

  • Primeiro tomamos o refresh_token do corpo de request.

  • Ensuite, nous avons vérifié la méthode d'évaluation du jeton.

  • Ensuite, nous vérifions si le jeton est toujours valide.

  • O refresh_uuid e o user_id sont donc des éléments supplémentaires, qui sont des métadonnées utilisées comme références pour créer un jeton de mise à jour.

  • Ensuite, nous avons acheté les métadonnées dans le magasin rouge et nous les avons utilisées en utilisant l'outil refresh_uid en tant qu'outil de travail.

  • Ensuite, nous avons créé un nouveau groupe de jetons d'accès et de rafraîchissement qui sont désormais utilisés pour les demandes futures.

  • Les métadonnées des jetons d'accès et d'actualisation sont des éléments importants du réseau.

  • Les jetons créés sont dévolus à l'appelant.

  • Par ailleurs, si le jeton de mise à jour n'est pas valide, l'utilisateur n'est pas autorisé à créer une nouvelle partie des jetons. Il est nécessaire d'effectuer une nouvelle connexion pour obtenir de nouveaux jetons.

Ensuite, ajoutez une barre de mise à jour des jetons à la fonction main():

router.POST("/token/refresh", Refresh)

Test du point de vue de l'utilisateur refresh_token válido :

[ ![testing}(https://www.nexmo.com/wp-content/uploads/2020/03/image7.png)]

Nous avons créé avec succès de nouveaux jeux de cartes. Beleza !! 😎.

Envoyer des messages en utilisant l'API de messages de Vonage

Notifiez les utilisateurs chaque fois qu'ils créent un ToDo en utilisant l'API de Mensagens Vonage.

Vous pouvez définir votre clé API et votre mot de passe de différentes manières et les utiliser ensuite dans cet outil sous cette forme :

var (
  NEXMO_API_KEY   = os.Getenv( "your_api_key")
  NEXMO_API_SECRET  = os.Getenv("your_secret")
)

Ensuite, nous définirons quelques structures contenant des informations sur le destinataire, le récepteur et le contenu du message :

type Payload struct {
  From    From    `json:"from"`
  To      To      `json:"to"`
  Message Message `json:"message"`
}
type From struct {
  Type   string `json:"type"`
  Number string `json:"number"`
}
type To struct {
  Type   string `json:"type"`
  Number string `json:"number"`
}
type Content struct {
  Type string `json:"type"`
  Text string `json:"text"`
}
type Message struct {
  Content Content `json:"content"`
}

Ensuite, nous définissons une fonction permettant d'envoyer un message à un utilisateur situé à l'autre bout du monde :

func SendMessage(username, phone string) (*http.Response, error) {
  data := Payload{
     From: From{
        Type:   "sms",
        Number: "Nexmo",
     },
     To: To{
        Type:   "sms",
        Number: phone,
     },
     Message: Message{
        Content: Content{
           Type: "text",
           Text: "Dear " + username + ", a todo was created from your account just now.",
        },
     },
  }
  payloadBytes, err := json.Marshal(data)
  if err != nil {
     return nil, err
  }
  body := bytes.NewReader(payloadBytes)

  req, err := http.NewRequest("POST", "https://api.nexmo.com/v0.1/messages", body)
  if err != nil {
     return nil, err
  }
  //Ensure headers
  req.SetBasicAuth(NEXMO_API_KEY, NEXMO_API_SECRET)
  req.Header.Set("Content-Type", "application/json")
  req.Header.Set("Accept", "application/json")

  resp, err := http.DefaultClient.Do(req)
  if err != nil {
     return nil, err
  }
  defer resp.Body.Close()

  return resp, nil
}

Dans la fonction suivante, le numéro To est le numéro de l'utilisateur, tandis que le numéro From doit être compté à partir du panneau de contrôle Vonage.

Certifiez-vous d'avoir ces variétés NEXMO_API_KEY e NEXMO_API_SECRET définies dans l'inventaire des variables d'ambiance.

Nous actualisons à présent la fonction CreateTodo pour inclure une fonction SendMessage qui a déjà été définie, en tenant compte des paramètres nécessaires :

func CreateTodo(c *gin.Context) {
  var td *Todo
  if err := c.ShouldBindJSON(&td); err != nil {
     c.JSON(http.StatusUnprocessableEntity, "invalid json")
     return
  }
  tokenAuth, err := ExtractTokenMetadata(c.Request)
  if err != nil {
     c.JSON(http.StatusUnauthorized, "unauthorized")
     return
  }
 userId, err = FetchAuth(tokenAuth)
  if err != nil {
     c.JSON(http.StatusUnauthorized, "unauthorized")
     return
  }
td.UserID = userId
//you can proceed to save the Todo to a database
//but we will just return it to the caller here:

//Send the user a notification
  msgResp, err := SendMessage(user.Username, user.Phone)
  if err != nil {
     c.JSON(http.StatusForbidden, "error occurred sending message to user")
     return
  }
  if msgResp.StatusCode > 299 {
     c.JSON(http.StatusForbidden, "cannot send message to user")
     return
  }

  c.JSON(http.StatusCreated, td)
}

Certifiez qu'un numéro de téléphone fixe est requis pour que vous puissiez recevoir un message lorsque vous souhaitez créer un article.

Conclusion

Vous avez vu comment vous pouvez créer et invalider un JWT. Vous avez également appris comment intégrer l'API Messages de Vonage dans votre application Golang pour envoyer des notifications. Pour plus d'informations sur les meilleures pratiques et l'utilisation d'un JWT, n'hésitez pas à consulter cette de GitHub. Vous pouvez mettre en place cette application et utiliser un banc de données réelles pour conserver les utilisateurs et tous les autres, et vous pouvez également utiliser React ou Vue.js pour construire un frontend. C'est ainsi que vous apprécierez vraiment le recours au jeton de rafraîchissement avec l'aide des intercepteurs d'Axios.

Partager:

https://a.storyblok.com/f/270183/150x150/e744ec943a/victor-steven.png
Victor Steven

Victor Steven is a self-taught full-stack developer that loves researching about doing things differently. He has a degree in Engineering and over 5 years of experience as a software developer. Steven is very interested in designing and building scalable APIs and he enjoys writing about his discoveries in software development.