Tutoriais

Keycloak

Tutorial completo de instalação, configuração e operação do Keycloak 26.x — Realms, clients, OIDC/OAuth2, integração com Spring Boot e Vue, themes customizados, backup e troubleshooting.

Tutorial de referência para instalar, configurar e operar Keycloak 26.x em produção, integrando com uma infraestrutura típica baseada em Docker Compose, PostgreSQL 17, nginx como reverse proxy com TLS Let’s Encrypt e restrição de acesso por allowlist de rede confiável (VPN).

A versão de referência é a 26.x (última major estável em 2026). Pontos em que algo mudou em relação à 25.x estão marcados explicitamente — Keycloak quebra coisas com frequência em majors, então preste atenção a esses avisos quando for fazer upgrade.


1. Visão geral

Keycloak é um servidor de Identity and Access Management (IAM) open-source mantido pela Red Hat / CNCF Sandbox. Funciona como Identity Provider (IdP) externo: suas aplicações deixam de lidar com senha, MFA, federação social, etc — basta delegar a autenticação ao Keycloak e validar os tokens que ele emite.

Três protocolos importam:

  • OAuth 2.0 — protocolo de autorização. Define como um cliente obtém um access token para chamar APIs em nome de um usuário.
  • OpenID Connect (OIDC) — camada de autenticação sobre OAuth 2.0. Acrescenta o ID token (JWT com claims do usuário) e endpoints de discovery (/.well-known/openid-configuration).
  • SAML 2.0 — alternativa antiga baseada em XML, ainda comum em ambientes corporativos legados (ADFS, ERP, etc).

Para aplicações novas use OIDC. SAML só quando algum sistema externo exige.

Quando faz sentido adotar Keycloak:

  • Tem mais de um app e quer SSO entre eles.
  • Quer centralizar usuários, roles, MFA, recuperação de senha.
  • Quer login social (Google, GitHub) sem implementar nada na app.
  • Quer um resource server (backend) validando JWT em vez de sessão stateful.

Para um único app pequeno, Keycloak é overkill — uma lib de auth no próprio backend resolve. Mas a partir do segundo serviço, vale.


2. Arquitetura

Visão geral de componentes

flowchart TD
    User[Usuário no navegador]
    VPN[Rede confiável / VPN<br/>172.28.0.0/16]
    Nginx[nginx-proxy<br/>TLS + allowlist]
    KC[Keycloak 26.x<br/>container]
    PG[(PostgreSQL 17<br/>postgres17)]
    Front[Frontend Vue 3<br/>keycloak-js]
    Back[Backend Spring Boot 3.3<br/>resource server]

    User -->|HTTPS| VPN
    VPN -->|allow 172.28.0.1| Nginx
    Nginx -->|proxy_pass<br/>X-Forwarded-*| KC
    Nginx -->|proxy_pass| Front
    Nginx -->|proxy_pass| Back
    Front -->|fetch + Bearer| Back
    Front -.->|redirect login| KC
    Back -.->|JWKS<br/>issuer-uri| KC
    KC -->|JDBC| PG

Fluxo Authorization Code + PKCE

É o fluxo recomendado para SPAs (frontend Vue) e mobile. O code_verifier substitui o client_secret que um public client não consegue guardar em segurança.

sequenceDiagram
    autonumber
    participant U as Usuário
    participant F as Frontend Vue<br/>(keycloak-js)
    participant KC as Keycloak
    participant B as Backend Spring

    U->>F: Abre o app
    F->>F: Gera code_verifier + code_challenge (S256)
    F->>KC: Redirect /authorize?response_type=code&code_challenge=...
    KC->>U: Tela de login
    U->>KC: credenciais + MFA
    KC->>F: 302 redirect_uri?code=XYZ
    F->>KC: POST /token (code + code_verifier)
    KC->>F: access_token (JWT) + refresh_token + id_token
    F->>B: GET /api/recurso  Authorization: Bearer JWT
    B->>KC: GET /realms/myapp/protocol/openid-connect/certs (JWKS, cacheado)
    KC->>B: chaves públicas
    B->>B: Valida assinatura + iss + exp + aud
    B->>F: 200 dados

