Introdução ao JWT

JWT (JSON Web Token) é um método conciso e autossuficiente de autenticação na web. Ele consiste em três partes: o cabeçalho, a carga útil e a assinatura. O cabeçalho contém o tipo do token e informações sobre o algoritmo de criptografia, a payload contém os dados a serem transmitidos (geralmente incluindo algumas informações de permissão e identificação do usuário) e a assinatura é usada para verificar a integridade e validade do token.

JWT é principalmente usado em dois cenários:

  1. Autenticação: Uma vez que um usuário faz login, cada solicitação subsequente incluirá um JWT, permitindo que o usuário acesse rotas, serviços e recursos permitidos.
  2. Troca de informações: O JWT é um bom método para transmitir informações de forma segura entre as partes, já que podem ser assinados digitalmente, por exemplo, usando pares de chaves pública/privada.

Formato dos Dados JWT

O JWT é uma forma compacta e segura de representar informações a serem trocadas entre as partes. Na verdade, um JWT consiste em três partes, separadas por pontos (.``): Cabeçalho.Payload.Assinatura`. A seguir, detalharemos os formatos de dados dessas três partes.

1. Cabeçalho

O cabeçalho normalmente consiste em duas partes: o tipo do token, geralmente JWT, e o algoritmo de assinatura ou criptografia usado, como HMAC SHA256 ou RSA. O cabeçalho é representado em JSON e, em seguida, codificado em uma string usando Base64Url. Por exemplo:

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

Após a codificação, você pode obter uma string semelhante a esta:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Payload

A carga útil consiste em uma série de declarações, que são informações sobre a entidade (geralmente um usuário) e outros dados. A payload pode incluir várias declarações predefinidas (também conhecidas como Declarações Registradas) e declarações personalizadas (Declarações Privadas).

As declarações predefinidas podem incluir:

  • iss (Emissor): Emissor
  • exp (Tempo de Expiração): Tempo de expiração
  • sub (Assunto): Assunto
  • aud (Público): Público
  • iat (Emitido em): Tempo de emissão
  • nbf (Não Antes de): Tempo efetivo
  • jti (ID do JWT): Identidade única do JWT

Um exemplo de payload pode ser assim (em formato JSON):

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

Essas informações também serão codificadas com Base64Url, e você pode obter uma string semelhante a esta:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0

3. Assinatura

A seção de assinatura é usada para assinar as duas strings codificadas mencionadas acima, a fim de verificar que a mensagem não foi alterada durante a transmissão. Primeiramente, você precisa especificar uma chave (se estiver usando o algoritmo HMAC SHA256) e, em seguida, usar o algoritmo especificado no cabeçalho para assinar o cabeçalho e a carga útil.

Por exemplo, se você tiver o seguinte cabeçalho e payload:

CabeçalhoCodificado.CargaÚtilCodificada

Um pseudocódigo para assiná-los usando uma chave poderia ser:

HMACSHA256(base64UrlEncode(cabeçalho) + "." + base64UrlEncode(cargaútil), chaveSecreta)

A string assinada resultante poderia ser:

dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

JWT Completo

Combinando essas três partes com um ponto (.) como separador forma o JWT completo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

O padrão JWT é altamente flexível e pode armazenar qualquer informação necessária na payload (mas, com o objetivo de reduzir o tamanho do JWT e por motivos de segurança, informações sensíveis não devem ser armazenadas) e garante a integridade dessas informações por meio da assinatura.

Instalando a biblioteca JWT em Go

A comunidade Go fornece um pacote github.com/golang-jwt/jwt para lidar com JWT. A instalação desta biblioteca é muito simples, basta executar o seguinte comando no diretório do seu projeto:

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

Uma vez instalada, você pode incluí-la em seu import da seguinte forma:

import "github.com/golang-jwt/jwt/v5"

Criando um token simples

Para criar um token JWT simples usando a linguagem Go e o algoritmo HS256, você precisa seguir estes passos:

Primeiro, gere um novo objeto Token, usando o algoritmo HS256:

token := jwt.New(jwt.SigningMethodHS256)

Em seguida, use o método SignedString para gerar uma representação de string deste token, passando a chave que você irá usar para assinar.

var minhaChaveDeAssinatura = []byte("seu-segredo-de-256-bits")
strToken, err := token.SignedString(minhaChaveDeAssinatura)
if err != nil {
    log.Fatalf("Ocorreu um erro: %v", err)
}
fmt.Println(strToken)

Isso irá gerar um token simples sem nenhuma reivindicação.

Criando um token com parâmetros

Uma das principais funções do JWT é carregar informações. Estas informações são codificadas no Token através de reivindicações. Por exemplo, vamos criar reivindicações personalizadas:

// Estrutura de Reivindicações Personalizadas
type MinhasReivindicações struct {
	jwt.RegisteredClaims
	NomeDeUsuário string `json:"username"`
	Admin    bool   `json:"admin"`
}

// Criar um token com reivindicações personalizadas
reivindicacoes := MinhasReivindicações{
    RegisteredClaims: jwt.RegisteredClaims{},
    NomeDeUsuário: "meu_nome_de_usuário",
	Admin: true,
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, reivindicacoes)

// Chave secreta para assinar
var minhaChaveDeAssinatura = []byte("seu-segredo-de-256-bits")

// Gerar o token em formato de string
strToken, err := token.SignedString(minhaChaveDeAssinatura)
if err != nil {
    log.Fatalf("Ocorreu um erro: %v", err)
}

fmt.Println(strToken)

No código acima, definimos a estrutura MinhasReivindicações para conter reivindicações registradas, bem como algumas informações personalizadas. Em seguida, ainda usamos SignedString para gerar a string do token.

Analisando e validando o token

Analisar e validar o JWT é muito importante e você pode fazer isso da seguinte maneira:

// Vamos usar a mesma estrutura MinhasReivindicações
tokenString := "sua-string-JWT"

// Precisamos definir uma função que o pacote jwt usará para analisar a tokenString
funcaoChave := func(t *jwt.Token) (interface{}, error) {
	// Verificar se o método de assinatura esperado está sendo usado
	if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
	    return nil, fmt.Errorf("Método de assinatura inesperado: %v", t.Header["alg"])
	}
	// Retornar a chave secreta para a token jwt, no formato []byte, consistente com a chave usada para assinar anteriormente
	return minhaChaveDeAssinatura, nil
}

// Analisando o token
reivindicacoes := &MinhasReivindicações{}
tokenAnalisado, err := jwt.ParseWithClaims(tokenString, reivindicacoes, funcaoChave)
if err != nil {
	log.Fatalf("Erro de análise: %v", err)
}

if !tokenAnalisado.Valid {
    log.Fatalf("Token inválido")
}

// Nesta fase, o tokenAnalisado foi verificado e podemos ler as reivindicações
fmt.Printf("Usuário: %s, Admin: %v\n", reivindicacoes.NomeDeUsuário, reivindicacoes.Admin)

No código acima, fornecemos a string do token, a instância MinhasReivindicações e a função de chave funcaoChave para a função jwt.ParseWithClaims. Esta função irá validar a assinatura e analisar o token, preenchendo a variável reivindicacoes se o token for válido.