servicosgovbr / manual-roteiro-integracao-login-unico

Roteiro de Integração do Login Único para os órgãos
https://manual-roteiro-integracao-login-unico.servicos.gov.br
42 stars 16 forks source link

Adicionar exemplo de como integrar o Keycloak como Identity Broker e o Gov.br como Identity Provider #25

Open piantino opened 1 year ago

piantino commented 1 year ago

Olá,

Gostaria de compartilhar a configuração que fiz para o Keycloak usar o Gov.br como Identity provider, ou seja, como adicionar um botão no Keycloak para o usuário se logar com o Gov.br.

image

A configuração utilizada: image

Acredito que seria interessante colocar no exemplo de implementação, mas apenas ficando registrado aqui já pode ajudar alguém.

rdurelli commented 1 year ago

Alguem testou isso? Deu certo?

weltonrodrigo commented 1 year ago

@rdurelli é basicamente isso, eu sugeriria apenas algumas alterações:

É importante notar que:

  1. Obtenção de informações do usuário: Para obter foto do usuário, níveis da conta (bronze, prata, ouro) e selos de confiabilidade (diversos), é preciso fazer chamadas na API e utilizar o token gov.br original, pra isso, é preciso ativar as opções Store token e Stored tokens readable para que o app possa obter o token gov.br original e chamar o endpoint apropriado. A obtenção do token é feita como descrito no doc https://www.keycloak.org/docs/latest/server_admin/#retrieving-external-idp-tokens:

    # para o nome de provider (gov.br) e realm (dev) usados na imagem :
    curl https://meu.servidorkeycloak.com.br/realms/dev/broker/gov.br/token \
        -H "Authorization: Bearer <KEYCLOAK ACCESS TOKEN DO USUÁRIO>"

    A resposta é o token original gov.br que a aplicação pode usar pra chamar os endpoints.

  2. Confiabilidade e nível de conta: Talvez antigamente as confiabilidades e nível da conta viessem dentro do token (daí a existência do escopo), mas atualmente não vem mais e é preciso essa chamada subsequente. É importante que o dev compreenda que é necessário fazer essa checagem do nível da conta e confiabilidade no backend e não confie numa checagem feita no frontend. Isso é importante porque embora o frontend possa ser mexido pelo usuário, quando ele submete o token no backend para chamar uma api do app, como o token tem uma garantia criptográfica de que está correto (e pode ser checado), o backend pode confiar que se o token é válido, as informações ali dentro também são e que o usuário muito provavelmente se logou corretamente (cuidado com token hijacking). Mas como a informação de nível de conta e confiabilidade não está DENTRO do token, precisaria ser passada por fora do token e por isso está sujeita a ser forjada. Dessa forma, a aplicação precisa ou checar o nível de confiabilidade toda vez (com um cache adequado) que os seus endpoints próprios sejam chamados, ou precisa considerar o usuário logado e autorizado apenas depois da checagem ocorrer no backend e aí emitir seu mecanismo próprio de controle de sessão (cookies ou um token JWT próprio).

  3. Set-up de Autenticação: É uma pena também que a solução atual não consiga fazer o set-up authentication (ver https://github.com/servicosgovbr/manual-roteiro-integracao-login-unico/issues/23) porque o Keycloak atualmente suporta e seria transparente passar a exigir segundo fator (ou múltiplos fatores) de autenticação para algumas operações.

  4. Integração transparente: se o gov.br é a única base de usuários e se a sua aplicação já tem o botão (obrigatório) "Entrar com gov.br", é possível esconder a tela de login do keycloak tornando o gov.br o authentication provider padrão: No exemplo da imagem, o id a ser inserido na configuração é gov.br (pode ser qualquer string que estiver como "Alias" do OIDC provider).

Eu venho usando o keycloak como intermediário entre o órgão e o gov.br há anos com essas configurações e funciona muito bem, mas com essas ressalvas.

UPDATE:

rdurelli commented 1 year ago

Basicamente vc deu uma aula :) irei teste. Qualquer coisa lhe peço ajuda. Muito obrigado

piantino commented 1 year ago

Perfeito as explicações @weltonrodrigo,

Sobre o scope "govbr_confiabilidades" é necessário para permitir que o token retornado pelo Gov.br tenha permissão de chamar os serviços de confiabilidade, do contrário retornará um erro assim:

{
  "errors": [
    {
      "status":401,
      "code":"ACCESSTOKEN_SCOPE_MUSTCONTAINSEXPECTEDSCOPE",
      "title":"Escopo requerido não encontrado. Valor esperado: '{0}', valor recebido: '{1}'." 
    }
  ]
}