Pontos importantes do fluxo:

  • O access token é um JWT assinado (RS256 por padrão). O backend valida localmente com a chave pública do Keycloak (JWKS) — sem round-trip a cada request.
  • O refresh token é opaco; só serve para pedir um novo access token ao Keycloak.
  • O id token carrega a identidade do usuário (sub, email, preferred_username); não deve ser enviado a APIs.
  • PKCE evita que um app malicioso no mesmo dispositivo intercepte o code e troque por um token.

3. Pré-requisitos

ItemVersão / detalhe
Docker24+ com plugin compose v2
PostgreSQL17.x acessível na network proxy
Domíniokeycloak.example.com com A record para 203.0.113.10
TLSgerenciado pelo nginx-proxy via Let’s Encrypt (acme.sh ou certbot)
RAMmínimo 1 GB para o container (recomendado 2 GB)
Javaembutido na imagem (Eclipse Temurin 21) — não precisa instalar no host

Verifique a network e o Postgres antes de prosseguir:

docker network inspect proxy | jq '.[0].IPAM.Config'
docker exec postgres17 psql -U postgres -c '\l'

4. Instalação com Docker Compose

4.1 Criar o database

O Keycloak precisa do seu próprio database e usuário no Postgres. Não reutilize um schema de aplicação.

docker exec -it postgres17 psql -U postgres <<'SQL'
CREATE USER keycloak WITH PASSWORD 'TROCAR_AQUI_SENHA_FORTE';
CREATE DATABASE keycloak OWNER keycloak ENCODING 'UTF8';
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;
\c keycloak
GRANT ALL ON SCHEMA public TO keycloak;
SQL

Em Postgres 15+ o GRANT ALL ON SCHEMA public é necessário porque o schema public deixou de ser gravável para PUBLIC por padrão.

4.2 Bloco docker-compose

Adicione ao /opt/docker/docker-compose.yaml:

services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    container_name: keycloak
    restart: unless-stopped
    command:
      - start
      - --optimized
    environment:
      # --- Banco de dados ---
      KC_DB: postgres
      KC_DB_URL_HOST: postgres17
      KC_DB_URL_PORT: "5432"
      KC_DB_URL_DATABASE: keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
      KC_DB_POOL_MAX_SIZE: "20"

      # --- Hostname (hostname:v2, default em 26) ---
      KC_HOSTNAME: https://keycloak.example.com
      KC_HOSTNAME_STRICT: "true"
      KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "false"

      # --- Reverse proxy ---
      # Em 26.x: NUNCA use KC_PROXY=edge (removido). Use proxy-headers.
      KC_PROXY_HEADERS: xforwarded
      KC_HTTP_ENABLED: "true"

      # --- Observabilidade ---
      KC_HEALTH_ENABLED: "true"
      KC_METRICS_ENABLED: "true"
      KC_LOG_LEVEL: info

      # --- Bootstrap admin (remover após primeiro boot) ---
      KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_BOOTSTRAP_ADMIN}
      KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_BOOTSTRAP_PASSWORD}

      # --- JVM (opcional, tune se necessário) ---
      JAVA_OPTS_APPEND: "-XX:MaxRAMPercentage=70"
    volumes:
      - ./keycloak/themes:/opt/keycloak/themes:ro
      - ./keycloak/providers:/opt/keycloak/providers:ro
    networks:
      - proxy
    depends_on:
      postgres17:
        condition: service_healthy
    healthcheck:
      # Em 26.x o endpoint /health vive na porta de management (9000)
      # quando KC_HEALTH_ENABLED=true. Usar wget porque a imagem não tem curl.
      test:
        - CMD-SHELL
        - >
          exec 3<>/dev/tcp/127.0.0.1/9000 &&
          printf 'GET /health/ready HTTP/1.0\r\nHost: localhost\r\n\r\n' >&3 &&
          grep -q '"status": "UP"' <&3
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 90s

networks:
  proxy:
    external: true

