Integrando os stacks com Keycloak

TL;DR

Nas quatro últimas seções do sub-galho 4 você viu cinco stacks diferentes — Spring, Django, FastAPI, Express, NestJS, Gin — cada um resolvendo autenticação e autorização à sua própria maneira, com seu próprio idioma de framework. Esta nota costura essas peças com um único IdP: o Keycloak não sabe, e não precisa saber, que existem cinco stacks diferentes consumindo os tokens que ele emite. Ele fala um protocolo (OAuth 2.1 + OpenID Connect), expõe um conjunto de endpoints padronizados (/authorize, /token, /certs — o JWKS), e cada stack faz o mesmo trabalho de fundo: buscar a chave pública do Keycloak, verificar a assinatura do token, conferir iss e aud, e traduzir os claims de roles do Keycloak (realm_access, resource_access) para o modelo de autorização nativo daquele framework. O fluxo de referência para 2026 combina três papéis que já vimos separadamente: a SPA nunca guarda token algum; um BFF (Backend-for-Frontend) troca o código OAuth pelo token e o mantém em sessão server-side, atrás de um cookie HttpOnly; e a API (o resource server — Spring, FastAPI, Gin, ou o próprio Nest/Express jogando os dois papéis) só valida o que chega, nunca emite nada. Depois do fluxo, uma tabela comparativa mostra, lado a lado, qual biblioteca cada stack usa, que papel ela assume e qual é o trecho de configuração que efetivamente faz a validação — issuer-uri no Spring, PyJWKClient no FastAPI, openid-client no Node, go-oidc no Gin. Fechamos nas armadilhas que se repetem em qualquer stack: aud ausente por padrão no Keycloak, clock skew mal calibrado, e cache de JWKS feito errado.

Um IdP, N stacks — o problema que esta nota resolve

Cada nota do sub-galho 4 tratou a integração com um Identity Provider externo como uma peça isolada — “a nota do Spring integra com Keycloak”, “a nota do FastAPI valida via JWKS” — mas nunca colocou as cinco lado a lado. Isso é deliberado: cada stack merece ser entendido em seus próprios termos antes de compará-los. Mas na prática, uma organização raramente roda só um stack. Um SaaS B2B típico em 2026 tem times diferentes escolhendo frameworks diferentes por motivos legítimos — o time de dados prefere FastAPI, o time de plataforma roda Spring, um squad novo escolheu NestJS — e todos eles, se a decisão de identidade foi feita direito, apontam para o mesmo Keycloak.


graph TD
    KC["Keycloak — 1 realm<br/>Authorization Server + IdP"]

    KC -->|"issuer-uri + JWKS"| SP["Spring Boot<br/>Resource Server"]
    KC -->|"JWKS + PyJWKClient"| FA["FastAPI<br/>Resource Server"]
    KC -->|"openid-client<br/>OIDC client"| NE["NestJS / Express<br/>BFF + Resource Server"]
    KC -->|"go-oidc<br/>Verifier"| GI["Gin<br/>Resource Server"]

    style KC fill:#4A90D9,color:#fff
    style SP fill:#F5A623,color:#000
    style FA fill:#F5A623,color:#000
    style NE fill:#F5A623,color:#000
    style GI fill:#F5A623,color:#000

O que esses quatro consumidores têm em comum não é biblioteca — é contrato. Todos falam contra o mesmo /.well-known/openid-configuration, todos buscam a mesma chave pública no /certs (o endpoint JWKS do Keycloak), e todos precisam decidir a mesma coisa: este token foi assinado por quem eu acho que assinou, ainda é válido, e é para mim que ele foi emitido? A resposta técnica difere por stack — issuer-uri declarativo no Spring, uma função explícita no FastAPI, um verifier construído à mão no Gin — mas a pergunta é idêntica em todos. É essa pergunta única, respondida de quatro formas, que esta nota organiza.

Versão em aberto

