https://a.storyblok.com/f/270183/33989/f53cfeec90/blog_jwt-golang_authentification_1200x600-2.png

Usando JWT para autenticação em um aplicativo em Golang

Publicado em August 20, 2021

Tempo de leitura: 14 minutos

Introdução

Um JSON Web Token (JWT) é uma forma compacta e independente de transmitir informações com segurança entre as partes na forma de um objeto JSON, e é comumente utilizado por desenvolvedores em suas APIs. Os JWTs são populares porque:

  1. Um JWT é sem estado. Ou seja, ele não precisa ser armazenado em um banco de dados (camada de persistência), ao contrário dos tokens opacos.

  2. A assinatura de um JWT nunca é decodificada depois de gerada, garantindo assim que o token seja seguro e protegido.

  3. Um JWT pode ser configurado para expirar após um determinado período de tempo. Isso ajuda a minimizar ou eliminar totalmente qualquer dano que possa ser causado por um hacker, caso o token seja comprometido.

Neste tutorial, vou demonstrar como criar, usar e invalidar um JWT em uma API RESTful simples, utilizando Golang e a API de Mensagens da Vonage.

Conta API da Vonage

Para concluir este tutorial, você precisará de uma conta da API da Vonage. Se ainda não tiver uma, pode se cadastrar hoje mesmo e começar a desenvolver usando créditos gratuitos. Assim que tiver uma conta, você poderá encontrar sua chave de API e seu segredo de API na parte superior do Painel da API da Vonage.

Este tutorial também utiliza um número de telefone virtual. Para adquirir um, acesse Números > Comprar Números e procure um que atenda às suas necessidades. Se você acabou de se cadastrar, o custo inicial de um número será facilmente coberto pelo seu crédito disponível.

Sign Up

O que é um JWT?

Um JWT é composto por três partes:

  • Cabeçalho: o tipo de token e o algoritmo de assinatura utilizado. O tipo de token pode ser “JWT”, enquanto o algoritmo de assinatura pode ser HMAC ou SHA256.

  • Payload: a segunda parte do token que contém as reivindicações. Essas reivindicações incluem dados específicos da aplicação (por exemplo, identificação do usuário, nome de usuário), tempo de validade do token (expiração), emissor(es), assunto(s) e assim por diante.

  • Assinatura: o “header” codificado, o “payload” codificado e uma senha fornecida por você são usados para criar a assinatura.

Vamos usar um token simples para entender os conceitos acima.

Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3V1aWQiOiIxZGQ5MDEwYy00MzI4LTRmZjMtYjllNi05NDRkODQ4ZTkzNzUiLCJhdXRob3JpemVkIjp0cnVlLCJ1c2VyX2lkIjo3fQ.Qy8l-9GUFsXQm4jqgswAYTAX9F4cngrl28WJVYNDwtM

Não se preocupe, o token é inválido e, portanto, não funcionará em nenhum aplicativo de produção.

Você pode navegar pelo site jwt.to e testar se a assinatura do token é válida ou não. Use o “HS512” como algoritmo. Você receberá a mensagem “Signature Verified” (Assinatura verificada):

[JWT.IO Example]

Para gerar a assinatura, seu aplicativo precisará fornecer uma chave. Essa chave garante que a assinatura permaneça segura — mesmo quando o JWT é decodificado, a assinatura permanece criptografada. É altamente recomendável usar sempre uma senha ao criar um JWT.

Tipos de token

Como um JWT pode ser configurado para expirar (ser invalidado) após um determinado período de tempo, dois tokens serão considerados nesta solicitação:

  • Token de acesso: Um token de acesso é usado para solicitações que exigem autenticação. Ele é normalmente incluído no cabeçalho da solicitação. Recomenda-se que um token de acesso tenha uma validade curta, digamos, 15 minutos. Atribuir um período curto de validade a um token de acesso pode evitar danos graves caso o token de um usuário seja adulterado ou hackeado. O hacker tem apenas 15 minutos ou menos para realizar suas operações antes que o token seja invalidado.

  • Token de atualização: Um token de atualização tem uma validade mais longa, geralmente de 7 dias. Esse token é usado para gerar novos tokens de acesso e de atualização. Caso o token de acesso expire, novos conjuntos de tokens de acesso e de atualização são criados quando a rota do token de atualização é acessada (a partir do nosso aplicativo).