Arquivo .env ao lado do compose:

KEYCLOAK_DB_PASSWORD=...senha_db_do_keycloak...
KEYCLOAK_BOOTSTRAP_ADMIN=bootstrap
KEYCLOAK_BOOTSTRAP_PASSWORD=...senha_temporaria_longa...

chmod 600 .env e nunca commitar.

4.3 Sobre start --optimized

Em produção, o comando ideal é start --optimized. Para usar --optimized, você precisa rodar antes o kc.sh build (geralmente embutido em uma imagem custom via Dockerfile). Para começar sem imagem custom, troque o command por apenas:

    command: ["start"]

Keycloak fará o build sob demanda no primeiro boot (mais lento, ~30s adicionais). Quando estabilizar a configuração, gere uma imagem custom:

# Dockerfile
FROM quay.io/keycloak/keycloak:26.0 AS builder
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:26.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

E aponte o compose para essa imagem (build: . ou tag local).

4.4 Subir o serviço

cd /opt/docker
docker compose up -d keycloak
docker compose logs -f keycloak

Aguarde a linha:

Listening on: http://0.0.0.0:8080
Management interface listening on http://0.0.0.0:9000

E o healthcheck virar healthy:

docker compose ps keycloak

5. Bootstrap do admin

Em Keycloak 26.x as variáveis legadas KEYCLOAK_ADMIN e KEYCLOAK_ADMIN_PASSWORD foram substituídas por KC_BOOTSTRAP_ADMIN_USERNAME e KC_BOOTSTRAP_ADMIN_PASSWORD.

Comportamento:

  • São lidas apenas no primeiro boot, quando o realm master é criado.
  • Criam um usuário admin temporário. A intenção do projeto é que você logue com ele, crie um admin permanente (com nome real / email) e remova essas variáveis do compose.

Passo a passo:

  1. Após o primeiro docker compose up, abra https://keycloak.example.com (a partir de um host dentro da rede confiável, se você aplicou a allowlist do passo 6).
  2. Logue com ${KEYCLOAK_BOOTSTRAP_ADMIN} / ${KEYCLOAK_BOOTSTRAP_PASSWORD}.
  3. Vá em master realmUsersAdd user.
  4. Crie um usuário admin permanente (ex. admin-prod), com email, marque Email verified, defina senha em Credentials, atribua role admin em Role mapping.
  5. Deslogue, logue como o novo usuário, confirme que tem todos os poderes.
  6. Em Users delete o usuário bootstrap temporário.
  7. Edite o .env removendo as variáveis KEYCLOAK_BOOTSTRAP_* e reinicie: docker compose up -d keycloak.

Se você precisar resetar (esqueceu a senha do admin), basta colocar de volta as variáveis e reiniciar — Keycloak detecta que não há admin no master e recria o temporário.


6. Reverse proxy nginx

Crie /opt/docker/nginx/conf.d/keycloak.example.com.conf:

# HTTP -> HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name keycloak.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name keycloak.example.com;

    ssl_certificate     /etc/letsencrypt/live/keycloak.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/keycloak.example.com/privkey.pem;
    include /etc/nginx/ssl-params.conf;

    # Allowlist: aceita apenas tráfego vindo da rede confiável.
    # Ajuste para o gateway/range da sua VPN ou rede privada.
    allow 172.28.0.1;
    deny all;

    # Buffers grandes — tokens JWT podem passar de 8KB
    client_max_body_size 16m;
    proxy_buffer_size    32k;
    proxy_buffers        8 32k;
    proxy_busy_buffers_size 64k;

    location / {
        proxy_pass http://keycloak:8080;

        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;

        # Login pode demorar (MFA, federação)
        proxy_connect_timeout 60s;
        proxy_send_timeout    120s;
        proxy_read_timeout    120s;
    }

    # NÃO exponha publicamente — health e metrics ficam na porta 9000 (mgmt),
    # acessíveis apenas dentro da network docker. Se quiser scrape Prometheus,
    # faça outro container na proxy falando direto com keycloak:9000.
}

