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

Uso de JWT para Autenticación en una Aplicación Golang

Publicado el August 20, 2021

Tiempo de lectura: 14 minutos

Introducción

Un JSON Web Token (JWT) es una forma compacta e independiente de transmitir información de forma segura entre las partes como un objeto JSON, y son comúnmente utilizados por las personas no involucradas en sus APIs. Los JWT son populares porque:

  1. Un JWT no tiene estado. O sea, no necesita ser almacenado en un banco de datos (cámara de persistencia), al contrario que los tokens opacos.

  2. La asinatura de un JWT nunca se descodifica una vez formada, garantizando así que el token esté seguro y protegido.

  3. Un JWT puede ser configurado para ser inválido después de un cierto período de tiempo. Esto ayuda a minimizar o eliminar por completo cualquier daño que pueda causar un hacker, en caso de que el token sea hackeado.

En este tutorial, mostraremos la creación, uso e invalidación de un JWT con una API RESTful simple usando Golang y la API de Mensajería de Vonage.

API Conta Vonage

Para completar este tutorial, necesitarás una cuenta Vonage API. Si aún no tienes una, puedes inscribirte aquí y comenzar a construirla usando créditos gratuitos. Una vez que tengas una cuenta, podrás encontrar tu clave de API y tu secreto de API en la parte superior del Panel de API de Vonage.

Este tutorial también utiliza un número de teléfono virtual. Para adquirirlo, ve a Números > Comprar Números y hazte con uno que se ajuste a tus necesidades. Si acabas de registrarte, la compra inicial de un número se hará fácilmente con tu crédito disponible.

Sign Up

¿Qué es un JWT?

Un JWT se compone de tres partes:

  • Cabecera: el tipo de token y el algoritmo de cifrado utilizado. El tipo de token puede ser "JWT", mientras que el algoritmo de cifrado puede ser HMAC o SHA256.

  • Payload: la segunda parte del token que contiene las reivindicaciones. Estas reivindicaciones incluyen datos específicos de la aplicación (por ejemplo, identificación del usuario, nombre de usuario), tiempo de caducidad del token (caducidad), emisor(es), asunto(s) y así sucesivamente.

  • Firma: la "cabecera" codificada, la "carga útil" codificada y una contraseña que tú creas se utilizan para crear la firma.

Vamos a usar un token sencillo para entender los conceptos anteriores.

Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtM

No se preocupe, el token es inválido, por lo que no funcionará en ninguna aplicación de producción.

Puede navegar por el sitio jwt.to y comprobar si el token está verificado o no. Utilice el algoritmo "HS512". Recibirás el mensaje "Signature Verified" (Firma verificada):

[JWT.IO Example]

Para realizar el cifrado, su aplicación deberá proporcionar una clave. Esta clave permite que la autenticación permanezca segura: incluso cuando se descodifica el JWT, la autenticación permanece criptografiada. Es muy recomendable utilizar siempre una contraseña para crear un JWT.

Tipos de fichas

Una vez que un JWT puede ser definido para expirar (ser invalidado) después de un determinado período de tiempo, dos tokens serán considerados en esta solicitud:

  • Token de acceso: Un token de acceso se utiliza para solicitudes que requieren autenticación. Normalmente se añade al encabezado de la solicitud. Se recomienda que un token de acceso tenga un tiempo de vida corto, digamos 15 minutos. Dar a un token de acceso un período corto de tiempo puede evitar cualquier daño grave si el token de un usuario es adulterado, en caso de que el token sea hackeado. El hacker sólo tiene 15 minutos o menos para realizar sus operaciones antes de que el token sea invalidado.

  • Token de actualización: Un token de actualización tiene una vida útil más larga, generalmente 7 días. Este token se utiliza para generar nuevos tokens de acceso y actualización. En caso de que el token de acceso caduque, se crearán nuevos conjuntos de tokens de acceso y actualización cuando se active la rotación del token de actualización (a partir de nuestra aplicación).

