Kaique Mitsuo Silva Yamamoto
Arquitetura software

Keycloak e IAM

Keycloak e uma solucao open-source de Identity and Access Management (IAM) que fornece Single Sign-On (SSO), autenticacao e autorizacao para aplicacoes.

Conceitos Fundamentais

ConceitoDescricao
RealmDominio de seguranca isolado
ClientAplicacao que usa Keycloak
UserUsuario do sistema
RolePermissao atribuivel
GroupConjunto de usuarios
Identity ProviderProvedor externo (Google, GitHub)
ProtocolOpenID Connect ou SAML

Instalacao

Docker

# docker-compose.yml
version: '3.8'

services:
  keycloak:
    image: quay.io/keycloak/keycloak:23.0
    command: start-dev
    environment:
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=admin
      - KC_DB=postgres
      - KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
      - KC_DB_USERNAME=keycloak
      - KC_DB_PASSWORD=keycloak
    ports:
      - "8080:8080"
    depends_on:
      - postgres

  postgres:
    image: postgres:15
    environment:
      - POSTGRES_DB=keycloak
      - POSTGRES_USER=keycloak
      - POSTGRES_PASSWORD=keycloak
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Kubernetes (Helm)

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install keycloak bitnami/keycloak \
  --set auth.adminUser=admin \
  --set auth.adminPassword=admin \
  --set postgresql.enabled=true

Configuracao de Clients

Tipos de Client

TipoUsoFlow
PublicSPAs, Mobile appsAuthorization Code + PKCE
ConfidentialBackend servicesClient Credentials
Bearer-onlyAPIsValidacao de token

Client SPA (React/Next.js)

{
  "clientId": "my-spa",
  "enabled": true,
  "publicClient": true,
  "redirectUris": ["http://localhost:3000/*"],
  "webOrigins": ["http://localhost:3000"],
  "standardFlowEnabled": true,
  "directAccessGrantsEnabled": false,
  "attributes": {
    "pkce.code.challenge.method": "S256"
  }
}

Client Backend (Service Account)

{
  "clientId": "my-backend",
  "enabled": true,
  "publicClient": false,
  "serviceAccountsEnabled": true,
  "standardFlowEnabled": false,
  "clientAuthenticatorType": "client-secret"
}

Integracao com Aplicacoes

Next.js com NextAuth

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import KeycloakProvider from 'next-auth/providers/keycloak'

const handler = NextAuth({
  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_CLIENT_ID!,
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
      issuer: process.env.KEYCLOAK_ISSUER,
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token
        token.refreshToken = account.refresh_token
        token.idToken = account.id_token
      }
      return token
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken
      return session
    },
  },
})

export { handler as GET, handler as POST }

React Native com Expo

import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser'

WebBrowser.maybeCompleteAuthSession()

const discovery = {
  authorizationEndpoint: 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth',
  tokenEndpoint: 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token',
  revocationEndpoint: 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/revoke',
}

export function useKeycloakAuth() {
  const [request, response, promptAsync] = AuthSession.useAuthRequest(
    {
      clientId: 'my-mobile-app',
      redirectUri: AuthSession.makeRedirectUri({ scheme: 'myapp' }),
      scopes: ['openid', 'profile', 'email'],
      usePKCE: true,
    },
    discovery
  )

  const login = () => promptAsync()

  return { login, response }
}

Spring Boot

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.example.com/realms/myrealm
          jwk-set-uri: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            );
        return http.build();
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
        converter.setAuthoritiesClaimName("realm_access.roles");
        converter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
        return jwtConverter;
    }
}

Node.js (Express + Passport)

import passport from 'passport'
import { Strategy as OAuth2Strategy } from 'passport-oauth2'

passport.use(new OAuth2Strategy({
    authorizationURL: 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth',
    tokenURL: 'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token',
    clientID: process.env.KEYCLOAK_CLIENT_ID,
    clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/callback'
  },
  (accessToken, refreshToken, profile, done) => {
    // Buscar ou criar usuario
    return done(null, { accessToken, profile })
  }
))

Roles e Autorizacao

Tipos de Roles

TipoEscopo
Realm RolesGlobais no realm
Client RolesEspecificas de um client
Composite RolesCombinacao de outras roles

Configuracao via API Admin

import KcAdminClient from '@keycloak/keycloak-admin-client'

const kcAdminClient = new KcAdminClient({
  baseUrl: 'https://keycloak.example.com',
  realmName: 'myrealm',
})