Keycloak 26.7.0 (lançado em julho de 2026) é o baseline desta nota — a mesma linha coberta em SG5-01 e SG5-02. As bibliotecas de cada stack também têm data de validade: spring-security-oauth2-resource-server (Spring Security 6.x), PyJWT + PyJWKClient (Python 3.12+), openid-client v6 (Node ESM-only), coreos/go-oidc v3. Ecossistemas mudam — trate os nomes de pacote como fotografia de 2026, não lei eterna.

O fluxo de referência: SPA + BFF + API

A pergunta que este fluxo resolve já apareceu, de lados diferentes, em SG2-05 (onde guardar o token no browser) e nas notas de Express e NestJS do SG4 (BFF como resposta a XSS/CSRF em SPA). Aqui ela ganha forma completa, com o Keycloak explicitamente no papel de Authorization Server.

A premissa: uma SPA rodando inteiramente no navegador não tem onde guardar um token com segurança. localStorage é legível por qualquer script — inclusive um script malicioso injetado via XSS ou uma dependência de terceiros comprometida — e mesmo sessionStorage não resolve o problema de fundo, só limita o escopo temporal. A resposta que RFC 9700 recomenda explicitamente, e que virou o padrão de mercado em 2026, é nunca deixar o token tocar o JavaScript da SPA: um servidor fino — o BFF — fica entre a SPA e o Keycloak, conduz a dança OAuth inteira, e devolve à SPA só um cookie de sessão HttpOnly — inacessível a JavaScript, portanto imune a roubo via XSS1.


sequenceDiagram
    participant SPA as SPA (browser)
    participant BFF as BFF (Node/NestJS/Spring)
    participant KC as Keycloak (Authorization Server)
    participant API as API (Resource Server<br/>Spring/FastAPI/Gin)

    SPA->>BFF: 1. GET /auth/login
    BFF->>KC: 2. Redirect /authorize<br/>(Authorization Code + PKCE)
    KC-->>SPA: 3. Tela de login Keycloak
    SPA->>KC: 4. Usuário autentica
    KC-->>BFF: 5. Redirect /callback?code=...
    BFF->>KC: 6. POST /token (back channel)<br/>code + code_verifier
    KC-->>BFF: 7. access_token + refresh_token + id_token
    Note over BFF: 8. Tokens ficam no servidor<br/>(sessão Redis, nunca no browser)
    BFF-->>SPA: 9. Set-Cookie: session=xyz<br/>(HttpOnly, Secure, SameSite)
    SPA->>BFF: 10. GET /api/orders<br/>Cookie: session=xyz
    BFF->>API: 11. GET /orders<br/>Authorization: Bearer <access_token>
    API->>API: 12. Valida JWT (JWKS do Keycloak)
    API-->>BFF: 13. 200 OK + dados
    BFF-->>SPA: 14. 200 OK + dados

Repare na divisão de papéis, porque é ela que organiza o resto da nota:

  • A SPA nunca vê um token OAuth. Ela só sabe que tem um cookie de sessão, e delega toda chamada de API ao BFF, que faz o proxy.
  • O BFF é o único componente que fala os dois lados do protocolo: ele é client OIDC do Keycloak (obtém tokens) e ao mesmo tempo mantém sua própria sessão web com a SPA (emite o cookie). Na prática, isso costuma ser Express, NestJS ou um Spring Cloud Gateway — qualquer stack com bom suporte a openid-client/OAuth2 client e sessão server-side.
  • A API (o resource server de verdade — pode ser Spring, FastAPI, Gin, ou uma segunda instância Node) nunca fala com o Keycloak para autenticar ninguém interativamente; ela só recebe o access_token que o BFF já obteve, e faz o trabalho que é o cerne desta nota: validar esse token sem chamar o Keycloak a cada request, usando a chave pública cacheada do JWKS.