Pelo menos foi o que aconteceu nos meus testes.

caduvieira commented 1 year ago

@rdurelli é basicamente isso, eu sugeriria apenas algumas alterações:

  • validação de assinaturas: a solução do gov.br atual interpreta errado o RFC e o formato do jwks não é reconhecido pelo keycloak. Dessa forma, precisa fazer validação manual, extraindo a chave (campo n) em https://sso.acesso.gov.br/jwk e colocando no campo validar assinatura (precisa estar atento porque quando a chave for rotacionada o login vai parar de funcionar)

Em que ponto o Login gov.br interpreta a RFC 7517 errada ? O exemplo no https://www.rfc-editor.org/rfc/rfc7517#appendix-A tem os mesmos campos retornados pelo https://sso.acesso.gov.br/jwk. Há algum problema com os valores?

weltonrodrigo commented 1 year ago

Em que ponto o Login gov.br interpreta a RFC 7517 errada ? O exemplo no https://www.rfc-editor.org/rfc/rfc7517#appendix-A tem os mesmos campos retornados pelo https://sso.acesso.gov.br/jwk. Há algum problema com os valores?

Você tem razão que o RFC coloca o campo 'use' como OPTIONAL. É mais correto eu dizer que o gov.br e o keycloak discordam sobre o formato do jwk.

No teste que realizei pra fazer esse comentário o pyjwt conseguiu validar corretamente a partir da url do keyset e quando eu criei esse repo, isso não acontecia, tinha que fornecer a chave manualmente. Suponho então que o pyjwt tenha se alinhado.

acostaaluiz commented 1 year ago

Quando adiciono um novo identity provider no keycloak, ele coloca um valor default no campo redirect uri. Eu não consigo edita-lo e para colocar o valor https://sso.tjsc..... conforme mostra no print. Alguma dica? Valeu

piantino commented 1 year ago

Quando adiciono um novo identity provider no keycloak, ele coloca um valor default no campo redirect uri. Eu não consigo edita-lo e para colocar o valor https://sso.tjsc..... conforme mostra no print. Alguma dica? Valeu

O Redirect URI é a URL do seu Keycloak (no print é o Keycloak do TJSC) e não pode ser alterado, já o Authorization URL é a do Gov.br. Pensa que o seu keycloak é o cliente do SSO do Gov.br, e outras aplicações serão os clientes do seu keycloak. Ficou mais claro agora?

carlosrodovalho commented 1 month ago

estou tantando fazer a implementação do gov.br em um servidor de homologação da minha instituição, no entanto após informar o CPF e senha na tela de login do Gov.br, o usuário é retornado pra aplicação e o keycloak exibe a mensagem:

Unexpected error when authenticating with identity provider
« Back to Application

no log do keycloak aparece esse erro:

2024-08-05 17:10:16,864 WARN  [org.keycloak.events] (executor-thread-173) type="IDENTITY_PROVIDER_LOGIN_ERROR", realmId="35-secret-e072bb", realmName="virtualif-homologacao", clientId="virtualif-modulo-copese", userId="null", ipAddress="192.168.1.254", error="identity_provider_login_failure", code_id="8158e5-secret-29abf3bc"
2024-08-05 18:19:51,484 WARN  [org.keycloak.keys.infinispan.InfinispanPublicKeyStorageProvider] (executor-thread-187) PublicKey wasn't found in the storage. Requested kid: 'rsa1' . Available kids: '[]'
2024-08-05 18:19:51,486 ERROR [org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider] (executor-thread-187) Failed to make identity provider oauth callback: org.keycloak.broker.provider.IdentityBrokerException: token signature validation failed
        at org.keycloak.broker.oidc.OIDCIdentityProvider.parseTokenInput(OIDCIdentityProvider.java:679)
        at org.keycloak.broker.oidc.OIDCIdentityProvider.validateToken(OIDCIdentityProvider.java:696)
        at org.keycloak.broker.oidc.OIDCIdentityProvider.validateToken(OIDCIdentityProvider.java:690)
        at org.keycloak.broker.oidc.OIDCIdentityProvider.getFederatedIdentity(OIDCIdentityProvider.java:379)
        at org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider$Endpoint.authResponse(AbstractOAuth2IdentityProvider.java:557)
        at org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider$Endpoint$quarkusrestinvoker$authResponse_ab908fbdd086ee82e140d8a818c077362a2d04b4.invoke(Unknown Source)
        at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
        at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
        at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
        at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
        at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
        at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
        at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:1583)

