← Todos os textos

Confiança distribuída com autenticação entre serviços

Nesta página

Em arquiteturas orientadas a serviços (SOA), os “serviços” são os blocos fundamentais da sua aplicação. Serviços são autônomos, fracamente acoplados e, em geral, sem estado. São autônomos porque não dependem do estado nem do contexto de outros serviços. Fracamente acoplados porque não dependem das tecnologias que outros serviços usam. Sem estado porque não guardam estado entre requisições.

Serviços são os blocos da sua aplicação e também os blocos da sua arquitetura de segurança.

Distribuindo a confiança

Em uma SOA, os serviços são independentes entre si. Isso significa que eles também são independentes dos mecanismos de segurança dos outros serviços.

Isso é bom.

Você pode usar mecanismos de segurança diferentes para serviços diferentes. Mecanismos de autenticação diferentes, de autorização diferentes, de criptografia diferentes, e assim por diante.

Isso também é ruim.

Você precisa gerenciar mecanismos de segurança diferentes para serviços diferentes. Autenticação diferente, autorização diferente, criptografia diferente, e assim por diante.

Autenticação entre serviços

Em uma SOA, os serviços são independentes entre si. Dá para ver o tema.

Autenticar requisições entre serviços é um desafio. Você não pode usar os mesmos mecanismos que usa para autenticar usuários. Não dá para usar cookies, não dá para usar sessões e, principalmente, você não quer depender de um provedor de identidade (IdP).

Com ofertas de Platform-as-a-Service (PaaS) como Cloudflare Workers, Heroku, Cloud Foundry e Vercel, você nem pode contar com a infraestrutura por baixo para dar um canal seguro entre serviços.

Você precisa de um jeito de autenticar requisições entre serviços que não dependa da infraestrutura e não dependa dos mecanismos de segurança dos outros serviços.

É aqui que entram os JWTs e os JWKSs.

JSON Web Tokens (JWTs)

JSON Web Tokens (JWTs) são um meio compacto e seguro para URLs de representar claims que trafegam 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 puro de uma estrutura JSON Web Encryption (JWE). Assim, as claims podem ser assinadas digitalmente ou ter a integridade protegida por um Message Authentication Code (MAC) e/ou ser criptografadas.

Estrutura do JWT

Um JWT é uma string de 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 em uso, como HMAC SHA256 ou RSA.

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

Payload do JWT

O payload contém as claims. Claims são afirmações sobre uma entidade (em geral, o usuário) e metadados adicionais. Existem três tipos de claims: registradas, públicas e privadas.

  • Claims registradas: um conjunto de claims predefinidas, não obrigatórias mas recomendadas, para dar um conjunto de claims úteis e interoperáveis. Algumas delas: iss (emissor), exp (data de expiração), sub (assunto), aud (audiência), entre outras.
  • Claims públicas: quem usa JWTs pode definir como quiser. Para evitar colisões, elas devem ser definidas no IANA JSON Web Token Registry ou como uma URI com um namespace resistente a colisões.
  • Claims privadas: claims personalizadas, criadas para compartilhar informação entre partes que combinaram de 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, respectivamente. A claim sub identifica o assunto do JWT. Aqui, o assunto é o ID do usuário em nome de quem a requisição é feita. Num caso real, eu recomendo reempacotar o JWT do usuário que fez a requisição dentro do novo JWT. Assim o serviço que recebe consegue verificar a identidade do usuário e conferir se ele tem as permissões certas para executar a ação pedida na sua camada de recursos.

O emissor e a audiência voltam a aparecer quando eu falar de limites de confiança.

Assinatura do JWT

Para criar a assinatura, você pega o header codificado, o payload codificado e um segredo, e assina tudo com o algoritmo indicado no header.

HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

A assinatura serve para verificar que a mensagem não mudou no caminho. No caso de tokens assinados com chave privada, ela também confirma que quem enviou o JWT é quem diz ser.

JSON Web Key Sets (JWKSs)

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 do JWKS

Um JWKS é um objeto JSON que representa um conjunto de JWKs. O objeto JSON DEVE ter um membro “keys”, que é um array de JWKs. Esse é 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. Elas servem para verificar a assinatura dos JWTs.

Modelo de confiança

O modelo de confiança é simples. Confiamos que o serviço que emite o JWT emite JWTs válidos. Confiamos que o serviço que recebe o JWT sabe verificá-lo. Não confiamos que o serviço que recebe o JWT emita JWTs válidos em nome do serviço que o chamou.

Se o Serviço A emite um JWT para o Serviço B, confiamos que o Serviço B verifica 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. É aí que entram os limites de confiança.

Limites de confiança

Limites de confiança são as fronteiras entre serviços que confiam uns nos outros.

Se o Serviço A emite um JWT para o Serviço B, confiamos que o Serviço B verifica o JWT contra o JWKS fornecido pelo Serviço A.

Todo JWT carrega uma claim iss e uma claim aud. A aud é a audiência pretendida do JWT e serve para validar que a requisição é destinada ao serviço certo. A iss é o emissor do JWT e serve para validar que ele foi emitido por um serviço confiável.

O Serviço B confia no Serviço A para emitir JWTs válidos. Supondo que o hostname do Serviço A seja service-a.willhackett.xyz e o do Serviço B seja service-b.willhackett.xyz, a claim de emissor seria https://service-a.willhackett.xyz e a claim de 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. Ele chamaria o endpoint JWKS do Serviço A para buscar as chaves públicas que verificam os JWTs emitidos pelo Serviço A.

Service mesh

Vale perguntar: por que não usar um service mesh? Service meshes são ótimos, mas não resolvem tudo. Infelizmente eles não servem para todos os casos. Conforme desenvolver para a web passa a ser desenvolver para a edge, ou para uma plataforma, o service mesh serve cada vez menos.

Dentro da sua infraestrutura, o service mesh é uma ótima solução. Subir contêineres com um sidecar proxy é um jeito excelente de padronizar a comunicação entre serviços. Mas e quando você faz deploy numa plataforma como Cloudflare Workers, Heroku, Cloud Foundry ou Vercel?

Com plataformas, você precisa se apoiar em padrões.

Outras considerações

Com esse modelo de confiança distribuída, você precisa garantir que as chaves RSA geradas pelo seu runtime sejam consistentes conforme você escala na horizontal. Assim a chamada ao JWKS sempre devolve as mesmas chaves públicas.

Esse é só um padrão para autenticação entre serviços via HTTP. Você também poderia usar TLS mútuo (mTLS), segredos compartilhados e por aí vai. O que importa é ter um padrão que não dependa da infraestrutura e não dependa dos mecanismos de segurança dos outros serviços.

Referências