Esse desenho não é o único válido — uma API mobile nativa, por exemplo, pode dispensar o BFF porque o app já tem onde guardar token com alguma segurança (keychain do SO) — mas é o desenho de referência para 2026 sempre que existe uma SPA no meio, e é o pano de fundo que todo o resto da nota assume.

Como cada stack se pluga: mapa e trecho-chave

Cada subseção abaixo assume que você já leu a nota correspondente do SG4 — aqui não se repete o que é Spring, FastAPI, NestJS/Express ou Gin; só se mostra a fatia que fala com o Keycloak.

Spring — resource server declarativo

A nota SG4-01 já cobriu o Spring como Authorization Server (emitindo tokens) e apontou para as 18 notas de Java/Segurança que cobrem o Spring como client (obtendo tokens de terceiros). O papel que falta amarrar aqui é o mais simples de configurar de todos: Spring como resource server validando um token que o Keycloak emitiu. Uma única propriedade resolve descoberta, JWKS e validação de issuer:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.exemplo.com/realms/saas-b2b

Com só isso, o Spring busca {issuer-uri}/.well-known/openid-configuration, descobre o endpoint JWKS (/realms/saas-b2b/protocol/openid-connect/certs), baixa as chaves públicas, cacheia, e valida iss e assinatura automaticamente em toda requisição autenticada2. O que não vem de graça é a tradução dos claims do Keycloak para o modelo de autorização do Spring: por padrão, o Spring espera authorities num claim scope/scp, mas o Keycloak entrega roles em realm_access.roles (roles do realm inteiro) e resource_access.<client_id>.roles (roles específicas daquele client). Um JwtAuthenticationConverter customizado faz essa ponte:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    var converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        var realmRoles = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
        var roles = (Collection<String>) realmRoles.getOrDefault("roles", List.of());
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    });
    return converter;
}

Esse converter é plugado no SecurityFilterChain via .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))3 — e a partir daí, @PreAuthorize("hasRole('ADMIN')") (SG4-01/Java-Segurança 07) enxerga roles do Keycloak como se fossem authorities nativas do Spring.

FastAPI — validação explícita via JWKS

A nota SG4-03 já mostrou o desenho inteiro: FastAPI não assume nada sobre auth, então “validar um token do Keycloak” é só mais uma dependência (Depends) que você escreve. A peça que faz a ponte com o Keycloak é o PyJWKClient, que resolve descoberta de chave e cache automaticamente — sem reimplementar rotação de JWKS na mão:

import jwt
from jwt import PyJWKClient
from fastapi import Depends, HTTPException
 
ISSUER = "https://auth.exemplo.com/realms/saas-b2b"
jwks_client = PyJWKClient(f"{ISSUER}/protocol/openid-connect/certs")
 
def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    try:
        signing_key = jwks_client.get_signing_key_from_jwt(token)
        payload = jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience="orders-api",
            issuer=ISSUER,
        )
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Token inválido")
 
    realm_roles = payload.get("realm_access", {}).get("roles", [])
    client_roles = payload.get("resource_access", {}).get("orders-api", {}).get("roles", [])
    return {"sub": payload["sub"], "roles": realm_roles + client_roles}

get_signing_key_from_jwt lê o kid (key ID) do header do token, busca a chave correspondente no JWKS — cacheada internamente pelo PyJWKClient, sem round-trip ao Keycloak a cada request — e devolve a chave pública certa mesmo que o Keycloak tenha rotacionado chaves recentemente4. Repare que audience e issuer são passados explicitamente ao jwt.decode() — sem isso, PyJWT não valida nenhum dos dois por padrão, o que abre a porta para o problema de aud que fechamos nas armadilhas.

NestJS / Express — BFF e OIDC client

Este é o papel duplo descrito no fluxo de referência: SG4-04 e SG4-05 já cobriram openid-client como o cliente OIDC canônico do ecossistema Node, incluindo Issuer.discover() e a troca Authorization Code + PKCE. O ponto que fecha aqui é que Express/NestJS, jogando o papel de BFF, não usa openid-client para validar tokens de terceiros (isso é papel da API) — usa para obter tokens do Keycloak em nome da SPA, e depois gerenciar sua própria sessão:

import * as client from 'openid-client'
 
const config = await client.discovery(
  new URL('https://auth.exemplo.com/realms/saas-b2b'),
  'bff-client-id',
  'bff-client-secret'
)
 
// callback do Keycloak — troca o code pelo token, guarda na sessão do BFF
app.get('/callback', async (req, res) => {
  const tokens = await client.authorizationCodeGrant(config, new URL(req.url, req.headers.origin), {
    pkceCodeVerifier: req.session.codeVerifier,
    expectedState: req.session.state,
  })
  req.session.accessToken = tokens.access_token   // fica no servidor, nunca vai pro browser
  req.session.refreshToken = tokens.refresh_token
  res.redirect('/')
})

nest-keycloak-connect está sem manutenção real

O pacote histórico keycloak-connect (e sua casca nest-keycloak-connect) depende de uma biblioteca que o próprio time do Keycloak sinalizou como legado desde a versão 19 — a última versão publicada tem mais de dois anos, e a recomendação oficial do projeto Keycloak é migrar para openid-client diretamente5. Times novos em 2026 não devem começar um projeto Nest/Express novo com nest-keycloak-connect — o caminho recomendado é openid-client puro, orquestrado manualmente como acima, ou embrulhado num AuthGuard do Nest (o mesmo padrão de “strategy vira guard” já visto em SG4-05).

Quando Express/NestJS assume também o papel de resource server (recebendo tokens de um mobile app, por exemplo, sem BFF no meio), a validação segue o mesmo princípio do FastAPI e do Spring: buscar o JWKS, verificar assinatura, iss, aud — bibliotecas como jwks-rsa + jsonwebtoken (já visto em Node/Segurança 04) resolvem isso sem reinventar nada.

Gin — verifier construído com go-oidc

SG4-06 já separou as duas ferramentas: golang-jwt/jwt para validar localmente contra uma chave que você já tem, e coreos/go-oidc para quando o emissor é externo — exatamente o caso do Keycloak. A descoberta OIDC constrói o Provider, e o Provider constrói o Verifier — sem tocar em JWKS manualmente:

ctx := context.Background()
provider, err := oidc.NewProvider(ctx, "https://auth.exemplo.com/realms/saas-b2b")
if err != nil {
    log.Fatal(err)
}
 
verifier := provider.Verifier(&oidc.Config{ClientID: "orders-api"})
 
func AuthMiddleware(verifier *oidc.IDTokenVerifier) gin.HandlerFunc {
    return func(c *gin.Context) {
        rawToken := extractBearerToken(c.Request)
        if rawToken == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "token ausente"})
            return
        }
 
        idToken, err := verifier.Verify(c.Request.Context(), rawToken)
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "token inválido"})
            return
        }
 
        var claims struct {
            RealmAccess struct{ Roles []string } `json:"realm_access"`
        }
        idToken.Claims(&claims)
        c.Set("roles", claims.RealmAccess.Roles)
        c.Next()
    }
}

oidc.NewProvider faz a descoberta (/.well-known/openid-configuration) uma única vez na inicialização, e o Verifier resultante já sabe buscar e cachear o JWKS internamente — cada chamada a verifier.Verify() reaproveita a chave em cache, sem round-trip ao Keycloak por requisição6. O ClientID passado em oidc.Config é o que o go-oidc usa para validar o aud do token — se ele não bater com o valor configurado no Config, Verify() retorna erro, fechando sozinho a armadilha de audience que voltamos a discutir abaixo.

Tabela comparativa