alguém tem alguma ideia do que pode ser? desde já agradeço

weltonrodrigo commented 1 month ago

estou tantando fazer a implementação do gov.br em um servidor de homologação da minha instituição, no entanto após informar o CPF e senha na tela de login do Gov.br, o usuário é retornado pra aplicação e o keycloak exibe a mensagem:

Unexpected error when authenticating with identity provider
« Back to Application

no log do keycloak aparece esse erro:

2024-08-05 17:10:16,864 WARN  [org.keycloak.events] (executor-thread-173) type="IDENTITY_PROVIDER_LOGIN_ERROR", realmId="35-secret-e072bb", realmName="virtualif-homologacao", clientId="virtualif-modulo-copese", userId="null", ipAddress="192.168.1.254", error="identity_provider_login_failure", code_id="8158e5-secret-29abf3bc"
2024-08-05 18:19:51,484 WARN  [org.keycloak.keys.infinispan.InfinispanPublicKeyStorageProvider] (executor-thread-187) PublicKey wasn't found in the storage. Requested kid: 'rsa1' . Available kids: '[]'
2024-08-05 18:19:51,486 ERROR [org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider] (executor-thread-187) Failed to make identity provider oauth callback: org.keycloak.broker.provider.IdentityBrokerException: token signature validation failed
        at org.keycloak.broker.oidc.OIDCIdentityProvider.parseTokenInput(OIDCIdentityProvider.java:679)
        at org.keycloak.broker.oidc.OIDCIdentityProvider.validateToken(OIDCIdentityProvider.java:696)
        at org.keycloak.broker.oidc.OIDCIdentityProvider.validateToken(OIDCIdentityProvider.java:690)
        at org.keycloak.broker.oidc.OIDCIdentityProvider.getFederatedIdentity(OIDCIdentityProvider.java:379)
        at org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider$Endpoint.authResponse(AbstractOAuth2IdentityProvider.java:557)
        at org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider$Endpoint$quarkusrestinvoker$authResponse_ab908fbdd086ee82e140d8a818c077362a2d04b4.invoke(Unknown Source)
        at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
        at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
        at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
        at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
        at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
        at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
        at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
        at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:1583)

alguém tem alguma ideia do que pode ser? desde já agradeço

Experimenta desativar a opção “validate signature”.

carlosrodovalho commented 1 month ago

Experimenta desativar a opção “validate signature”.

Muito obrigado @weltonrodrigo . Desativando a checagem pelo JWKS o login funciona perfeitamente. No entanto ao fazer isso eu não estaria diminuindo a segurança do processo de login? Existe alguma maneira de realizar a integração mantendo a checagem da assinatura e tmb usando PKCE? Mais uma vez obrigado pela ajuda

caduvieira commented 1 month ago

Isso está relacionado a issue #29.

Experimenta desativar a opção “validate signature”.

Muito obrigado @weltonrodrigo . Desativando a checagem pelo JWKS o login funciona perfeitamente. No entanto ao fazer isso eu não estaria diminuindo a segurança do processo de login?

Sim. Estaria.

carlosrodovalho commented 1 month ago

Sim. Estaria.

Entendi. Obrigado.

Caso eu crie um endopoint próprio, que leia o objeto retornado por https://sso.acesso.gov.br/jwk, adicione a ele o campo use: sig, e responda a requisição com objeto "completo", conforme o keycloak espera. E então configurar esse endpoint como 'JWKS URL' no keycloak.

Em teoria, com essa estratégia eu poderia realizar a assinatura, correto?

E imagino que nesse cenário não seria possível ativar a opção 'Use PKCE', certo?

weltonrodrigo commented 1 month ago

Experimenta desativar a opção “validate signature”.

Muito obrigado @weltonrodrigo . Desativando a checagem pelo JWKS o login funciona perfeitamente. No entanto ao fazer isso eu não estaria diminuindo a segurança do processo de login? Existe alguma maneira de realizar a integração mantendo a checagem da assinatura e tmb usando PKCE? Mais uma vez obrigado pela ajuda

Sim, @carlosrodovalho, diminui. Mas não é teu grave pq tá num ambiente controlado e você recebe o token gov.br a partir de um request que você mesmo faz ao endpoint de token do Gov.br. Nao é algo que você recebe dos clientes e precisa validar.

Você pode manter a validação copiando e colando a chave no campo apropriado (e substituindo manualmente quando o gov.br trocar a chave).

O PKCE é independente e não relacionado. Você pode ativar ele mesmo sem validar assinaturas do token.

Sobre o seu endpoint ajustando o keyset, vai funcionar de boas.