Pontos relevantes:

  • KC_PROXY_HEADERS=xforwarded no Keycloak diz para confiar nos cabeçalhos X-Forwarded-*. Sem isso, Keycloak vê http://keycloak:8080 e gera URLs erradas no discovery.
  • O allow 172.28.0.1; deny all; restringe o acesso ao range da rede confiável: nesse exemplo, o IP do gateway da bridge docker — atrás dele ficam, por exemplo, clientes de uma VPN como WireGuard. Substitua pelo CIDR/IP apropriado ao seu ambiente.
  • O endpoint /admin fica acessível porque está atrás dessa restrição. Em um cenário publicamente exposto, restrinja /admin em um location separado.

Recarregue o nginx:

docker exec nginx-proxy nginx -t && docker exec nginx-proxy nginx -s reload

Teste o discovery:

curl -s https://keycloak.example.com/realms/master/.well-known/openid-configuration | jq .issuer
# Deve retornar: "https://keycloak.example.com/realms/master"

Se vier http://keycloak:8080/..., o KC_PROXY_HEADERS ou o KC_HOSTNAME está errado.


7. Configurando um Realm

Realm é um silo isolado de usuários, roles, clients e configurações. Use um realm separado por tenant lógico. Nunca use o master para suas aplicações — ele existe só para gerenciar Keycloak.

7.1 Criar o realm myapp

Admin Console → dropdown de realms (canto superior esquerdo) → Create Realm → nome myappCreate.

Configurações iniciais recomendadas (em Realm settings):

  • General → Display name: MyApp.
  • Login → Forgot password: on, Remember me: on, Email as username: preferência sua.
  • Tokens → Access Token Lifespan: 5min (curto, refresh cobre). SSO Session Idle: 30min. SSO Session Max: 10h.
  • Email → configure SMTP (Gmail App Password ou outro). Sem isso, reset de senha não funciona.

7.2 Roles

Em Realm rolesCreate role:

  • app-user — usuário comum dos apps.
  • app-admin — pode acessar áreas administrativas dos apps.
  • report-read, report-write — exemplo de granularidade por domínio.

Você também pode criar Client roles (escopo do client), mas para ambientes pequenos, com poucos apps, realm roles bastam.

7.3 Groups

Em GroupsCreate groupusuarios-internos. Em Role mapping do grupo, atribua app-user. Depois, qualquer usuário colocado no grupo herda a role — bem mais escalável do que atribuir role usuário a usuário.

7.4 Client api-backend (confidential)

Backend Spring Boot é um resource server: só valida JWT, não inicia login. Estritamente falando, ele não precisa de client próprio para funcionar — basta apontar o issuer-uri. Mas é boa prática criar para service accounts (machine-to-machine) e audience mapping.

ClientsCreate client:

  • Client type: OpenID Connect.
  • Client ID: api-backend.
  • Next.
  • Client authentication: On (= confidential).
  • Authorization: Off (use só se for usar Keycloak Authorization Services).
  • Authentication flow: marcar Service accounts roles (M2M); demais podem ficar marcados.
  • Next.
  • Root URL / Home URL / Valid redirect URIs / Web origins: pode deixar vazio se for apenas resource server.
  • Save.

Em Credentials anote o Client secret (vai para o .env do backend, se ele precisar fazer M2M).

Em Client scopesapi-backend-dedicatedAdd mapperFrom predefined mappers → adicione o Audience mapper apontando para api-backend. Isso garante que o JWT terá aud: api-backend, que o backend valida.

7.5 Client app-frontend (public + PKCE)

Frontend Vue é public client — vive no navegador, não consegue guardar segredo.

ClientsCreate client:

  • Client type: OpenID Connect.
  • Client ID: app-frontend.
  • Next.
  • Client authentication: Off (= public).
  • Authentication flow: marcar Standard flow (Authorization Code). Desmarcar Implicit flow (deprecated) e Direct access grants (Resource Owner Password — só para debug local).
  • Next.
  • Root URL: https://app.example.com.
  • Valid redirect URIs:
    • https://app.example.com/*
    • http://localhost:5173/* (Vite dev — só em ambiente de dev).
  • Valid post logout redirect URIs: mesma coisa.
  • Web origins: + (significa “use os redirect URIs como CORS origin”).
  • Save.

