Há alguns meses, estive implementando um servidor OpenID Connect para gerenciar o acesso a centenas de nossos aplicativos internos. Passamos de soluções próprias, convenientes em menor escala, para um padrão amplamente aceito. O acesso através de um serviço centralizado simplifica bastante operações monótonas, reduz os custos de implementação de autorizações, permite encontrar muitas soluções prontas e evita complicações ao desenvolver novas. Neste artigo, vou falar sobre essa transição e sobre os desafios que enfrentamos.

Era uma vez… Como tudo começou
Há alguns anos, quando o número de aplicativos internos se tornou grande demais para gerenciar manualmente, escrevemos um aplicativo para controlar acessos dentro da empresa. Era um simples aplicativo em Rails que se conectava a um banco de dados com informações sobre os funcionários, onde o acesso a várias funcionalidades era configurado. Naquela época, implementamos o primeiro SSO, que se baseava na verificação de tokens entre o cliente e o servidor de autorização, o token era transmitido de forma criptografada com vários parâmetros e validado no servidor de autorização. Essa não era a solução mais conveniente, pois cada aplicativo interno precisaria descrever uma camada considerável de lógica, e as bases de dados dos funcionários não estavam sincronizadas com o servidor de autorização.
Após algum tempo, decidimos simplificar a tarefa de autorização centralizada. O SSO foi transferido para um balanceador. Com o OpenResty em Lua, adicionamos um template que verificava os tokens, sabia para qual aplicativo a solicitação estava indo e podia verificar se o acesso era permitido. Essa abordagem simplificou bastante a tarefa de controle de acessos aos aplicativos internos — no código de cada aplicativo, já não era necessário descrever lógica adicional. No final, bloqueamos o tráfego externamente, e o próprio aplicativo não sabia nada sobre a autorização.
No entanto, um dos problemas permaneceu sem solução. Como lidar com os aplicativos que precisam de informações sobre os funcionários? Poderíamos ter criado uma API para o serviço de autorização, mas isso exigiria adicionar lógica adicional para cada um desses aplicativos. Além disso, queríamos nos livrar da dependência de um de nossos aplicativos personalizados, que visamos no futuro para a transição para OpenSource, do nosso servidor interno de autorização. Sobre isso, falaremos em outra ocasião. A solução para ambos os problemas foi o OAuth.
Padrões amplamente aceitos
OAuth é um padrão de autorização compreensível e amplamente aceito, mas como sua funcionalidade por si só não é suficiente, começamos a considerar diretamente o OpenID Connect (OIDC). O OIDC em si é a terceira implementação de um padrão de autenticação aberto, que flui para uma camada sobre o protocolo OAuth 2.0 (protocolo de autorização aberto). Essa solução resolve o problema da falta de dados sobre o usuário final, além de permitir a troca do provedor de autorização.
No entanto, não escolhemos um provedor específico e decidimos adicionar integração com OIDC ao nosso servidor de autorização existente. A favor dessa escolha, está o fato de que OIDC é muito flexível em termos de autorização do usuário final. Assim, havia a possibilidade de implementar suporte ao OIDC em nosso atual servidor de autorização.

Nossa abordagem para implementar nosso próprio servidor OIDC
1) Transformamos os dados na forma necessária
Para a integração do OIDC, é necessário formatar os dados atuais dos usuários de uma maneira que seja compreensível para o padrão. No OIDC, isso é chamado de Claims. Os claims são essencialmente os campos finais no banco de dados de usuários (nome, email, telefone, etc.). Existe , e tudo o que não estiver nessa lista é considerado personalizado. Portanto, o primeiro ponto a se considerar se você decidir escolher um provedor de OIDC existente é a possibilidade de personalização conveniente dos novos claims.
Um grupo de claims é combinado em um subconjunto chamado Scope. Durante a autorização, o acesso não é solicitado a claims específicos, mas sim a scopes, mesmo que parte dos claims do scope não sejam necessários.
2) Implementamos os grants necessários
A próxima parte da integração OIDC é a seleção e implementação dos tipos de autorização, chamados de grants. O grant escolhido determinará o cenário de interação do aplicativo selecionado com o servidor de autorização. Um diagrama aproximado da seleção do grant apropriado é apresentado na figura abaixo.