Cómo crear un JWT

Para una aplicación de gran producción, es muy recomendable almacenar JWTs en una cookie HttpOnly. Para ello, cuando se envía la cookie generada en el backend al frontend (cliente), se envía una bandera HttpOnly que indica al navegador que no debe enviar la cookie a través de los scripts del cliente. Esto puede prevenir ataques de XSS (Cross Site Scripting). El JWT también puede almacenarse en el almacenamiento local del navegador o en el almacenamiento de la sesión. El almacenamiento de un JWT de esta forma puede exponerlo a varios ataques como el XSS mencionado anteriormente, de modo que es generalmente menos seguro cuando se compara con el uso de la técnica 'HttpOnly cookie'.

Aplicación

Consideraremos una API Restful ToDo.

Crear un directorio llamado jwt-tododespués de iniciar go.mod para la gestión de las dependencias. O go.mod se inicia con:

go mod init jwt-todo

Ahora, cree un archivo main.go dentro do diretório raiz /jwt-todoy añádele esto:

package main

func main() {}

Usamos gin para el enrutamiento y tratamiento de peticiones HTTP. Gin Framework ayuda a reducir el código de boilerplate y es muy eficiente en la construcción de APIs escalables.

Puedes instalar el gin, si aún no lo has hecho, usando:

go get github.com/gin-gonic

A continuación, actualice el archivo 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"))
}

En una situación ideal, la rotación /login toma las credenciales de un usuario, las compara con algún banco de datos y registra si las credenciales son válidas. Pero en esta API, sólo usamos una cuenta de usuario que definimos en memoria. Crea una cuenta de usuario en una "estructura". Añade esto al archivo 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",
}

Solicitud de acceso

Cuando se verifican los datos de un usuario, éste se registra y se genera una JWT con su nombre. Conseguiremos esto en la función Login() definida más abajo:

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

Recibimos la solicitud del usuario y, a continuación, la deserializamos para el "struct" del usuario. A continuación, comparamos el usuario de entrada con lo que hemos definido en la memoria. Si utilizamos un banco de datos, lo compararemos con un registro del banco de datos.

Para no desaprovechar la función de inicio de sesión, la lógica para crear una JWT es la siguiente CreateToken. Observe que la identificación del usuario se pasa a esta función. Se utiliza como una reivindicación para generar el JWT.

La función CreateToken hace uso del paquete dgrijalva/jwt-goPodemos instalarlo así:

go get github.com/dgrijalva/jwt-go

Vamos a definir la función 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
}

Definimos que el token sea válido sólo durante 15 minutos, y una vez invalidado, no podrá ser usado para ninguna solicitud de autenticación. Observe también que asimilamos el JWT utilizando una contraseña (ACCESS_SECRET) obtenida de nuestra variable de entorno. Es muy recomendable que esta contraseña no se exponga en su base de datos de código, sino que se muestre en el entorno tal y como se muestra a continuación. Puede guardarlo en un archivo .env.yml o como funcione para usted.

Hasta ahora, nuestro artículo main.go se parece a esto:

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
}

Ahora puede ejecutar la aplicación:

go run main.go

¡Ahora podemos experimentarlo y ver lo que conseguimos! Accede a tu API favorita y haz clic en el punto final de inicio de sesión:

[result]

Como se ve arriba, vamos a hacer un JWT que durará 15 minutos.

Lagunas de aplicación

Sim, nós podemos fazer o login de um usuário e gerar um JWT, mas há muitos erros com a implementação acima:

  1. El JWT sólo puede ser invalidado cuando expira. Una gran limitación a esto es que un usuario puede iniciar sesión y luego decidir salir inmediatamente, pero el JWT del usuario permanecerá válido hasta que se alcance el tiempo de expiración.

  2. El JWT puede ser pirateado y utilizado por un hacker sin que el usuario haga nada al respecto, hasta que el token expire.

  3. El usuario necesitará registrarse de nuevo tras la expiración del token, proporcionando así una mayor experiencia al usuario.