Em Advanced habilite:

  • Proof Key for Code Exchange Code Challenge Method: S256. Sem isso, o frontend até pode usar PKCE, mas o Keycloak não força — um client malicioso poderia fazer sem.

7.6 Usuários

UsersAdd user:

  • Username, email, first/last name.
  • Email verified: on (caso contrário, primeiro login pede verificação por email).
  • Save. Vá em Credentials → set password (desmarque Temporary se for um usuário real, marque para forçar troca no primeiro login).
  • Em Groups adicione ao usuarios-internos.

8. Integração backend (Spring Boot 3.3.x)

Spring Security tem suporte nativo a OAuth2 Resource Server desde a versão 5. Não use mais o adapter legado do Keycloak (foi removido).

8.1 Dependências (Gradle)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
}

8.2 application.yaml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.example.com/realms/myapp
          # audiences é opcional aqui — validamos no converter abaixo
          # jwk-set-uri é resolvido automaticamente via discovery

O issuer-uri aponta para o realm. Spring resolve automaticamente o jwk-set-uri em /realms/myapp/protocol/openid-connect/certs chamando o discovery endpoint no startup. Se o Keycloak estiver fora do ar nesse momento, a app falha ao subir — considere jwk-set-uri explícito + retry se isso for problema.

8.3 SecurityFilterChain

package com.example.app.shared.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("app-admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
            );
        return http.build();
    }

    private Converter<Jwt, AbstractAuthenticationToken> jwtAuthConverter() {
        JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
        scopes.setAuthorityPrefix("SCOPE_");

        JwtAuthenticationConverter conv = new JwtAuthenticationConverter();
        conv.setJwtGrantedAuthoritiesConverter(jwt -> {
            // 1. scopes (claim "scope")
            Collection<org.springframework.security.core.GrantedAuthority> auths =
                new java.util.ArrayList<>(scopes.convert(jwt));

            // 2. realm_access.roles -> ROLE_*
            Map<String, Object> realmAccess = jwt.getClaim("realm_access");
            if (realmAccess != null) {
                Object roles = realmAccess.get("roles");
                if (roles instanceof List<?> rolesList) {
                    rolesList.stream()
                        .filter(String.class::isInstance)
                        .map(String.class::cast)
                        .map(r -> new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + r))
                        .forEach(auths::add);
                }
            }
            return auths;
        });
        // username = preferred_username em vez de sub
        conv.setPrincipalClaimName("preferred_username");
        return conv;
    }
}

O detalhe importante é o claim realm_access.roles — é onde Keycloak coloca roles do realm. Spring Security espera authorities numa lista plana, então o converter extrai e prefixa com ROLE_ para casar com .hasRole("app-admin").

8.4 Cookies HttpOnly (variação opcional)

Em vez de enviar o JWT no header Authorization, alguns projetos preferem guardá-lo em cookie HttpOnly (mitiga XSS, mas exige CSRF token). O ResourceServer aceita ambos via BearerTokenResolver:

@Bean
BearerTokenResolver bearerTokenResolver() {
    return request -> {
        String cookie = Stream.ofNullable(request.getCookies())
            .flatMap(java.util.Arrays::stream)
            .filter(c -> "access_token".equals(c.getName()))
            .map(jakarta.servlet.http.Cookie::getValue)
            .findFirst()
            .orElse(null);
        if (cookie != null) return cookie;
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    };
}

Registre no filter chain:

.oauth2ResourceServer(oauth2 -> oauth2
    .bearerTokenResolver(bearerTokenResolver())
    .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
);

9. Integração frontend (Vue 3 + keycloak-js)

9.1 Instalação

npm install keycloak-js

9.2 Inicialização (src/auth/keycloak.ts)

import Keycloak from 'keycloak-js'

const config = (window as any).APP_CONFIG ?? import.meta.env