StackPapel no fluxoBibliotecaTrecho-chave de validação
Spring BootResource serverspring-security-oauth2-resource-serverissuer-uri (descoberta automática de JWKS) + JwtAuthenticationConverter para realm_access
FastAPIResource serverPyJWT + PyJWKClientjwt.decode(token, signing_key.key, audience=..., issuer=...)
Express / NestJSBFF (client OIDC) + resource server opcionalopenid-client v6client.authorizationCodeGrant() para obter token; jwks-rsa para validar quando resource server
Gin (Go)Resource servercoreos/go-oidc v3provider.Verifier(&oidc.Config{ClientID}).Verify(ctx, token)
Django (SG4-02)Resource server (via DRF)PyJWT + PyJWKClient (mesmo padrão do FastAPI)Mesma lógica de jwt.decode embrulhada em uma Authentication class do DRF

O padrão que atravessa a tabela inteira: nenhum stack chama o Keycloak a cada request. Todos descobrem o JWKS uma vez (ou periodicamente, via cache com TTL), guardam a chave pública em memória, e validam localmente — a mesma economia que já apareceu em SG2-05 ao comparar tokens opacos (exigem introspecção a cada uso) com JWT (verificável localmente). É essa propriedade — validação offline, sem round-trip síncrono ao IdP — que torna JWT + JWKS a escolha natural para múltiplos resource servers atrás de um único Keycloak.

Mapeamento de roles do Keycloak: realm_access vs resource_access

Todo trecho de código acima tropeça na mesma decisão de modelagem, então vale nomear o que ela significa. O Keycloak distingue dois níveis de role, e essa distinção é anterior a qualquer stack — ela nasce na modelagem de SG5-01:

  • realm_access.roles — roles do realm inteiro, válidas para qualquer client registrado naquele realm. Um usuário com a role admin no realm é admin em todo lugar que aceitar aquele token, independentemente de qual API está validando.
  • resource_access.<client_id>.roles — roles por client (Keycloak chama de “client roles”). Um usuário pode ser viewer no client reports-api e editor no client orders-api, ao mesmo tempo, no mesmo token — porque cada client tem seu próprio namespace de roles.

graph LR
    JWT["access_token (JWT)"] --> RA["realm_access.roles<br/>['admin', 'default-roles-saas-b2b']"]
    JWT --> RSA["resource_access"]
    RSA --> C1["orders-api.roles<br/>['editor']"]
    RSA --> C2["reports-api.roles<br/>['viewer']"]

    style JWT fill:#4A90D9,color:#fff
    style RA fill:#F5A623,color:#000
    style RSA fill:#F5A623,color:#000

A decisão prática que cada stack precisa tomar — e que a tabela de trechos-chave acima já resolveu de facto — é qual dos dois usar para autorização fina dentro daquela API específica. A resposta que se repete: roles de realm_access funcionam bem para papéis amplos e transversais (“é admin da organização”), enquanto resource_access.<client_id> é o lugar certo para permissões que só fazem sentido dentro daquele serviço (“pode editar pedidos”, só relevante para a orders-api). Misturar os dois sem critério — jogar tudo em realm_access porque é mais simples de ler no código — tende a produzir uma explosão de roles genéricas no realm inteiro, o mesmo role explosion já discutido em SG3-01.

Armadilhas comuns

aud (audience) não bate — e o Keycloak não coloca isso por padrão

O que acontece: uma API valida o aud do token (boa prática, como vimos acima) e recebe 401 para tokens que, do ponto de vista do usuário, “deveriam funcionar” — o login foi bem-sucedido, o token chegou, mas a validação falha. Por quê: por padrão, o Keycloak define o aud do access token como o client_id que pediu o token — não necessariamente o client_id da API que vai consumi-lo. Se a SPA (client saas-web) obtém o token e o repassa para a orders-api, o aud no token é saas-web, não orders-api — e uma API que valida audience="orders-api" rejeita, corretamente, um token que não foi emitido para ela. Como evitar: criar um client scope dedicado com um mapper do tipo Audience, configurado para incluir o client_id da API de destino (ou um valor customizado, se preferir um audience lógico como orders-api em vez do client técnico) — e atribuir esse scope ao client da SPA, para que toda emissão de token já inclua o aud correto7. Em ambientes com múltiplas APIs atrás do mesmo Keycloak, isso normalmente significa um client scope por “superfície lógica de API”, não um scope genérico único.

