Keycloak e uma solucao open-source de Identity and Access Management (IAM) que fornece Single Sign-On (SSO), autenticacao e autorizacao para aplicacoes.
# 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
{
"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 })
}
))
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"