export const keycloak = new Keycloak({
  url: config.VITE_KEYCLOAK_URL,           // https://keycloak.example.com
  realm: config.VITE_KEYCLOAK_REALM,        // myapp
  clientId: config.VITE_KEYCLOAK_CLIENT_ID, // app-frontend
})

export async function initKeycloak(): Promise<boolean> {
  const authenticated = await keycloak.init({
    onLoad: 'check-sso',
    silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
    pkceMethod: 'S256',
    checkLoginIframe: false, // evita problemas com SameSite=Lax + iframe
  })
  if (authenticated) {
    scheduleRefresh()
  }
  return authenticated
}

function scheduleRefresh() {
  setInterval(async () => {
    try {
      // renova se faltar menos de 60s
      const refreshed = await keycloak.updateToken(60)
      if (refreshed) {
        // opcional: log, ou broadcast para outras abas
      }
    } catch {
      // refresh expirou -> manda para login
      keycloak.login()
    }
  }, 30_000)
}

export function login() {
  return keycloak.login({ redirectUri: window.location.href })
}

export function logout() {
  return keycloak.logout({ redirectUri: window.location.origin })
}

9.3 silent-check-sso.html (em public/)

<!doctype html>
<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin)
    </script>
  </body>
</html>

Esse arquivo é carregado num iframe escondido. Quando o Keycloak vê sessão existente, redireciona para essa URL com code=... e o iframe manda os dados para a janela pai via postMessage. Resultado: SSO sem piscar a tela.

9.4 Bootstrap no main.ts

import { createApp } from 'vue'
import App from './App.vue'
import { initKeycloak, keycloak } from './auth/keycloak'

initKeycloak().then(() => {
  const app = createApp(App)
  app.provide('keycloak', keycloak)
  app.mount('#app')
})

9.5 Interceptor Axios

import axios from 'axios'
import { keycloak } from '@/auth/keycloak'

export const http = axios.create({
  baseURL: (window as any).APP_CONFIG?.VITE_API_BASE_URL,
  withCredentials: true, // necessário se backend usa cookie HttpOnly
})

http.interceptors.request.use(async (cfg) => {
  if (keycloak.authenticated) {
    try {
      await keycloak.updateToken(30)
    } catch {
      keycloak.login()
      return Promise.reject(new axios.Cancel('refresh failed'))
    }
    cfg.headers.Authorization = `Bearer ${keycloak.token}`
  }
  return cfg
})

http.interceptors.response.use(
  (r) => r,
  (err) => {
    if (err.response?.status === 401) {
      keycloak.login()
    }
    return Promise.reject(err)
  }
)

9.6 Logout completo

keycloak.logout({ redirectUri: window.location.origin })

Isso desloga no Keycloak (não só no app local), invalidando a sessão SSO. Outros apps que dependem do mesmo realm vão pedir login na próxima navegação.


10. Themes customizados

Keycloak permite customizar telas de login, account, email e admin via themes — pastas com FreeMarker templates, CSS e mensagens i18n. O recomendado é manter o tema em um repositório próprio (ex. keycloak-theme).

10.1 Estrutura

keycloak-theme/
  mytheme/
    login/
      theme.properties        # parent=keycloak, styles=css/custom.css
      login.ftl                # tela de login
      template.ftl             # layout base
      resources/
        css/custom.css
        img/logo.svg
      messages/
        messages_pt_BR.properties
        messages_en.properties
    account/                   # opcional
    email/                     # opcional (emails de reset, verify, etc)

theme.properties mínimo:

parent=keycloak
import=common/keycloak
styles=css/login.css css/custom.css
locales=pt-BR,en

10.2 Deploy via volume

Já existe no docker-compose.yaml da seção 4.2:

volumes:
  - ./keycloak/themes:/opt/keycloak/themes:ro

Copie o conteúdo de keycloak-theme/mytheme para /opt/docker/keycloak/themes/mytheme e reinicie o container.

docker compose restart keycloak

10.3 Ativar o tema