Podemos resolver los problemas mencionados de dos maneras:

  1. Usando una cámara de almacenamiento persistente para almacenar metadatos JWT. Esto nos permite invalidar un logotipo JWT cuando el usuario cierra la sesión, mejorando así la seguridad.

  2. Utilizando el concepto de actualizar el token para generar un nuevo token de acceso, en caso de que el token de acceso caduque, mejorando así la experiencia del usuario.

Usando Redis para almacenar metadatos de JWT

Una de las soluciones que ofrecemos ahora es salvar metadatos JWT en una campaña de persistencia. Esto puede hacerse en cualquier cámara de persistencia de contraseña, pero se recomienda encarecidamente la redistribución. Una vez que los JWTs que gestionamos tienen tiempo de expiración, redis tiene una característica que elimina automáticamente los datos cuyo tiempo de expiración se ha alcanzado. La redis también puede manipular varios escritos y escalar horizontalmente.

Como redis es un almacenamiento de tipo clave-valor, sus claves tienen que ser únicas, para conseguirlo, usaremos uuid como clave y usaremos el id del usuario como valor.

Además, vamos a instalar dos paquetes para usar:

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

También importaremos los que están en el archivo main.go assim:

import (

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

)

Nota: Esperamos que tengas instalada la redis en tu máquina local. En caso contrario, puedes parar y hacerlo antes de continuar.

Vamos a iniciar ahora la redistribución:

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

El cliente redis se inicia en la función init(). Esto asegura que cada vez que ejecutamos el archivo main.goredis se conecta automáticamente.

Cuando creamos un token a partir de este punto, generamos un uuid que se usará como uno de los token claims, igual que usamos el id del usuario como un claim en la implementación anterior.

Definir los Metadatos=

En nuestra propuesta de solución, en lugar de crear sólo un token, crearemos dos JWT:

  1. El token de acceso

  2. O Refresco de fichas

Para ello, será necesario definir una estructura que englobe estas definiciones de tokens, sus pruebas de validez y UUIDS:

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

El plazo de validez y los uuids son muy limitados porque se utilizan para salvar metadatos simbólicos en redes.

Ahora, vamos a actualizar la función CreateToken para tener este aspecto:

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
}

En la función siguiente, la ficha de acceso caduca a los 15 minutos y la ficha de recarga a los 7 días. También puedes observar que añadimos un uuid como reclamo a cada token.

Como el uuid es único cada vez que se crea, un usuario puede crear más de un token. Esto ocurre cuando un usuario se registra en diferentes dispositivos. El usuario también puede cerrar sesión en cualquiera de los dispositivos sin tener que desconectarse de todos ellos. ¡Qué legal!

Salvando metadatos de JWTs

Ahora vamos a enlazar la función que se utilizará para salvar los metadatos de los JWTs:

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
}

Pasamos a TokenDetails que contiene información sobre el tiempo de caducidad de los JWT y los uuids utilizados en la creación de JWT. Si se alcanza el tiempo de caducidad tanto para el token de actualización como para el token de acceso, el JWT se excluye automáticamente de la red.

Yo también uso Redily, una interfaz gráfica para redes. Es una gran herramienta. Puedes echar un vistazo abajo para ver cómo se almacenan los metadatos JWT en el par clave-valor.

[results]

Antes de probar de nuevo el inicio de sesión, seleccione la función CreateAuth() en la función Login(). Actualizar la función de inicio de sesión:

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

Podemos intentar entrar de nuevo en el sistema. Salve el archivo main.go y ejecútelo. Cuando el Postman haya iniciado sesión, deberemos haberlo hecho:

[postman result]

¡Excelente! Temos tanto o access_token como o refresh_token, e também temos metadados simbólicos persistidos no redis.

Criando un Todo

Ahora podemos proceder a las solicitudes que requieran autenticación mediante JWT.

