Arquitetura software Seguranca 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.
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
# 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 :
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
Tipo Uso Flow Public SPAs, Mobile apps Authorization Code + PKCE Confidential Backend services Client Credentials Bearer-only APIs Validacao de token
{
"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"
}
}
{
"clientId" : "my-backend" ,
"enabled" : true ,
"publicClient" : false ,
"serviceAccountsEnabled" : true ,
"standardFlowEnabled" : false ,
"clientAuthenticatorType" : "client-secret"
}
// 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 }
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 }
}
# 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;
}
}
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 })
}
))
Tipo Escopo Realm Roles Globais no realm Client Roles Especificas de um client Composite Roles Combinacao de outras roles
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' }],
})
themes/
└── my-theme/
├── login/
│ ├── theme.properties
│ ├── resources/
│ │ ├── css/
│ │ │ └── styles.css
│ │ └── img/
│ │ └── logo.png
│ └── messages/
│ └── messages_pt_BR.properties
└── account/
└── ...
parent =keycloak
import =common/keycloak
styles =css/login.css css/styles.css
// 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>
)
}
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 () {
}
}
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 () {
}
}
{
"alias" : "google" ,
"providerId" : "google" ,
"enabled" : true ,
"config" : {
"clientId" : "${GOOGLE_CLIENT_ID}" ,
"clientSecret" : "${GOOGLE_CLIENT_SECRET}" ,
"defaultScope" : "openid profile email"
}
}
{
"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"
}
}
# 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
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"