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| PGFluxo 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 dadosPontos 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
codee troque por um token.
3. Pré-requisitos
| Item | Versão / detalhe |
|---|---|
| Docker | 24+ com plugin compose v2 |
| PostgreSQL | 17.x acessível na network proxy |
| Domínio | keycloak.example.com com A record para 203.0.113.10 |
| TLS | gerenciado pelo nginx-proxy via Let’s Encrypt (acme.sh ou certbot) |
| RAM | mínimo 1 GB para o container (recomendado 2 GB) |
| Java | embutido 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;
SQLEm 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: trueArquivo .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 keycloakAguarde a linha:
Listening on: http://0.0.0.0:8080
Management interface listening on http://0.0.0.0:9000E o healthcheck virar healthy:
docker compose ps keycloak5. 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:
- Após o primeiro
docker compose up, abrahttps://keycloak.example.com(a partir de um host dentro da rede confiável, se você aplicou a allowlist do passo 6). - Logue com
${KEYCLOAK_BOOTSTRAP_ADMIN}/${KEYCLOAK_BOOTSTRAP_PASSWORD}. - Vá em master realm → Users → Add user.
- Crie um usuário admin permanente (ex.
admin-prod), com email, marque Email verified, defina senha em Credentials, atribua roleadminem Role mapping. - Deslogue, logue como o novo usuário, confirme que tem todos os poderes.
- Em Users delete o usuário bootstrap temporário.
- Edite o
.envremovendo as variáveisKEYCLOAK_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=xforwardedno Keycloak diz para confiar nos cabeçalhosX-Forwarded-*. Sem isso, Keycloak vêhttp://keycloak:8080e 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
/adminfica acessível porque está atrás dessa restrição. Em um cenário publicamente exposto, restrinja/adminem um location separado.
Recarregue o nginx:
docker exec nginx-proxy nginx -t && docker exec nginx-proxy nginx -s reloadTeste 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 myapp → Create.
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 roles → Create 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 Groups → Create group → usuarios-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.
Clients → Create 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 scopes → api-backend-dedicated → Add mapper → From 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.
Clients → Create 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
Users → Add 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 discoveryO 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-js9.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,en10.2 Deploy via volume
Já existe no docker-compose.yaml da seção 4.2:
volumes:
- ./keycloak/themes:/opt/keycloak/themes:roCopie o conteúdo de keycloak-theme/mytheme para /opt/docker/keycloak/themes/mytheme e reinicie o container.
docker compose restart keycloak10.3 Ativar o tema
Admin Console → realm myapp → Realm settings → Themes:
- Login theme:
mytheme - Account theme:
mytheme(se você criou) - Email theme:
mytheme(se você criou) - Internationalization: On, default
pt-BR, supportedpt-BR, en.
Save. O próximo /auth já renderiza com o tema.
Dica de desenvolvimento: durante a iteração, ative em Realm settings → Themes a opção Cache themes: Off (ou
KC_SPI_THEME_STATIC_MAX_AGE=-1no 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.dumpRestore (DB precisa existir vazio):
docker exec -i postgres17 pg_restore -U postgres -d keycloak --clean --if-exists < keycloak.dump11.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-exportAtençã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:
- Leia o release notes e o Upgrading Guide da versão de destino (sempre): https://www.keycloak.org/docs/latest/upgrading/
- Backup completo do database (seção 11.1).
- Pare o container atual:
docker compose stop keycloak. - Snapshot do volume de temas/providers se houver alterações.
- Atualize a tag no compose:
image: quay.io/keycloak/keycloak:27.0. - Se você usa Dockerfile custom com
kc.sh build, reconstrua a imagem — obuildprecisa ser refeito a cada major. - 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. - Acompanhe logs:
docker compose logs -f keycloak. Veja se há warnings tipo “feature X removed”. - Smoke test: login admin, login num app, validação de JWT no backend, refresh token. Veja o
/.well-known/openid-configurationcontinua 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 settings → Keys → Providers → 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=xforwardedno compose. - Confirme que o nginx envia
X-Forwarded-Proto $scheme;. - Confirme
KC_HOSTNAME=https://...(comhttps://explícito). - Em 26.x, o antigo
KC_PROXY=edgefoi removido; useKC_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-Originapropriado. 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.yamldo Spring você pode aumentar a tolerância:
e definir umspring: security: oauth2: resourceserver: jwt: issuer-uri: ...JwtTimestampValidatorcomDuration.ofSeconds(60)como skew permitido.
Cookie 4KB / “Request Header Too Large”
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 Client → Advanced → Use 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_oncorreto no compose comcondition: service_healthy. - Considere
spring.security.oauth2.resourceserver.jwt.jwk-set-uriexplí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
JwtDecoderscomcacheexplí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: falsenoinit(já está no exemplo).- Use o
silent-check-sso.htmlem 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-configPara 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/exp13.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 || truePara 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
- Documentação oficial: https://www.keycloak.org/documentation
- Server Administration Guide: https://www.keycloak.org/docs/latest/server_admin/
- Server Installation/Configuration: https://www.keycloak.org/server/configuration
- Container guide (Docker): https://www.keycloak.org/server/containers
- Reverse proxy: https://www.keycloak.org/server/reverseproxy
- Hostname v2: https://www.keycloak.org/server/hostname
- Todas as opções de config: https://www.keycloak.org/server/all-config
- Upgrading guide: https://www.keycloak.org/docs/latest/upgrading/
- Securing Applications: https://www.keycloak.org/docs/latest/securing_apps/
- JavaScript adapter (keycloak-js): https://www.keycloak.org/securing-apps/javascript-adapter
- Spring Boot OAuth2 Resource Server: https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html
- OIDC spec: https://openid.net/specs/openid-connect-core-1_0.html
- OAuth 2.1 (draft com PKCE obrigatório): https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/