Una de las solicitudes no autenticadas en esta API es la creación de todas las solicitudes.

Primero vamos a definir una estructura Todo:

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

Al ejecutar cualquier solicitud autenticada, necesitamos validar el token pasado en el cabezal de autenticación para ver si es válido. Para ello, debemos definir algunas funciones de ayuda.

Primero tenemos que extraer el token del cabezal de la petición usando la función 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 ""
}

A continuación, comprobaremos el token:

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
}

Nos encontramos ExtractToken dentro de la función VerifyToken para obtener la cadena de tokens, después procederemos a la verificación del método de assinatura.

A continuación, comprobaremos la validez de este token, si sigue siendo útil o si ha caducado, utilizando la función 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
}

También vamos a extraer los metadatos del token que se obtendrán en nuestra tienda redis que montamos anteriormente. Para extraer el token, definimos la función 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 función ExtractTokenMetadata devuelve un AccessDetails (que es una estructura). Esta estructura contiene los metadatos (access_uuid e user_id) que necesitamos buscar en la red. Si tiene alguna razón para no conseguir los metadatos de este token, la solicitud se interrumpirá con un mensaje de error.

La estructura AccessDetails mencionado anteriormente se parece a esto:

type AccessDetails struct {
    AccessUuid string
    UserId   uint64
}

También mencionamos la búsqueda de los metadatos del token en las redes. Vamos a definir una función que nos permita hacerlo:

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 función ExtractTokenMetadatay, a continuación, procura la redistribución. Si el registro no se encuentra, puede significar que el token ha caducado, o que se ha producido un error.

Vamos finalmente a vincular la función CreateTodo para entender mejor la implementación de las funciones anteriores:

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

Como ya vimos, usamos el ExtractTokenMetadata para extraer los metadatos de JWT que se utilizan en FetchAuth para comprobar si los metadatos siguen existiendo en nuestro almacén de redis. Si todo va bien, Todo puede guardarse en el banco de datos, pero optamos por devolvérselo a la persona que llama.

Vamos a actualizar el main() para incluir la función CreateTodo:

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

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

Para probarlo CreateTodoinicia sesión y copia el access_token y añádelo al campo "Bearer Token Field" como este:

[bearer token]

A continuación, añada un título al cuerpo de la petición para crear todo y realice una petición POST al endpoint /todocomo se muestra a continuación:

[result]

El intento de crear un ToDo sin acceso será denegado:

[denied]

Solicitud de cierre de sesión

Hasta ahora, hemos visto cómo se usa un JWT para autenticar una solicitud. Cuando un usuario cierra sesión, revocamos/invalidamos instantáneamente su JWT. Esto es posible apagando los metadatos del JWT de nuestro almacén de redis.

Ahora vamos a definir una función que nos permita excluir metadatos JWT de redis:

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

La función anterior apagará el registro en redis que corresponda al uuid pasado como parámetro.

La función Logout tiene 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 la función Logoutextraemos primero los metadatos del JWT. Si esto sucede, procedemos a eliminar estos metadatos, convirtiendo el JWT en inválido inmediatamente.

Antes de probarlo, actualice el archivo main.go para incluir el endpoint de logout como éste:

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

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

Forneça um access_token válido asociado a un usuario y, a continuación, cierra la sesión del usuario. Deja de añadir el access_token a Authorization Bearer Token y, a continuación, accede al punto final de cierre de sesión:

[logout endpoint]

Ahora el usuario está desconectado, y ninguna otra petición puede ser enviada con ese JWT de nuevo, una vez que ha sido invalidada inmediatamente. Esta implementación es más segura que esperar a que un JWT caduque después de que un usuario se desconecte del sistema.

Protegiendo Rotas Autenticadas

Tenemos dos rutas que requieren autorización:/login e /logout. Ahora mismo, con o sin autorización, cualquier persona puede acceder a estas rutas. Vamos a cambiar esto.