Para nosso primeiro aplicativo, utilizamos o grant mais comum – o Authorization Code. Sua principal diferença em relação aos outros é que ele é um processo de três etapas, ou seja, passa por uma verificação adicional. Primeiro, o usuário faz uma solicitação de autorização, recebe um token – o Authorization Code, e então, com esse token, como se fosse um bilhete de passagem, solicita um token de acesso. Toda a interação principal deste cenário de autorização é baseada em redirecionamentos entre o aplicativo e o servidor de autorização. Você pode ler mais sobre este grant. .
O OAuth segue a concepção de que os tokens de acesso, obtidos após a autorização, devem ser temporários e trocados, preferencialmente, a cada 10 minutos. O grant Authorization Code é uma verificação de três etapas por meio de redirecionamentos; realizar essa troca a cada 10 minutos, para ser sincero, não é a atividade mais agradável. Para resolver esse problema, existe outro grant – o Refresh Token, que também utilizamos. Aqui tudo é mais simples. Durante a verificação de outro grant, além do token de acesso principal, um outro token – o Refresh Token – é fornecido, que pode ser usado apenas uma vez e geralmente tem uma vida útil muito maior. Com esse Refresh Token, quando o TTL (Time to Live) do token de acesso principal expirar, a solicitação de um novo token de acesso será feita em um endpoint de um grant diferente. O Refresh Token utilizado é imediatamente anulado. Essa verificação é uma operação de duas etapas e pode ser realizada em segundo plano, imperceptível para o usuário.
3) Configuramos os formatos de saída dos dados do usuário
Após a implementação dos subsídios selecionados, a autorização funcionando, é importante mencionar a obtenção de dados sobre o usuário final. No OIDC, existe um endpoint separado para isso, onde, com o seu token de acesso atual e enquanto este for válido, você pode solicitar dados do usuário. E se os dados do usuário não mudam com frequência, mas é necessário buscá-los várias vezes, pode-se chegar a uma solução como os tokens JWT. Esses tokens também são suportados pelo padrão. Em si, um token JWT consiste em três partes: header (informações sobre o token), payload (quaisquer dados necessários) e signature (assinatura, o token é assinado pelo servidor e pode ser verificado em seguida a origem de sua assinatura).
Na implementação do OIDC, o token JWT é chamado de id_token. Ele pode ser solicitado junto com o token de acesso normal e tudo o que resta é verificar a assinatura. Para isso, o servidor de autorização possui um endpoint separado com a chave pública em formato . E ao falar sobre isso, vale a pena mencionar que existe outro endpoint que, com base no padrão , reflete a configuração atual do servidor OIDC. Ele contém todos os endereços dos endpoints (incluindo o endereço da coleção de chaves públicas usadas para assinatura), os claims suportados, os escopos, os algoritmos de criptografia usados, os grants suportados, etc.
Por exemplo, no Google:
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"device_authorization_endpoint": "https://oauth2.googleapis.com/device/code",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"revocation_endpoint": "https://oauth2.googleapis.com/revoke",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token",
"none"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid",
"email",
"profile"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"claims_supported": [
"aud",
"email",
"email_verified",
"exp",
"family_name",
"given_name",
"iat",
"iss",
"locale",
"name",
"picture",
"sub"
],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:jwt-bearer"
]
}Assim, usando o id_token, é possível transmitir todos os claims necessários no payload do token sem precisar solicitar os dados do usuário ao servidor de autorização a cada vez. A desvantagem dessa abordagem é que a alteração dos dados do usuário pelo servidor não é imediata, chegando apenas com um novo token de acesso.
Resumo da implementação
Assim, após a implementação de nosso próprio servidor OIDC e a configuração das conexões a ele do lado dos aplicativos, resolvemos o problema da transmissão de informações sobre os usuários.
Como o OIDC é um padrão aberto, tivemos a possibilidade de escolher um provedor existente ou implementar nosso próprio servidor. Testamos o Keycloak, que se mostrou muito conveniente para configuração; após ajustar e mudar as configurações de conexão do lado dos aplicativos, ele estava pronto para uso. No lado dos aplicativos, resta apenas alterar as configurações de conexão.
Falando sobre soluções existentes
No âmbito da nossa organização, como primeiro servidor OIDC, montamos nossa própria implementação, a qual foi complementada conforme necessário. Após uma análise detalhada de outras soluções prontas, pode-se dizer que essa é uma questão controversa. A favor da decisão de implementar nosso próprio servidor estiveram as preocupações por parte dos provedores quanto à falta de funcionalidades necessárias, além da presença de um sistema antigo onde existiam diferentes autorizações personalizadas para alguns serviços e já havia acumulado uma quantidade considerável de dados sobre os funcionários. No entanto, as implementações prontas oferecem facilidades para integração. Por exemplo, o Keycloak possui seu próprio sistema de gerenciamento de usuários e os dados são armazenados diretamente nele, e migrar os nossos usuários para lá não é uma tarefa difícil. Para isso, o Keycloak tem uma API que permitirá realizar todas as ações necessárias para a migração.
Outro exemplo de uma implementação certificada e interessante, na minha opinião, é o Ory Hydra. Ele é interessante por ser composto por diferentes componentes. Para a integração, você precisará vincular seu serviço de gerenciamento de usuários ao serviço de autorização deles e expandir conforme necessário.
Keycloak e Ory Hydra não são as únicas soluções prontas. É melhor optar por uma implementação certificada pela OpenID Foundation. Normalmente, essas soluções têm o selo de Certificação OpenID.

Além disso, não se esqueça dos provedores pagos existentes, se você não quiser manter seu servidor OIDC. Atualmente, há muitas boas opções.
O que vem a seguir
Em breve, planejamos fechar o tráfego para serviços internos de outra maneira. Estamos planejando mover nosso SSO atual para um balanceador usando OpenResty em um proxy baseado em OAuth. Aqui também já existem muitas soluções prontas, como:
Materiais adicionais
– um bom serviço para verificar tokens JWT
— uma lista de implementações certificadas de OIDC
Fonte: habr.com