Admin Console → realm myappRealm settingsThemes:

  • Login theme: mytheme
  • Account theme: mytheme (se você criou)
  • Email theme: mytheme (se você criou)
  • Internationalization: On, default pt-BR, supported pt-BR, en.

Save. O próximo /auth já renderiza com o tema.

Dica de desenvolvimento: durante a iteração, ative em Realm settingsThemes a opção Cache themes: Off (ou KC_SPI_THEME_STATIC_MAX_AGE=-1 no container). Lembre-se de ligar novamente em produção.


11. Backup e upgrade

11.1 Backup do database

Faça pg_dump periódico (cron diário) do db keycloak:

docker exec postgres17 pg_dump -U keycloak -Fc -f /tmp/keycloak.dump keycloak
docker cp postgres17:/tmp/keycloak.dump /opt/backups/keycloak-$(date +%F).dump
docker exec postgres17 rm /tmp/keycloak.dump

Restore (DB precisa existir vazio):

docker exec -i postgres17 pg_restore -U postgres -d keycloak --clean --if-exists < keycloak.dump

11.2 Export de realm (opcional, complementar)

Útil para versionar a configuração de realm em git:

docker exec keycloak /opt/keycloak/bin/kc.sh export \
  --dir /tmp/realm-export \
  --realm myapp \
  --users realm_file
docker cp keycloak:/tmp/realm-export ./myapp-realm-export

Atenção: senhas/segredos são exportados com hash. Trate como secret.

11.3 Upgrade major (26 → 27)

Keycloak segue versionamento semver mas quebra entre majors. Procedimento genérico:

  1. Leia o release notes e o Upgrading Guide da versão de destino (sempre): https://www.keycloak.org/docs/latest/upgrading/
  2. Backup completo do database (seção 11.1).
  3. Pare o container atual: docker compose stop keycloak.
  4. Snapshot do volume de temas/providers se houver alterações.
  5. Atualize a tag no compose: image: quay.io/keycloak/keycloak:27.0.
  6. Se você usa Dockerfile custom com kc.sh build, reconstrua a imagem — o build precisa ser refeito a cada major.
  7. Suba: docker compose up -d keycloak. Keycloak detecta a versão antiga no DB e roda migrations Liquibase automaticamente. O primeiro boot pode demorar bastante (alguns minutos) — não interrompa.
  8. Acompanhe logs: docker compose logs -f keycloak. Veja se há warnings tipo “feature X removed”.
  9. Smoke test: login admin, login num app, validação de JWT no backend, refresh token. Veja o /.well-known/openid-configuration continua respondendo.

Em caso de problema sério, restaure backup do DB e volte a imagem anterior.

11.4 Rotação de chaves de assinatura

Em Realm settingsKeysProviders → adicione um novo provider rsa-generated (ou rsa-enc-generated), com prioridade maior que a atual, e desative a antiga. JWKS expõe ambas por algum tempo para que tokens emitidos antes ainda validem.


12. Troubleshooting

”Invalid parameter: redirect_uri”

O redirect URI passado pelo frontend não bate com nenhum Valid Redirect URI configurado no client.

  • Verifique se o URI é exatamente igual (case-sensitive, com / no fim ou sem).
  • Use wildcard com cuidado: https://app.example.com/* cobre qualquer path; mas não cobre subdomínios.
  • Não esqueça o http://localhost:5173/* para dev Vite.

”HTTPS required” / “Mixed Content”

Keycloak está enxergando a requisição como HTTP porque o reverse proxy não está repassando o protocolo.

  • Confirme KC_PROXY_HEADERS=xforwarded no compose.
  • Confirme que o nginx envia X-Forwarded-Proto $scheme;.
  • Confirme KC_HOSTNAME=https://... (com https:// explícito).
  • Em 26.x, o antigo KC_PROXY=edge foi removido; use KC_PROXY_HEADERS.

CORS bloqueado

O frontend chama /realms/.../token ou seu backend e o navegador bloqueia.

  • Para chamadas ao Keycloak: cheque Web Origins no client. Use + para herdar dos redirect URIs.
  • Para chamadas ao backend: o backend precisa devolver Access-Control-Allow-Origin apropriado. Configure CORS no Spring separado — isso não é responsabilidade do Keycloak.