Precisaremos definir la función TokenAuthMiddleware() para asegurar estas rotaciones:

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()
  }
}

Como se ve arriba, usamos la función TokenValid() (definida anteriormente) para comprobar si el token sigue siendo válido o ha caducado. La función se usará en las rutas autenticadas para protegerlas.

Vamos a actualizar main.go para incluir este middleware:

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

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

Atulizando os Tokens

Actualmente, podemos crear, utilizar y revocar JWTs. En una aplicación que envuelva una interfaz de usuario, ¿qué ocurre si el token de acceso caduca y el usuario necesita hacer un pedido autenticado? ¿Se desautorizará al usuario y se le pedirá que inicie sesión de nuevo? Infelizmente, este será el caso. Pero esto puede evitarse utilizando un token de actualización. El usuario no necesita registrarse de nuevo.

El token de actualización creado junto con el token de acceso se utilizará para crear nuevos pares de tokens de acceso y actualización.

Usando JavaScript para consumir nuestros terminales API, podemos actualizar los JWTs fácilmente usando interceptores axiales. En nuestra API, necesitaremos enviar una solicitud POST con un refresh_token como cuerpo del endpoint /token/refresh.

Vamos primero a crear la función 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")
  }
}

Como en esta función ocurren muchas cosas, vamos a intentar entender el flujo.

  • En primer lugar tomamos el refresh_token do corpo de request.

  • A continuación, comprobamos el método de asimilación del token.

  • A continuación, comprobamos si el token sigue siendo válido.

  • O refresh_uuid y o user_id son entonces extraídos, que son metadatos utilizados como reclamos para crear el token de actualización.

  • A continuación, buscamos los metadatos en redis store y los apagamos usando el refresh_uid como llave.

  • A continuación, creamos un nuevo par de tokens de acceso y actualización que ahora se utilizarán para solicitudes futuras.

  • Los metadatos de los tokens de acceso y de actualización están a la vista en la red.

  • Los tokens creados se devuelven a la persona que llama.

  • En otra declaración, si el token de actualización no es válido, el usuario no tendrá permiso para crear un nuevo par de tokens. Tendrás que iniciar sesión de nuevo para obtener nuevos tokens.

A continuación, añada la rotación de actualización de fichas a la función main():

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

Probando el punto de mira con un refresh_token válido:

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

Y creamos con éxito nuevas parejas de fichas. ¡¡Beleza!! 😎.

Enviar mensajes usando la API de mensajes de Vonage

Notificaremos a los usuarios cada vez que creen un ToDo usando la API de Mensagens Vonage.

Puede definir su clave API y su contraseña en varios formatos y luego utilizarlas en este archivo de esta forma:

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

A continuación, definiremos algunas estructuras que contienen información sobre el remitente, el receptor y el contenido del mensaje:

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"`
}

A continuación definimos la función de enviar un mensaje a un usuario de más abajo:

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
}

En la función anterior, el número To es el número del usuario, mientras que el número From debe comprarse a través de tu panel de control de Vonage.

Certifique-se de ter suas variáveis NEXMO_API_KEY e NEXMO_API_SECRET definidas en su archivo de variables de entorno.

Actualizamos ahora la función CreateTodo para incluir la función SendMessage que acababa de ser definida, pasando por los parámetros necesarios:

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

Asegúrate de que se ha proporcionado un número de teléfono válido para que puedas recibir el mensaje cuando quieras crear todo.

Conclusão

Verás cómo puedes crear e invalidar un JWT. También verás cómo puedes integrar Vonage Messages API en tu aplicación Golang para enviar notificaciones. Para obtener más información sobre las mejores prácticas y el uso de un JWT, no dejes de consultar este repositorio de GitHub. Puedes instalar esta aplicación y usar un banco de datos real para mantener a los usuarios y a todos, y también puedes usar React o Vue.js para construir una interfaz. Aquí es donde realmente apreciarás el recurso Refresh Token con la ayuda de Axios Interceptors.

Compartir:

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.