Clock skew mal calibrado — tokens expiram cedo ou tarde demais

O que acontece: tokens são rejeitados como “expirados” segundos antes do horário esperado, ou — pior — continuam válidos além do previsto porque a checagem de expiração foi desabilitada para “resolver” o primeiro problema. Por quê: relógios de servidores diferentes (o Keycloak e cada API) nunca estão perfeitamente sincronizados; um desvio de alguns segundos entre NTP configurado de forma diferente em cada máquina é normal, não um bug. Bibliotecas de validação de JWT permitem configurar uma margem de tolerância (leeway) na checagem de exp/iat/nbf — mas a tentação de “só desabilitar a checagem de expiração” para fazer o erro sumir remove uma das poucas garantias reais de segurança do token. Como evitar: configurar uma margem de tolerância pequena e explícita — algo entre 30 segundos e alguns minutos, nunca mais que isso — em vez de desabilitar a validação. A maioria das bibliotecas usadas nesta nota aceita esse parâmetro diretamente (leeway no PyJWT, configuração de clock skew no go-oidc); a regra de ouro é margem limitada, não perdão ilimitado.

Cache de JWKS ausente, ou cache eterno demais

O que acontece: ou a API faz uma requisição HTTP ao Keycloak para buscar o JWKS a cada validação de token (latência e carga desnecessárias, e um ponto de falha síncrono a mais), ou cacheia a resposta indefinidamente e não percebe quando o Keycloak rotaciona suas chaves de assinatura — resultando em tokens legítimos rejeitados após uma rotação de chave em produção. Como evitar: todas as bibliotecas usadas nesta nota (PyJWKClient, o cache interno do Spring, go-oidc, jwks-rsa) já implementam cache com TTL razoável por padrão — a armadilha real é reimplementar a busca de JWKS na mão (por exemplo, um fetch() manual sem cache algum) em vez de usar a biblioteca madura. Quando o cache expira e um kid desconhecido aparece (sinal de rotação de chave), o comportamento correto é buscar o JWKS de novo uma vez antes de rejeitar — não assumir imediatamente que o token é inválido.

Em entrevista

A pergunta “como você integraria múltiplos serviços em stacks diferentes com um IdP central?” testa exatamente a costura que esta nota fez: entender que o protocolo é o contrato comum, e que a implementação por stack é só tradução de um mesmo conceito para idiomas diferentes.

Uma resposta fraca lista bibliotecas: “no Spring uso issuer-uri, no FastAPI uso PyJWT…” — é factualmente correto, mas não demonstra entendimento do porquê de cada peça existir.

Uma resposta forte amarra o protocolo à decisão arquitetural: “o Keycloak expõe um endpoint OIDC discovery padrão e um JWKS; qualquer resource server, em qualquer linguagem, faz a mesma coisa de fundo — descobre a chave pública, cacheia, valida assinatura/issuer/audience localmente, sem round-trip síncrono ao IdP por request. A diferença entre stacks é só quanto trabalho a biblioteca abstrai: o Spring resolve isso com uma propriedade, o Gin exige eu construir o verifier explicitamente — mas o modelo de confiança é idêntico. E para uma SPA no meio, eu nunca deixo o token tocar o browser: um BFF fala com o Keycloak, guarda o token em sessão server-side, e entrega só um cookie HttpOnly — é a recomendação direta da RFC 9700.”

Entrevistador: “Você tem uma SPA em React consumindo três APIs diferentes — uma em Spring, uma em FastAPI, uma em Go. Como você desenharia a autenticação?”

Resposta fraca: “Cada API valida o token com sua própria biblioteca JWT.”