”Token is not active” / clock skew

Tokens JWT têm exp e nbf. Se o relógio do servidor da app diverge do Keycloak por mais de 30s, validação falha.

  • Configure NTP/chrony em todos os hosts.
  • Em application.yaml do Spring você pode aumentar a tolerância:
    spring:
      security:
        oauth2:
          resourceserver:
            jwt:
              issuer-uri: ...
    e definir um JwtTimestampValidator com Duration.ofSeconds(60) como skew permitido.

JWT com muitas roles e claims passa de 4KB; alguns proxies/CDNs cortam cookies maiores.

  • Reduza claims no token: em Client scopes tire mappers redundantes.
  • Use lightweight access tokens (feature em 26): em ClientAdvancedUse lightweight access token. Joga claims só no ID token.
  • Aumente buffers no nginx (já configurado na seção 6).

”Failed to fetch JWK set” / startup lento

Spring busca JWKS no startup; se Keycloak está down, falha.

  • Garanta depends_on correto no compose com condition: service_healthy.
  • Considere spring.security.oauth2.resourceserver.jwt.jwk-set-uri explícito para evitar uma chamada extra ao discovery.
  • Em produção, Spring cacheia o JWKS por padrão; rotação de chave pode exigir reinício se o cache é longo. Configure JwtDecoders com cache explícito se necessário.

”iframe blocked” / checkLoginIframe

Cookies SameSite=Lax (default moderno) bloqueiam o iframe escondido que o keycloak-js usa para verificar sessão.

  • checkLoginIframe: false no init (já está no exemplo).
  • Use o silent-check-sso.html em vez disso (já configurado).

”Account is not fully set up”

Usuário tem required actions pendentes (verificar email, configurar OTP, trocar senha).

  • Users → usuário → Required user actions → remova/complete.

Logs detalhados

docker compose exec keycloak /opt/keycloak/bin/kc.sh show-config

Para debug temporário:

KC_LOG_LEVEL: "info,org.keycloak.events:debug,org.keycloak:debug"

E reinicie. Não esqueça de voltar — debug é verboso.


13. Operação com kcadm.sh

O kcadm.sh é o CLI oficial. Útil para automação (provisionar client/role num pipeline, scriptar mudanças entre ambientes).

13.1 Login

docker exec -it keycloak /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://localhost:8080 \
  --realm master \
  --user admin-prod \
  --password '<sua-senha-forte>'

Salva o token em /opt/keycloak/.config/kcadm.config dentro do container.

13.2 Operações comuns

# Listar realms
docker exec keycloak /opt/keycloak/bin/kcadm.sh get realms --fields realm,enabled

# Criar role
docker exec keycloak /opt/keycloak/bin/kcadm.sh create roles \
  -r myapp -s name=relatorios-export

# Adicionar usuário a um grupo
docker exec keycloak /opt/keycloak/bin/kcadm.sh add-members \
  -r myapp --gname usuarios-internos --uusername fulano

# Atualizar client (ex: aumentar token lifespan via attribute)
docker exec keycloak /opt/keycloak/bin/kcadm.sh update \
  clients/<UUID-DO-CLIENT> -r myapp \
  -s 'attributes."access.token.lifespan"=900'

# Exportar realm
docker exec keycloak /opt/keycloak/bin/kc.sh export \
  --realm myapp --dir /tmp/exp --users realm_file

# Importar realm
docker exec keycloak /opt/keycloak/bin/kc.sh import \
  --dir /tmp/exp

13.3 Idempotência

kcadm.sh create falha se já existe; update falha se não existe. Em scripts, use get antes para checar, ou capture o erro:

docker exec keycloak /opt/keycloak/bin/kcadm.sh create roles \
  -r myapp -s name=foo 2>/dev/null || true

Para reprodutibilidade completa, versione um realm-export.json no git (com segredos sanitizados) e importe via KC_FEATURES=import no boot, ou rode kc.sh import no entrypoint.


14. Referências