Arquitetura softwareSeguranca
Keycloak e IAM
Baixar PDFKeycloak e uma solucao open-source de Identity and Access Management (IAM) que fornece Single Sign-On (SSO), autenticacao e autorizacao para aplicacoes.
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
| Conceito | Descricao |
|---|---|
| Realm | Dominio de seguranca isolado |
| Client | Aplicacao que usa Keycloak |
| User | Usuario do sistema |
| Role | Permissao atribuivel |
| Group | Conjunto de usuarios |
| Identity Provider | Provedor externo (Google, GitHub) |
| Protocol | OpenID 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=trueConfiguracao de Clients
Tipos de Client
| Tipo | Uso | Flow |
|---|---|---|
| Public | SPAs, Mobile apps | Authorization Code + PKCE |
| Confidential | Backend services | Client Credentials |
| Bearer-only | APIs | Validacao 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
| Tipo | Escopo |
|---|---|
| Realm Roles | Globais no realm |
| Client Roles | Especificas de um client |
| Composite Roles | Combinacao 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.cssReact 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=trueKubernetes 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"