Resposta forte: “Primeiro, a SPA não guarda token nenhum — ela fala só com um BFF, que conduz o Authorization Code + PKCE contra o Keycloak e mantém a sessão em cookie HttpOnly. O BFF repassa o access_token para cada API via header Authorization. Cada API — Spring, FastAPI, Go — valida esse token da mesma forma conceitual: busca o JWKS do Keycloak uma vez, cacheia, confere assinatura, issuer e audience localmente. A única coisa que varia entre elas é a biblioteca: issuer-uri no Spring, PyJWKClient no FastAPI, go-oidc no Go. Se cada API precisa de audience diferente, eu configuro client scopes com Audience mapper no Keycloak, um por API, e atribuo à SPA os scopes que ela precisa solicitar.”

How to explain it in English

“Keycloak issues tokens, the stacks consume them — that’s the whole story. Every resource server, regardless of language, does the same thing under the hood: discover the public key via the JWKS endpoint, cache it, verify signature/issuer/audience locally, no synchronous round-trip to the IdP per request. What differs is how much of that work each framework’s library hides — Spring resolves it with a single issuer-uri property, Gin makes you build the verifier by hand — but the trust model is identical. For a browser-based SPA, tokens never touch client-side JavaScript: a BFF talks to Keycloak, keeps tokens server-side in session, and hands the SPA only an HttpOnly cookie — the pattern RFC 9700 explicitly recommends.”

PTEN
Servidor de recursosResource server
Servidor de autorizaçãoAuthorization server
Backend-for-FrontendBackend-for-Frontend (BFF)
Conjunto de chaves públicasJSON Web Key Set (JWKS)
Descoberta OIDCOIDC discovery
Reivindicação de audiênciaAudience claim
Tolerância de relógioClock skew / leeway
Roles do realmRealm roles
Roles do clientClient roles
Mapeador de audiênciaAudience mapper
Cache de chavesKey caching
Rotação de chaveKey rotation

O que vem a seguir

Esta nota fecha o sub-galho 5 — Keycloak — e com ele, os cinco sub-galhos da trilha Auth e Identidade estão completos: fundamentos de identidade, os protocolos (OAuth 2.1/OIDC), autorização e multi-tenancy, os stacks, e o IdP que os une. O que falta é costurar tudo isso numa única decisão de ponta a ponta — não mais “como validar um token”, mas “que arquitetura de identidade eu desenho, do zero, para um produto real”.

  • Desenhando a identidade de um SaaS B2B do zero — o capstone do galho-pai: build vs buy (Keycloak vs Auth0/Cognito vs better-auth embutido), sessão vs token vs BFF, social + passkeys + senha, SSO/SAML/SCIM para clientes enterprise, RBAC+ReBAC por organização, MFA — a síntese de todos os cinco sub-galhos desta trilha.
  • 01 - Keycloak — realms, clients e flows — a arquitetura de realm/client/role que os mappers de audience e as roles desta nota pressupõem.
  • 02 - Keycloak em produção — HA, upgrade, Organizations — o Keycloak que essas integrações precisam encontrar rodando de verdade.

Fontes

Footnotes

  1. FusionAuth, A Guide to Backend-for-Frontend (BFF) Auth — RFC 9700 recomenda manter tokens fora do browser.

  2. Baeldung, A Quick Guide to Using Keycloak with Spring Bootissuer-uri e descoberta automática de JWKS.

  3. Medium (K. Selman Poyraz), Spring Boot & Keycloak: Role-Based Authorization with JWTJwtAuthenticationConverter customizado para realm_access.

  4. Skycloak, FastAPI Authentication with KeycloakPyJWKClient e cache de chave por kid.

  5. GitHub, Keycloak-nodejs-connect deprecation is there any other alternatives? — recomendação oficial de migrar para openid-client.

  6. pkg.go.dev, oidc packageNewProvider e Verifier com cache interno de JWKS.

  7. DEV Community, How To Configure Audience In Keycloak — mapper de audiência por client scope.