Onde armazenar um JWT

Para uma aplicação em ambiente de produção, é altamente recomendável armazenar JWTs em um cookie HttpOnly. Para isso, ao enviar o cookie gerado pelo backend para o frontend (cliente), um sinalizador HttpOnly é enviada junto com o cookie, instruindo o navegador a não exibir o cookie por meio de scripts do lado do cliente. Isso pode prevenir ataques de XSS (Cross Site Scripting). O JWT também pode ser armazenado no armazenamento local do navegador ou no armazenamento de sessão. Armazenar um JWT dessa forma pode expô-lo a vários ataques, como o XSS mencionado acima; por isso, geralmente é menos seguro quando comparado ao uso da técnica do “cookie HttpOnly”.

A Aplicação

Vamos considerar uma API RESTful ToDo.

Crie um diretório chamado jwt-todo, depois inicialize go.mod para gerenciamento das dependências. O go.mod é inicializado usando:

go mod init jwt-todo

Agora, crie um arquivo main.go dentro do diretório raiz /jwt-todoe adicione o seguinte a ele:

package main

func main() {}

Usaremos o Gin para o roteamento e o tratamento de solicitações HTTP. O Gin Framework ajuda a reduzir o código repetitivo e é muito eficiente na construção de APIs escaláveis.

Você pode instalar o gin, caso ainda não o tenha feito, usando:

go get github.com/gin-gonic

Em seguida, atualize o 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"))
}

Em uma situação ideal, a rota /login recebe as credenciais de um usuário, compara-as com algum banco de dados e as registra caso sejam válidas. Mas, nesta API, usaremos apenas um usuário de exemplo que definiremos na memória. Crie um usuário de exemplo em uma “struct”. Adicione isso ao arquivo 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",
}

Solicitação de login

Quando os dados de um usuário são verificados, ele é autenticado e um JWT é gerado em seu nome. Faremos isso na função Login() definida abaixo:

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

Recebemos a solicitação do usuário e, em seguida, a deserializamos para a “struct” do usuário. Comparamos então o usuário recebido com aquele que definimos na memória. Se estivéssemos utilizando um banco de dados, teríamos comparado com um registro no banco de dados.

Para não tornar a função de login obsoleta, a lógica para gerar um JWT é tratada pela CreateToken. Observe que a identificação do usuário é passada para essa função. Ela é usada como uma reivindicação ao gerar o JWT.

A função CreateToken utiliza o pacote dgrijalva/jwt-go; podemos instalá-lo da seguinte maneira:

go get github.com/dgrijalva/jwt-go