await kcAdminClient.auth({
  grantType: 'client_credentials',
  clientId: 'admin-cli',
  clientSecret: process.env.KEYCLOAK_ADMIN_SECRET,
})

// Criar role
await kcAdminClient.roles.create({
  name: 'premium-user',
  description: 'Usuario premium com acesso extra',
})

// Atribuir role ao usuario
await kcAdminClient.users.addRealmRoleMappings({
  id: userId,
  roles: [{ id: roleId, name: 'premium-user' }],
})

Temas Customizados

Estrutura

themes/
└── my-theme/
    ├── login/
    │   ├── theme.properties
    │   ├── resources/
    │   │   ├── css/
    │   │   │   └── styles.css
    │   │   └── img/
    │   │       └── logo.png
    │   └── messages/
    │       └── messages_pt_BR.properties
    └── account/
        └── ...

theme.properties

parent=keycloak
import=common/keycloak

styles=css/login.css css/styles.css

React Theme (Keycloakify)

// src/login/pages/Login.tsx
import { getKcClsx } from 'keycloakify/login/lib/kcClsx'
import type { PageProps } from 'keycloakify/login/pages/PageProps'

export default function Login(props: PageProps<'login.ftl'>) {
  const { kcContext, i18n } = props
  const { kcClsx } = getKcClsx({ ...props })

  return (
    <div className="custom-login-page">
      <img src="/logo.png" alt="Logo" />
      <h1>{i18n.msg('loginTitle')}</h1>

      <form action={kcContext.url.loginAction} method="post">
        <input
          name="username"
          type="text"
          placeholder={i18n.msg('username')}
        />
        <input
          name="password"
          type="password"
          placeholder={i18n.msg('password')}
        />
        <button type="submit">
          {i18n.msg('doLogIn')}
        </button>
      </form>
    </div>
  )
}

SPI (Service Provider Interface)

Autenticador Customizado

public class CustomAuthenticator implements Authenticator {

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        UserModel user = context.getUser();

        // Logica customizada
        if (isBlocked(user)) {
            context.failure(AuthenticationFlowError.INVALID_USER);
            return;
        }

        context.success();
    }

    @Override
    public void action(AuthenticationFlowContext context) {
        // Processar form submission
    }

    @Override
    public boolean requiresUser() {
        return true;
    }

    @Override
    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
        return true;
    }

    @Override
    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
    }

    @Override
    public void close() {
    }
}

Event Listener

public class CustomEventListener implements EventListenerProvider {

    @Override
    public void onEvent(Event event) {
        if (event.getType() == EventType.LOGIN) {
            String userId = event.getUserId();
            String ip = event.getIpAddress();

            // Enviar para sistema de auditoria
            auditService.logLogin(userId, ip);
        }
    }

    @Override
    public void onEvent(AdminEvent event, boolean includeRepresentation) {
        // Eventos administrativos
    }

    @Override
    public void close() {
    }
}

Identity Brokering

Configurar Google como IdP

{
  "alias": "google",
  "providerId": "google",
  "enabled": true,
  "config": {
    "clientId": "${GOOGLE_CLIENT_ID}",
    "clientSecret": "${GOOGLE_CLIENT_SECRET}",
    "defaultScope": "openid profile email"
  }
}

SAML com Azure AD

{
  "alias": "azure-ad",
  "providerId": "saml",
  "enabled": true,
  "config": {
    "singleSignOnServiceUrl": "https://login.microsoftonline.com/.../saml2",
    "nameIDPolicyFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
    "principalType": "ATTRIBUTE",
    "principalAttribute": "email"
  }
}

Producao

Configuracao

# Variaveis de ambiente
KC_DB=postgres
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME=keycloak
KC_DB_PASSWORD=${DB_PASSWORD}

KC_HOSTNAME=auth.example.com
KC_HOSTNAME_STRICT=true
KC_PROXY=edge

KC_CACHE=ispn
KC_CACHE_CONFIG_FILE=cache-ispn.xml

KC_METRICS_ENABLED=true
KC_HEALTH_ENABLED=true

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: keycloak
          image: quay.io/keycloak/keycloak:23.0
          args: ["start"]
          env:
            - name: KC_DB
              value: postgres
            - name: KC_CACHE_STACK
              value: kubernetes
            - name: JAVA_OPTS_APPEND
              value: "-Djgroups.dns.query=keycloak-headless"
          resources:
            requests:
              memory: "512Mi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "2000m"

Recursos