Confiança distribuída com autenticação serviço a serviço
Nesta página
Nas arquiteturas orientadas a serviços (SOA, de Service-oriented Architectures), os “serviços” são os blocos fundamentais da tua aplicação. Os serviços são autónomos, de acoplamento fraco e, em geral, sem estado. São autónomos porque não dependem do estado nem do contexto que outros serviços fornecem. Têm acoplamento fraco porque não dependem das tecnologias que os outros serviços usam. Não têm estado porque não guardam estado entre pedidos.
Os serviços são os blocos da tua aplicação e são também os blocos da tua arquitetura de segurança.
Distribuir a confiança
Numa SOA, os serviços são independentes uns dos outros. Isso quer dizer que também são independentes dos mecanismos de segurança dos outros serviços.
É bom.
Podes usar mecanismos de segurança diferentes em serviços diferentes. Podes usar mecanismos de autenticação diferentes, mecanismos de autorização diferentes, mecanismos de cifragem diferentes, etc.
Também é mau.
Tens de gerir mecanismos de segurança diferentes para serviços diferentes. Tens de gerir mecanismos de autenticação diferentes, mecanismos de autorização diferentes, mecanismos de cifragem diferentes, etc.
Autenticação serviço a serviço
Numa SOA, os serviços são independentes uns dos outros. Já deves ter percebido o tema.
Autenticar pedidos entre serviços é um desafio. Não podes usar os mesmos mecanismos que usas para autenticar utilizadores. Não podes usar cookies, não podes usar sessões e, sobretudo, não queres depender de um fornecedor de identidade (IdP).
Com ofertas de Platform-as-a-Service (PaaS), como os Cloudflare Workers, o Heroku, o Cloud Foundry e a Vercel, nem sequer podes contar com a infraestrutura de base para te dar um canal seguro entre serviços.
Precisas de uma forma de autenticar pedidos entre serviços que seja independente da infraestrutura de base e dos mecanismos de segurança dos outros serviços.
É aqui que entram os JWTs e os JWKSs.
JSON Web Tokens (JWTs)
Os JSON Web Tokens (JWTs) são um meio compacto, seguro para URLs, de representar claims a transferir entre duas partes. As claims de um JWT são codificadas como um objeto JSON, usado como payload de uma estrutura JSON Web Signature (JWS) ou como texto simples de uma estrutura JSON Web Encryption (JWE). Assim, as claims podem ser assinadas digitalmente ou protegidas contra alterações com um Message Authentication Code (MAC) e/ou cifradas.
Estrutura de um JWT
Um JWT é uma string com três partes: o header, o payload e a assinatura. O header e o payload são objetos JSON. A assinatura é uma string codificada em Base64.
<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>
Header do JWT
O header costuma ter duas partes: o tipo do token, que é JWT, e o algoritmo de assinatura usado, como o HMAC SHA256 ou o RSA.
{
"alg": "HS256",
"typ": "JWT"
}
Payload do JWT
O payload contém as claims. As claims são afirmações sobre uma entidade (em geral, o utilizador) e metadados adicionais. Há três tipos de claims: registadas, públicas e privadas.
- Claims registadas: um conjunto de claims predefinidas, não obrigatórias mas recomendadas, para haver claims úteis e interoperáveis. Algumas são: iss (emissor), exp (data de expiração), sub (assunto), aud (audiência), entre outras.
- Claims públicas: quem usa JWTs pode defini-las à vontade. Mas, para evitar colisões, devem ser definidas no IANA JSON Web Token Registry ou como um URI com um espaço de nomes resistente a colisões.
- Claims privadas: claims personalizadas, criadas para partilhar informação entre partes que concordam em usá-las.
{
"sub": "1234567890",
"iss": "https://my-experience-api.willhackett.xyz",
"aud": "https://my-resource-api.willhackett.xyz"
}
As claims iss e aud identificam o emissor e a audiência do JWT, respetivamente. A claim sub identifica o assunto do JWT. Neste caso, o assunto é o ID do utilizador em nome de quem o pedido é feito. Num caso real, recomendo reembalar o JWT do utilizador que fez o pedido dentro do novo JWT. Assim, o serviço que o recebe pode verificar a identidade do utilizador e confirmar que ele tem as permissões certas para executar a ação pedida na tua camada de recursos.
O emissor e a audiência vão ser importantes mais à frente, quando falar de fronteiras de confiança.
Assinatura do JWT
Para criar a assinatura, tens de pegar no header codificado, no payload codificado, num segredo e no algoritmo indicado no header, e assinar tudo isso.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
A assinatura serve para verificar que a mensagem não foi alterada pelo caminho e, no caso de tokens assinados com uma chave privada, também confirma que quem enviou o JWT é quem diz ser.
JSON Web Key Sets (JWKSs)
Os JSON Web Key Sets (JWKSs) são conjuntos de chaves com as chaves públicas usadas para verificar qualquer JSON Web Token (JWT) emitido pelo servidor de autorização e assinado com o algoritmo RS256.
Estrutura de um JWKS
Um JWKS é um objeto JSON que representa um conjunto de JWKs. O objeto JSON TEM de ter um membro “keys”, que é um array de JWKs. Este é o formato JWK Set.
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use": "enc",
"kid": "1"
},
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFb.....<remainder of RSA key omitted>.....ZU3e_TURo9-F1bp-hqOY_BLPbEx-Rmc6XaXk",
"e": "AQAB",
"alg": "RS256",
"kid": "2011-04-29"
}
]
}
No nosso caso, só nos interessam as chaves públicas. As chaves públicas servem para verificar a assinatura dos JWTs.
Modelo de confiança
O modelo de confiança é simples. Confia-se no serviço que emite o JWT para emitir JWTs válidos. Confia-se no serviço que recebe o JWT para o verificar. Não se confia no serviço que recebe o JWT para emitir JWTs válidos em nome do serviço que o chamou.
Se o Serviço A emite um JWT para o Serviço B, confia-se no Serviço B para verificar o JWT contra o JWKS fornecido pelo Serviço A. O Serviço B não pode assinar um JWT e esperar que o Serviço C confie nele. É aqui que entram as fronteiras de confiança.
Fronteiras de confiança
As fronteiras de confiança são os limites entre serviços que confiam uns nos outros.
Se o Serviço A emite um JWT para o Serviço B, confia-se no Serviço B para verificar o JWT contra o JWKS fornecido pelo Serviço A.
Cada JWT contém uma claim iss e uma claim aud. A aud é a audiência a que o JWT se destina e serve para validar que o pedido vai para o serviço certo. A iss é o emissor do JWT e serve para validar que o JWT foi emitido por um serviço de confiança.
O Serviço B confia no Serviço A para emitir JWTs válidos. Assumindo que o hostname do Serviço A é service-a.willhackett.xyz e o do Serviço B é service-b.willhackett.xyz, a claim do emissor seria https://service-a.willhackett.xyz e a claim da audiência seria https://service-b.willhackett.xyz.
O Serviço B confiaria em tokens emitidos com a claim de emissor https://service-a.willhackett.xyz e a claim de audiência https://service-b.willhackett.xyz. O Serviço B chamaria o endpoint JWKS do Serviço A para obter as chaves públicas que verificam os JWTs emitidos pelo Serviço A.
Service mesh
Vale a pena perguntar: porque não usar uma service mesh? As service meshes são ótimas, mas não são uma solução mágica. Infelizmente, não servem para todos os casos. À medida que desenvolver para a web passa a ser desenvolver para a edge, ou para uma plataforma, as service meshes são cada vez menos úteis.
Dentro da tua infraestrutura, uma service mesh é uma excelente solução. Fazer deploy de contentores com um sidecar proxy é uma ótima forma de padronizar a comunicação entre serviços. Mas e quando fazes deploy para uma plataforma como os Cloudflare Workers, o Heroku, o Cloud Foundry ou a Vercel?
Com plataformas, tens de apoiar-te em standards.
Outras notas
Com este modelo de confiança distribuída, tens de garantir que as chaves RSA geradas pelo teu runtime são consistentes à medida que escalas na horizontal. Assim, a chamada ao JWKS devolve sempre as mesmas chaves públicas.
Este é só um standard para autenticação serviço a serviço em HTTP. Podias usar em alternativa TLS mútuo (mTLS), segredos partilhados, etc. O que importa é ter um standard que seja independente da infraestrutura de base e dos mecanismos de segurança dos outros serviços.