Vamos definir a função 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 o token seja válido apenas por 15 minutos; logo após esse período, ele será invalidado e não poderá mais ser usado para nenhuma solicitação de autenticação. Observe também que assinamos o JWT utilizando uma senha (ACCESS_SECRET) obtida de nossa variável de ambiente. É altamente recomendável que essa senha não seja exposta em seu código-fonte, mas sim obtida do ambiente, como fizemos acima. Você pode salvá-la em um .env, `.yml ou da maneira que for mais conveniente para você.

Até agora, nosso arquivo main.go está assim:

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
}

Agora podemos executar o aplicativo:

go run main.go

Agora podemos testá-lo e ver o que conseguimos! Ative sua ferramenta de API preferida e clique no endpoint de login:

[result]

Como visto acima, geramos um JWT com validade de 15 minutos.

Lacunas na implementação

Sim, podemos fazer o login de um usuário e gerar um JWT, mas há muitos erros na implementação acima:

  1. O JWT só pode ser invalidado quando expirar. Uma grande limitação disso é: um usuário pode fazer login e, em seguida, decidir sair imediatamente, mas o JWT do usuário permanece válido até que o prazo de validade seja atingido.

  2. O JWT pode ser hackeado e usado por um hacker sem que o usuário faça nada a respeito, até que o token expire.

  3. O usuário precisará se cadastrar novamente após o vencimento do token, o que resultará em uma experiência negativa para o usuário.

Podemos resolver os problemas mencionados acima de duas maneiras:

  1. Utilizando uma camada de armazenamento persistente para armazenar metadados de JWT. Isso nos permitirá invalidar um JWT no exato momento em que o usuário sair da conta, melhorando assim a segurança.

  2. Utilizando o conceito de atualização do token para gerar um novo token de acesso, caso o token de acesso expire, melhorando assim a experiência do usuário.

Usando o Redis para armazenar metadados de JWT

Uma das soluções que oferecemos acima é salvar os metadados do JWT em uma camada de persistência. Isso pode ser feito em qualquer camada de persistência de sua escolha, mas o Redis é altamente recomendado. Como os JWTs que geramos têm prazo de validade, o Redis possui um recurso que elimina automaticamente os dados cujo prazo de validade foi atingido. O Redis também é capaz de processar um grande volume de gravações e pode ser escalado horizontalmente.

Como o Redis é um banco de dados do tipo chave-valor, suas chaves precisam ser únicas. Para isso, usaremos um UUID como chave e o ID do usuário como valor.

Portanto, vamos instalar dois pacotes para usar:

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

Também importaremos os que estão no arquivo main.go assim:

import (

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

)

Nota: Espera-se que você tenha o Redis instalado em seu computador. Caso contrário, você pode interromper o processo e fazer a instalação antes de continuar.

Vamos agora iniciar o Redis:

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

O cliente Redis é inicializado na função init(). Isso garante que, sempre que executarmos o arquivo main.go, o Redis seja conectado automaticamente.

Quando criamos um token a partir deste ponto, geramos um UUID que será usado como um dos claims do token, da mesma forma que usamos o ID do usuário como um claim na implementação anterior.

Definir os metadados=

Na solução que propomos, em vez de criar apenas um token, precisaremos criar dois JWTs:

  1. O token de acesso

  2. Atualização do token

Para isso, será necessário definir uma estrutura que contenha essas definições de tokens, seus prazos de validade e UUIDS:

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

O prazo de validade e os UUIDs são muito úteis, pois serão utilizados ao salvar metadados simbólicos no Redis.

Agora, vamos atualizar a função CreateToken para que fique assim:

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
}

Na função acima, o token de acesso expira após 15 minutos e o token de atualização expira após 7 dias. Você também pode observar que adicionamos um UUID como claim a cada token.

Como o UUID é único sempre que é gerado, um usuário pode criar mais de um token. Isso ocorre quando um usuário está conectado em diferentes dispositivos. O usuário também pode sair de qualquer um dos dispositivos sem que isso o desconecte de todos os demais. Que legal!

Salvando metadados de JWTs

Vamos agora chamar a função que será usada para salvar os metadados dos 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
}

Passamos pelo TokenDetails que contêm informações sobre o tempo de validade dos JWTs e os UUIDs utilizados na criação dos JWTs. Se o tempo de validade for atingido tanto para o Token de Atualização quanto para o Token de Acesso, o JWT é automaticamente excluído do Redis.

Eu, pessoalmente, uso o Redily, uma interface gráfica para o Redis. É uma boa ferramenta. Você pode dar uma olhada abaixo para ver como os metadados do JWT são armazenados no formato chave-valor.

[results]

Antes de testarmos o login novamente, precisaremos chamar a função CreateAuth() na função Login(). Atualizar a função 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)
}

Podemos tentar acessar o sistema novamente. Salve o arquivo main.go e execute-o. Quando o Postman realizar o login, já devemos ter feito o seguinte:

[postman result]

Excelente! Temos tanto o access_token quanto o refresh_token, além de metadados simbólicos armazenados no Redis.

Criando um Todo

Agora podemos processar as solicitações que exigem autenticação usando o JWT.

Uma das solicitações não autenticadas nesta API é a criação de qualquer solicitação.

Primeiro, vamos definir uma estrutura Todo:

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

Ao executar qualquer solicitação autenticada, precisamos validar o token enviado no cabeçalho de autenticação para verificar se ele é válido. Precisamos definir algumas funções auxiliares para isso.

Primeiro, precisamos extrair o token do cabeçalho da solicitação usando a função 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 ""
}

Em seguida, verificaremos o 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
}

Chamamos o ExtractToken dentro da função VerifyToken para obter a string do token e, em seguida, verificamos o método de assinatura.

Em seguida, verificaremos a validade desse token — se ele ainda é válido ou se já expirou — usando a função 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
}

Também vamos extrair os metadados do token, que serão buscados em nosso banco de dados Redis, que configuramos anteriormente. Para extrair o token, definimos a função 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
}

A função ExtractTokenMetadata retorna uma AccessDetails (que é uma estrutura). Essa estrutura contém os metadados (access_uuid e user_id) de que precisaremos para realizar uma busca no Redis. Se, por algum motivo, não for possível obter os metadados desse token, a solicitação é interrompida com uma mensagem de erro.

A estrutura AccessDetails mencionada acima tem a seguinte aparência:

type AccessDetails struct {
    AccessUuid string
    UserId   uint64
}

Também mencionamos a consulta aos metadados do token no Redis. Vamos definir uma função que nos permitirá fazer isso:

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 da função ExtractTokenMetadatae, em seguida, procura no Redis. Se o registro não for encontrado, isso pode significar que o token expirou; portanto, é lançado um erro.

Vamos finalmente chamar a função CreateTodo para entender melhor a implementação das funções acima:

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 vimos, chamamos o ExtractTokenMetadata para extrair os metadados do JWT que é usado no FetchAuth para verificar se os metadados ainda existem em nosso armazenamento Redis. Se tudo estiver correto, o Todo pode então ser salvo no banco de dados, mas optamos por devolvê-lo ao chamador.

Vamos atualizar o main() para incluir a função CreateTodo:

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

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

Para testar o CreateTodo, faça o login e copie o access_token e adicione-o ao campo “Bearer Token Field” da seguinte forma:

[bearer token]

Em seguida, adicione um título ao corpo da solicitação para formar um todo e envie uma solicitação POST para o endpoint /todo, conforme mostrado abaixo:

[result]

A tentativa de criar uma tarefa sem acesso será negada:

[denied]

Solicitação de logout

Até agora, vimos como um JWT é usado para fazer uma solicitação autenticada. Quando um usuário faz o logout, revogamos/invalidamos instantaneamente seu JWT. Isso é possível apagando os metadados do JWT do nosso armazenamento Redis.

Vamos agora definir uma função que nos permita excluir metadados JWT do Redis:

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

A função acima apagará o registro no Redis que corresponde ao uuid passado como parâmetro.

A função Logout é assim:

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

Na função Logout, primeiro extraímos os metadados do JWT. Se for bem-sucedido, procedemos com a remoção desses metadados, tornando assim o JWT inválido imediatamente.

Antes de testar, atualize o arquivo main.go para incluir o endpoint logout da seguinte forma:

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

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

Insira uma access_token válido associado a um usuário e, em seguida, faça o logout do usuário. Lembre-se de adicionar o access_token ao Authorization Bearer Token e, em seguida, acesse o endpoint de logout:

[logout endpoint]

Agora o usuário está desconectado, e nenhuma outra solicitação pode ser feita com esse JWT novamente, uma vez que ele é imediatamente invalidado. Essa implementação é mais segura do que esperar que um JWT expire depois que o usuário se desconecta do sistema.

Protegendo rotas autenticadas

Temos duas rotas que exigem autenticação:/login e /logout. No momento, com ou sem autenticação, qualquer pessoa pode acessar essas rotas. Vamos mudar isso.

Precisaremos definir a função TokenAuthMiddleware() para garantir essas rotas:

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 visto acima, chamamos a função TokenValid() (definida anteriormente) para verificar se o token ainda é válido ou se expirou. A função será usada nas rotas autenticadas para protegê-las.

Vamos agora atualizar o main.go para incluir esse middleware:

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

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

Atualizando os tokens

Até o momento, podemos criar, utilizar e revogar JWTs. Em um aplicativo que envolva uma interface de usuário, o que acontece se o token de acesso expirar e o usuário precisar fazer uma solicitação autenticada? O usuário será desautenticado e será obrigado a fazer login novamente? Infelizmente, isso é o que acontecerá. Mas isso pode ser evitado usando o conceito de um token de atualização. O usuário não precisa fazer login novamente.

O token de atualização criado junto com o token de acesso será usado para criar novos pares de tokens de acesso e de atualização.

Usando JavaScript para acessar nossas APIs, podemos atualizar os JWTs facilmente por meio de interceptadores do Axios. Em nossa API, precisaremos enviar uma solicitação POST com um refresh_token como corpo para o endpoint /token/refresh.

Vamos primeiro criar a função 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")
  }
}

Embora haja muita coisa acontecendo nessa função, vamos tentar entender o fluxo.

  • Primeiro, pegamos o refresh_token do corpo da solicitação.

  • Em seguida, verificamos o método de assinatura do token.

  • Em seguida, verificamos se o token ainda é válido.

  • O refresh_uuid e o user_id são então extraídos; trata-se de metadados usados como claims ao criar o token de atualização.

  • Em seguida, buscamos os metadados no Redis Store e os apagamos usando a refresh_uid como chave.

  • Em seguida, criamos um novo par de tokens de acesso e de atualização, que agora serão usados para futuras solicitações.

  • Os metadados dos tokens de acesso e de atualização são armazenados no Redis.

  • Os tokens criados são devolvidos ao chamador.

  • Por outro lado, se o token de atualização não for válido, o usuário não terá permissão para criar um novo par de tokens. Será necessário fazer um novo login para obter novos tokens.

Em seguida, adicione a rota de atualização de tokens na função main():

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

Testando o empoint com um refresh_token válido:

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

E criamos com sucesso novos pares de fichas. Que legal!! 😎.

Enviar mensagens usando a API de Mensagens da Vonage

Vamos notificar os usuários sempre que eles criarem uma tarefa usando a API de mensagens da Vonage.

Você pode definir sua chave de API e sua senha em uma variável `env` e, em seguida, usá-las neste arquivo da seguinte maneira:

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

A seguir, definiremos algumas estruturas que contêm informações sobre o remetente, o destinatário e o conteúdo da mensagem:

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 seguir, definimos a função para enviar uma mensagem a um usuário, conforme mostrado abaixo:

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
}

Na função acima, o número “To” é o número do usuário, enquanto o número “From” deve ser adquirido por meio do seu painel de controle da Vonage.

Certifique-se de ter suas variáveis NEXMO_API_KEY e NEXMO_API_SECRET definidas em seu arquivo de variáveis de ambiente.

Atualizamos, então, a função CreateTodo para incluir a função SendMessage que acabou de ser definida, passando os parâmetros necessários:

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

Certifique-se de fornecer um número de telefone válido para que você possa receber a mensagem ao tentar criar uma conta.

Conclusão

Você viu como é possível criar e invalidar um JWT. Você também viu como integrar a Messages API do Vonage à sua aplicação em Golang para enviar notificações. Para mais informações sobre as melhores práticas e o uso de um JWT, não deixe de conferir este repositório do GitHub. Você pode ampliar essa aplicação e usar um banco de dados real para armazenar usuários e outros dados, além de poder usar React ou Vue.js para construir um front-end. É aí que você realmente vai apreciar o recurso Refresh Token com a ajuda dos Axios Interceptors.

Compartilhar:

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

Victor Steven é um desenvolvedor full-stack autodidata que adora pesquisar maneiras diferentes de fazer as coisas. Ele é formado em Engenharia e tem mais de 5 anos de experiência como desenvolvedor de software. Steven tem grande interesse em projetar e criar APIs escaláveis e gosta de escrever sobre suas descobertas no campo do desenvolvimento de software.