No desenvolvimento de aplicações web modernas, a segurança é um componente crítico. JSON Web Tokens (JWTs) emergiram como um padrão robusto para gerenciar autenticação e autorização entre cliente e servidor. Este artigo aborda o uso do JWT em aplicações Delphi utilizando a biblioteca JOSE de Paolo Rossi e o middleware Horse JWT.
Afinal, o que é JWT?
O uso de JWT (JSON Web Tokens) tem se popularizado como um método eficiente para gerenciamento de autenticação e autorização em aplicações web e móveis. Há várias razões para sua adoção, sendo as principais:
1. Simplicidade e Autossuficiência
JWTs são autossuficientes, contendo todas as informações necessárias sobre o usuário. Isso inclui quem é o usuário (claims de identidade), quais permissões ele possui (claims de autorização) e outros dados necessários. Uma vez que o token é emitido, ele pode ser verificado e validado sem a necessidade de acessar novamente o banco de dados ou armazenamento de sessão. Isso é particularmente útil em ambientes distribuídos, como microserviços, onde cada serviço pode validar o token independentemente.
2. Compactação
JWTs são compactos e podem ser facilmente transmitidos através de URLs, parâmetros POST, ou cabeçalhos HTTP. Isso os torna ideais para transmissões em redes onde a largura de banda pode ser uma limitação, como em dispositivos móveis ou em serviços que requerem alta performance.
3. Segurança
JWTs oferecem múltiplas opções de segurança. Eles podem ser assinados usando um segredo (com HMAC) ou um par de chaves pública/privada (usando RSA ou ECDSA). Isso garante que apenas a parte que possui a chave secreta ou a chave privada pode gerar um token válido. Além disso, é possível encriptar todo o token para proporcionar privacidade entre as partes comunicantes.
4. Desempenho
Ao reduzir a necessidade de ir ao banco de dados para cada operação de autenticação, JWTs ajudam a diminuir a carga no servidor de banco de dados e melhoram a latência geral das solicitações. Isso pode ser crucial para aplicações de alto desempenho e alta escalabilidade.
5. Facilidade de Uso em Diferentes Domínios
JWTs funcionam bem em cenários de Single Sign-On (SSO), pois são facilmente usados entre diferentes domínios e aplicações. Isso os torna uma escolha eficiente para grandes sistemas distribuídos onde um usuário pode interagir com múltiplas aplicações ou serviços que necessitam de informações de autenticação compartilhada.
6. Padronização e Comunidade
Sendo um padrão aberto, JWT é apoiado por uma ampla variedade de bibliotecas e frameworks em praticamente todas as linguagens de programação e plataformas, garantindo que há amplo suporte e recursos disponíveis para desenvolvedores.
7. Facilidade de Integração com Outros Sistemas
Os JWTs podem ser facilmente integrados com sistemas existentes e novos, incluindo APIs de terceiros que suportam autenticação baseada em token. Isso torna mais simples a expansão de serviços e a integração com ecossistemas maiores.
Anatomia do JWT
A anatomia de um JSON Web Token (JWT) é fundamental para entender como ele funciona e como é usado em sistemas de autenticação e autorização. Um JWT é dividido em três partes distintas, cada uma codificada em Base64 e separada por pontos (.):
1. Header
O cabeçalho do JWT geralmente consiste em dois componentes principais:
- alg: Este é o algoritmo de assinatura utilizado. Pode ser, por exemplo, HS256 para HMAC SHA-256 ou RS256 para RSA com SHA-256.
- typ: Este campo especifica o tipo do token, que é JWT.
1 2 3 4 5 6 |
{ "alg": "HS256", "typ": "JWT" } |
Este cabeçalho é então codificado em Base64, resultando em uma string que forma a primeira parte do token.
2. Payload
O payload contém as “claims” ou afirmações sobre o sujeito (geralmente um usuário) e outras informações necessárias. Existem três tipos de claims:
- Registered claims: São um conjunto predefinido de claims que não são obrigatórias, mas recomendadas. Incluem iss (emissor), exp (tempo de expiração), sub (assunto do token), aud (audiência), entre outros.
- Public claims: Podem ser definidas à vontade; contudo, para evitar colisões, elas devem ser definidas em um registro IANA ou ser definidas como um URI que evita colisões.
- Private claims: São claims criadas para compartilhar informações entre partes que concordam com o uso dessas claims e não são registradas nem públicas.
1 2 3 4 5 6 7 8 |
{ "sub": "1234567890", "name": "John Doe", "admin": true, "iat": 1516239022 } |
Este payload também é codificado em Base64, formando a segunda parte do token.
3. Signature
A assinatura é usada para verificar se o token não foi alterado. Para criar a assinatura, pega-se o header codificado em Base64, o payload codificado em Base64, e utiliza-se um segredo para assinar esses dados com o algoritmo especificado no cabeçalho.
Como a assinatura é criada:
Para um cabeçalho e payload codificados, a assinatura seria gerada como:
1 2 3 4 5 6 |
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) |
Exemplo de um Token JWT Completo
Um token pode parecer com isso quando todas as partes são juntas:
1 2 3 4 5 |
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
Quebrei as linhas pra ficar mais simples o entendimento. Note que há um ponto ao final de cada linha. Você pode pegar esse JWT e acessar o site https://jwt.io e colar o conteúdo lá, e o site mostrará cada parte. Elas vão se parecer com a imagem abaixo.
- A primeira parte
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
é o cabeçalho codificado em Base64. - A segunda parte
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
é o payload codificado em Base64. - A terceira parte
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
é a assinatura HMAC SHA-256 do cabeçalho e do payload usando um segredo.
Espero que isso te ajude a entender melhor a estrutura e a função de cada parte de um JWT!
Como gerar um Token JWT no Horse?
Agora que já entendemos o que é um JWT, sua importância a anatomia de um token JWT, podemos partir para a prática e construir um exemplo prático de uso no Delphi. Nós precisaremos basicamente de 02 (duas) bibliotecas, uma delas para gerar o Token e a outra para fazer uso do JWT no Horse. Em uma aplicação real recomendo que você crie 02 serviços, um deles será uma API Horse para gerar o Token e outra API principal com suas regras de negócios, dessa forma você pode reutilizar a primeira API para qualquer dos seus sistemas se precisar. Entretanto, pra facilitar o entendimento e ficar mais fácil de montar o exemplo, faremos isso na mesma API.
Não entrarei em detalhes sobre a criação de uma API Horse, vou tomar como verdade que você já sabe como fazer isso.
Com um projeto Horse aberto no Delphi vamos iniciar nosso exemplo criando um endpoint /token capaz de gerar pra nós o token JWT. Mas antes disso vamos instalar a biblioteca necessária. Ao acessar o site https://jwt.io e clicar no menu Libraries você será levado para a página que contém diversas bibliotecas para geração do token em diversas linguagens. Procure por Delphi e encontrará a biblioteca do Paolo Rossi e então clique no ícone do GitHub dele para ir direto ao projeto da biblioteca ou acesse esse link.
Recomendo fortemente que pare alguns instantes para ler a documentação, pois há muito mais detalhes do que esse artigo.
Instalando e gerando o JWT
A biblioteca Delphi JOSE e JWT, disponível no repositório do Paolo Rossi, facilita a criação, a validação e a decodificação de JWTs em aplicações Delphi. A utilização básica envolve:
Vá até a raiz do seu projeto e instale a biblioteca com o Boss conforme exemplo abaixo:
1 2 3 |
$ boss install github.com/paolo-rossi/delphi-jose-jwt |
Para gerar o token bastaria usar um exemplo como abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
uses JOSE.Core.JWT, JOSE.Core.Builder; var Token: TJWT; CompactToken: string; begin Token := TJWT.Create; try Token.Claims.Issuer := 'your-issuer'; Token.Claims.Subject := 'your-subject'; CompactToken := TJWTBuilder.Create.JWT(Token).SetAlgorithm(HS256).SetKey('your-secret').Compact; // Utilize CompactToken como necessário finally Token.Free; end; end; |
Pra ficar mais profissional, vamos criar um controller e então registrar um endpoint pra isso.
Para isso, crie uma nova Unit, salve-a como api.controller.token.pas
, ou o nome que desejar, e vamos codificar conforme abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
unit api.controller.token; interface uses Horse, System.StrUtils, System.SysUtils, System.Classes, System.JSON, System.Generics.Collections, DateUtils, JOSE.Core.JWT, JOSE.Core.Builder, API.Infra.Claims; const C_SECRET_JWT = 'adrianosantostreina2024'; procedure Registry; implementation procedure DoGetToken(Req: THorseRequest; Res: THorseResponse); var LToken: TJWT; LExpiration: TDateTime; LCompactToken: string; LCNPJ: string; LCodLicenca: string; LSerialHD: string; LResult: TJSONObject; LClaims: TCustomClaims; begin LToken := TJWT.Create(TCustomClaims); try Req.Body<TJSONobject>.TryGetValue<string>('cnpj', LCNPJ); LClaims := TCustomClaims(LToken.Claims); LClaims.Issuer := 'Meu nome de Empresa'; LClaims.CNPJ := LCNPJ; LExpiration := IncMinute(Now, 10); LClaims.Expiration := LExpiration; LCompactToken := TJOSE.SHA256CompactToken(C_SECRET_JWT, LToken); LResult := TJSONObject.Create; LResult.AddPair('Expiration', FormatDateTime('DD/MM/YYYY hh:mm:ss.nnn', LExpiration)); LResult.AddPair('Token', LCompactToken); Res.Send<TJSONObject>(LResult); finally LToken.Free; end; end; procedure Registry; begin THorse .Post('/token', DoGetToken); end; initialization Registry; end. |
Deixe-me explirar um pouco sobre o código. Nós fizemos os uses necessários incluindo JOSE.Core.JWT
e JOSE.Core.Buider
que são units do JOSE, a biblioteca de geração de JWT do Paolo Rossi. Declaramos uma constante que vai ficar gravada a chave secreta do nosso Token JWT. Em seguida criamos um método interno DoGetToken
para responder no endpoint /token
da nossa api.
Em nosso método declamamos algumas variáveis para receber os valores, entre elas LToken
do tipo TJWT
. Também criamos uma variável do tipo TCustomClaims
, já explico o que é isso.
Em um token JWT nós temos os claims default e também podemos criar nossos próprios claims, que são campos dentro do token para armazenamento de informações personalizadas, como um CNPJ, nome de usuário, ID da aplicação ou qualquer coisa que desejar colocar nesse token. Sei que a maioria dos meus alunos e seguidores vão me perguntar como criar claims personalizados, por isso ao invés de criar um token simples, já vou ensinar você a criar algo personalizado. 😉
Criando Claims Personalizados
Na documentação do Middleware Horse-JWT
você encontrará mais detalhes sobre isso, por esse motivo não vou detalhar muito essa etapa. Quero apenas que entenda que criaremos uma classe chamada TCustomClaims
(que pode ser o nome que você desejar) e passar essa classe como parâmetro para o Create
da classe TJWT, assim ela receberá os campos personalizados que vamos criar. Portanto, crie uma nova unit no Delphi, dê o nome de api.infra.claims.pas
(ou o nome que preferir) e escreva seu código como abaixo. Não há muito o que explicar, é relativamente simples.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
unit api.infra.claims; interface uses System.StrUtils, System.SysUtils, System.Classes, JOSE.Core.JWT, JOSE.Types.JSON; type TCustomClaims = class(TJWTClaims) strict private function GetCNPJ: string; procedure SetCNPJ(const Value: string); public property CNPJ: string read GetCNPJ write SetCNPJ; end; implementation { TCustomClaims } function TCustomClaims.GetCNPJ: string; begin Result := TJSONUtils.GetJSONValue('CNPJ', FJSON).AsString; end; procedure TCustomClaims.SetCNPJ(const Value: string); begin TJSONUtils.SetJSONValueFrom<string>('CNPJ', Value, FJSON); end; end. |
Declaramos as units necessárias, criamos uma classe herdando de TJWTClaims
e então declaramos os campos personalizados. Aqui estou simulando que nosso JWT conterá o cnpj do cliente que será passado no body da requisição quando requisitarmos o endpoint /token
, vamos fazer de conta que esse valor será obrigatório no endepoint. Por fim codificamos os métodos Get
e Set
.
Retorne em então ao arquivo api.controller.token.pas
e perceba que já estamos pegando o cnpj do cliente do Body da requisição.
1 2 3 |
Req.Body<TJSONobject>.TryGetValue<string>('cnpj', LCNPJ); |
Adriano será que posso enviar essa informação no Header
pra ficar menos exposto possível? Claro, fica seu critério ou de seu cliente. Aqui vou considerar o uso do Body da requisição.
Ponto importante!
É altamente recomendando que a geração do Token seja feita em um verbo POST, por isso perceba que nossa rota está configurada para esse verbo.
Muito bem. A essa altura do artigo, se você fez tudo direitinho, basta compilar a API, executar e então fazer a chamada ao endpoint http://localhost:9000/token
enviando um JSON no Body da requisição com o cnpj. Sua requisição se parecerá muito com a imagem abaixo. Copie o token para a memória, entre no https://jwt.io
e cole na áre de teste.
Se tudo correu bem, seu JWT terá a aparência abaixo:
Isso abre um leque quase infinito de possibilidades. Aqui usamos um CNPJ, mas imagine quais informações você poderia implementar aqui?
Instalando o middleware Horse-JWT na API
Até o momento fizemos apenas a geração do token, agora precisamos implementar a leitura desse JWT e evitar que as rotas sejam acessadas se o programador não enviar o Token ou ele estiver inválido, expirado, etc. Para isso vamos utilizar o Middleware Horse-JWT que está disponível como middleware oficial no github da HashLoad. Retorne ao terminal do Windows e instale o middleware usando a linha de comando abaixo:
1 2 3 |
boss install horse-jwt |
Use a força, leia a documentação. No GitHub do middlware tem todas as instruções, inclusive a parte de geração e leitura de claims personalizados. De qualquer forma vou facilitar sua vida por aqui e depois você se aprofunda melhor. Pra simularmos as rotas, criei dois endpoints simples no DPR da nossa API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
THorse .Use(Jhonson); THorse .Get('/clientes', procedure (Req: THorseRequest; Res: THorseResponse) begin Res.Send<TJSONObject>(TJSONObject.Create.AddPair('cliente', '123456')) end ) .Get('/version', procedure (Req: THorseRequest; Res: THorseResponse) begin Res.Send<TJSONObject>(TJSONObject.Create.AddPair('Versão API', 'v1.0.0')) end ); THorse .Listen; |
O endpoint /clientes
vai simular a rota para carregar um cliente. Já o endpoint /version
simula uma rota que devolve a versão da API, só pra brincarmos depois de “não exigir JWT” para alguma rota. Portanto, será obrigatório enviar o token para /clientes, mas para /version não será obrigatório. Se executarmos agora nossa api novamente e tentarmos acionar o endpoint de clientes, veremos que ele está acessível sem enviar um token, vamos agora implementar o uso do middleware JWT.
Impedindo o acesso a uma rota sem enviar o Token
Estamos perto da etapa final, vamos configurar agora para que possamos travar o uso do endpoint caso o desenvolvedor não envie o Token. Adicione a unit Horse.JWT
ao uses do DPR, em seguida faça uso do middlware como abaixo:
1 2 3 4 5 6 7 8 |
THorse .Use( HorseJWT('adrianosantostreina2024', THorseJWTConfig.New .SkipRoutes(['/version', '/token'])) ); |
Perceba que repetimos a chave secreta e na sequência usamos a classe THorseJWTConfig pra configurar as rotas que serão “skipadas”, ou seja, vão ignorar o token e poderão ser acessadas normalmente. O códido completo do DPR ficou assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
program ApiHorseComJWT; {$APPTYPE CONSOLE} {$R *.res} uses Horse, Horse.Jhonson, Horse.JWT, System.SysUtils, System.JSON, api.controller.token in 'controller\api.controller.token.pas', api.infra.claims in 'infra\api.infra.claims.pas'; begin THorse .Use(Jhonson); THorse .Use( HorseJWT('adrianosantostreina2024', THorseJWTConfig.New .SkipRoutes(['/version', '/token'])) ); THorse .Get('/clientes', procedure (Req: THorseRequest; Res: THorseResponse) begin Res.Send<TJSONObject>(TJSONObject.Create.AddPair('cliente', '123456')) end ) .Get('/version', procedure (Req: THorseRequest; Res: THorseResponse) begin Res.Send<TJSONObject>(TJSONObject.Create.AddPair('Versão API', 'v1.0.0')) end ); THorse .Listen; end. |
Expirmente rodar a api, chamar o endpoint sem enviar um Token. Você deverá receber a mensagem "Token not found"
. Perfeito, agora rode o endpoint /token
, copie o token para a memória e vá até o endpoint /clientes
(Aqui estou usando o Postman). Clique na aba Auth, selecione Bearer Token e no campo ao lado direito cole o token da nossa api.
Certamente terá o seguinte resultado:
Em outras palavras, conseguimos acessar o endpoint. Aguarde 10 minutos (tempo que colocamos para expiração do Token) e tente novamente. O token vai expirar e a mensagem de retorno da api será Unauthorized.
Faça o teste também na rota /version
, ela não precisa de token. Perceba também que precisamos adicionar /token
em SkipRoutes
, do contrário, teríamos que enviar um token pra gerar um token, não seria uma coisa legal.
Lendo os campos personalizados
Tenho certeza que deve estar se perguntando: E como pego esse CNPJ enviando encapsulado no Token? Rá, boa pergunta. Sabia que ia perguntar. Essa parte é bem fácil, precisamos apenas executar algumas mudanças no endpoint /clientes
, vejamos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
... THorse .AddCallback(HorseJWT('adrianosantostreina2024', THorseJWTConfig.New.SessionClass(TCustomClaims))) .Get('/clientes', procedure (Req: THorseRequest; Res: THorseResponse) var LClaims: TCustomClaims; LCNPJ: string; begin LClaims := Req.Session<TCustomClaims>; LCNPJ := LClaims.CNPJ; Res.Send<TJSONObject>( TJSONObject.Create .AddPair('cliente', '123456') .AddPair('CNPJ', LCNPJ) ) end ) ... |
Bom, vamos lá. Perceba que adicionamos um Clallback
no registro da rota usado AddCallback
. Nesse callback usamos HorseJWT
e o THorseJWTConfig.New.SessionClass(TCustomClaims)
. Isso fará com que tenhamos acesso aos campos personalizados do Token enviado, dessa forma na variável LClaims conseguimos ter acesso ao CNPJ em LClaims.CNPJ
.
Em seguida, criei mais um par de chaves e adicionei o CNPJ lido no json de retorno.
Isso é legal, porque uma vez lido o claim personalizado, podemos fazer qualquer coisa com ele, tal como: validar se o CNPJ é válido, se existe no banco, capturar informações sobre o cliente no Database, etc. Imagine as possibilidades?
Quer aprender um pouco mais?
O nosso colega Vinicius Sanchez gravou um vídeo super detalhado falando sobre os assunto e tenho certeza que vai agregar muito ao aprendizado que obtivemos aqui. Assista o vídeo abaixo:
Conclusão
O uso de JSON Web Tokens (JWT) em aplicações Delphi, especialmente quando combinado com o framework Horse, oferece uma solução robusta e eficiente para gerenciamento de autenticação e autorização. A integração entre Delphi, uma linguagem poderosa e versátil, com o framework Horse, que é leve e adequado para desenvolvimento rápido de aplicações web, cria um ambiente de desenvolvimento ágil e seguro.
JWTs, com sua capacidade de encapsular as credenciais do usuário de forma compacta e segura, são ideais para a comunicação entre diferentes serviços e aplicações, garantindo que as informações do usuário sejam mantidas seguras e consistentes em todas as operações. Além disso, a autossuficiência dos JWTs reduz a necessidade de verificações de banco de dados constantes, o que pode melhorar significativamente o desempenho das aplicações.
O middleware Horse JWT facilita a implementação de JWT em aplicações Delphi, permitindo aos desenvolvedores integrar facilmente autenticação baseada em token em seus sistemas. Isso não só protege as aplicações contra acessos não autorizados, mas também simplifica o processo de manutenção e expansão dos sistemas.
Em resumo, a combinação de JWT e Horse em Delphi oferece uma estratégia de autenticação moderna, segura e eficiente. Ela permite aos desenvolvedores construir aplicações web que não apenas atendem aos requisitos modernos de segurança digital, mas também proporcionam uma experiência de usuário ágil e responsiva. Isso destaca o Delphi como uma excelente escolha para desenvolvimento de aplicações empresariais e sistemas de larga escala que requerem uma solução de autenticação avançada.
Comunidade no Telegram
🚀Comente no campo abaixo 👇👇👇 o que achou e qual sua dúvida.
Te vejo na próxima
Adriano Santos
Boa tarde.
Muito bacana essa explicação. Parabéns.
Sempre fiquei com uma dúvida: As informações contidas no ‘payload’ do token, podem ser acessadas por qualquer pessoa com conhecimento.
Porque se criou esse padrão aberto? Não deveria ter uma criptográfia por padrão também?
Olá, muito obrigado.
É verdade, pegando o PayLoad e jogando no jwt.io você consegue ver os dados, entretanto você pode criptografar as partes do payload tranquilamente quando estiver compondo o Token. Se você usar por exemplo a biblitoeca BCrypt, você pode criptografar os claims personalizados. O BCrypt possui também uma senha mestre, portanto somente você saberá como descriptografa os dados contidos no PayLoad. Ai a segurança aumenta ainda mais.
Boa tarde,
Muito bom.
Como eu valido se o token está válido.
Exemplo, meu fronEnd envia o token no cabeçalho Authorization.
Como na minha API como eu pego esse token e valido se ele está dentro do período válido para dar uma mensagem para usuário ou direcionar para a tela de login novamente.
Gleyson,
Muito obrigado pelo comentário. Você não precisa fazer nada, o próprio Horse através do middleware já valida isso e devolve um erro 401 ou 403 indicando que não houve autorização para prosseguir, portanto você valida no seu aplicativo/software, ou seja, no frontend qual resposta recebeu. Se for um 401 ou 403, sabe que precisa novamente chamar o endpoint do token pra pegar novo token.