Skip to content

Keycloak

Keycloak es una solución de código abierto para gestión de identidad y acceso (IAM - Identity and Access Management) de grado empresarial. Esta basada en la máquina virtual de Java (JVM). Proporciona autenticación, autorización, Single Sign-On (SSO), y gestión de usuarios para aplicaciones y servicios. Como ventajas, esta herramienta nos ofrece lo siguiente:

  • Single Sign-On (SSO): Una sola autenticación para múltiples aplicaciones
  • Protocolos estándar: OAuth 2.0, OpenID Connect, SAML 2.0
  • Federación de identidad: Integración con proveedores externos
  • Gestión centralizada: Usuarios, roles y permisos en un solo lugar
  • Personalización: Temas, flujos de autenticación, extensiones
  • Open Source: Gratuito y con comunidad activa

Esta es una guía esencial para introducirse en la gestión de identidad con Keycloak

Conceptos clave

Para introducirnos en Keycloak es necesario comprender los siguientes conceptos:

  • Realm: Traduce reino (o gobierno) y se refiere a un espacio aislado que gestiona usuarios, roles y aplicaciones
  • Client: Aplicación que solicita autenticación a Keycloak
  • User: Usuario que se autentica en el sistema
  • Role: Permisos asignados a usuarios o grupos
  • Identity Provider: Proveedor externo de identidad (Google, GitHub, LDAP)
  • Token: JWT que contiene información de autenticación y autorización

Instalación de Keycloak

Docker (modo desarrollo)

bash
docker run -d \
  --name keycloak \
  -p 8080:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:latest \
  start-dev

# Acceder a la consola
open http://localhost:8080

Instalación local

bash
# Descargar Keycloak
wget https://github.com/keycloak/keycloak/releases/download/23.0.0/keycloak-23.0.0.zip
unzip keycloak-23.0.0.zip
cd keycloak-23.0.0

# Iniciar en modo desarrollo
bin/kc.sh start-dev

Configuración inicial

Crear Realm y Client

Lo primero es definir el realm siguiendo los siguientes pasos:

  1. Acceder a http://localhost:8080
  2. Login con admin/admin
  3. Crear nuevo Realm: my-realm
  4. Crear Client:
    • Client ID: my-app
    • Client Protocol: openid-connect
    • Access Type: confidential
    • Valid Redirect URIs: http://localhost:3000/*
  5. Obtener Client Secret de la pestaña Credentials

Crear usuario de prueba

Debe crearse un primer usuario para gestionar algún acceso. Para ello considera lo siguiente:

  1. En el Realm my-realm, ir a Users
  2. Crear usuario: testuser
  3. Establecer contraseña en Credentials
  4. Asignar roles si es necesario

Ejemplo esencial: Obtener token con JBang

java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.httpcomponents.client5:httpclient5:5.2.1
//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
//JAVA 21

import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.HashMap;
import java.util.Map;

/**
 * Keycloak Token Example
 * Obtains access token using password grant
 */
public class KeycloakTokenExample {

    public static void main(String[] args) throws Exception {
        System.out.println("Keycloak Token Example");
        System.out.println("=====================\n");

        // Configuración
        String keycloakUrl = "http://localhost:8080";
        String realm = "my-realm";
        String clientId = "my-app";
        String clientSecret = "your-client-secret"; // Obtener de Keycloak
        String username = "testuser";
        String password = "testpass";

        // URL del token endpoint
        String tokenUrl = String.format(
            "%s/realms/%s/protocol/openid-connect/token",
            keycloakUrl, realm
        );

        // Preparar request
        Map<String, String> params = new HashMap<>();
        params.put("grant_type", "password");
        params.put("client_id", clientId);
        params.put("client_secret", clientSecret);
        params.put("username", username);
        params.put("password", password);

        String formData = params.entrySet().stream()
            .map(e -> e.getKey() + "=" + e.getValue())
            .reduce((a, b) -> a + "&" + b)
            .orElse("");

        // Hacer request
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(tokenUrl);
            post.setHeader("Content-Type", "application/x-www-form-urlencoded");
            post.setEntity(new StringEntity(formData));

            String response = client.execute(post, httpResponse -> 
                EntityUtils.toString(httpResponse.getEntity())
            );

            // Parse response
            ObjectMapper mapper = new ObjectMapper();
            JsonNode json = mapper.readTree(response);

            String accessToken = json.get("access_token").asText();
            String refreshToken = json.get("refresh_token").asText();
            int expiresIn = json.get("expires_in").asInt();

            System.out.println("Access Token obtenido:");
            System.out.println(accessToken.substring(0, 50) + "...");
            System.out.println("\nExpira en: " + expiresIn + " segundos");
            System.out.println("\nRefresh Token:");
            System.out.println(refreshToken.substring(0, 50) + "...");
        }
    }
}

Ejecutar:

bash
jbang KeycloakTokenExample.java

Ejemplo: Validar token y obtener info de usuario

java
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.apache.httpcomponents.client5:httpclient5:5.2.1
//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
//JAVA 21

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

public class KeycloakUserInfoExample {

    public static void main(String[] args) throws Exception {
        System.out.println("Keycloak UserInfo Example");
        System.out.println("=========================\n");

        if (args.length == 0) {
            System.err.println("Usage: jbang KeycloakUserInfoExample.java <access_token>");
            System.exit(1);
        }

        String accessToken = args[0];
        String keycloakUrl = "http://localhost:8080";
        String realm = "my-realm";

        // URL del userinfo endpoint
        String userInfoUrl = String.format(
            "%s/realms/%s/protocol/openid-connect/userinfo",
            keycloakUrl, realm
        );

        // Hacer request
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet get = new HttpGet(userInfoUrl);
            get.setHeader("Authorization", "Bearer " + accessToken);

            String response = client.execute(get, httpResponse -> 
                EntityUtils.toString(httpResponse.getEntity())
            );

            // Parse response
            ObjectMapper mapper = new ObjectMapper();
            JsonNode json = mapper.readTree(response);

            System.out.println("User Information:");
            System.out.println("  Username: " + json.get("preferred_username").asText());
            System.out.println("  Email: " + json.get("email").asText());
            System.out.println("  Name: " + json.get("name").asText());
            System.out.println("  Sub: " + json.get("sub").asText());

            if (json.has("realm_access")) {
                System.out.println("\nRoles:");
                json.get("realm_access").get("roles").forEach(role -> 
                    System.out.println("  - " + role.asText())
                );
            }
        }
    }
}

Ejecutar:

bash
# Primero obtener token
TOKEN=$(jbang KeycloakTokenExample.java | grep "Access Token" -A 1 | tail -1)

# Luego validar
jbang KeycloakUserInfoExample.java "$TOKEN"

Integración con Spring Boot

El archivo application.yml de Spring Boot puede contener lo siguiente:

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8080/realms/my-realm
          jwk-set-uri: http://localhost:8080/realms/my-realm/protocol/openid-connect/certs

keycloak:
  realm: my-realm
  auth-server-url: http://localhost:8080
  resource: my-app
  credentials:
    secret: your-client-secret

Para la clase SecurityConfig se puede aplicar un filterChain como el siguiente:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

Configuración de roles y permisos

Crear roles

Para crear roles considera lo siguiente:

  1. En Keycloak Admin Console, ir a Realm Roles
  2. Crear roles: admin, user, manager
  3. Asignar roles a usuarios en Users - Role Mappings

Client Scopes

Para crear scopes considera lo siguinte:

  1. Ir a Client Scopes
  2. Create scope: "custom-scope"
  3. Agregar mappers para incluir claims personalizados

Comandos útiles

bash
# Exportar configuración de realm
bin/kc.sh export --dir /tmp/keycloak-export --realm my-realm

# Importar configuración
bin/kc.sh import --dir /tmp/keycloak-export

# Modo producción
bin/kc.sh start --hostname=keycloak.example.com

# Ver logs
docker logs -f keycloak

Endpoints destacados

A continuación, veamos los endpoints para token, userinfo, logout, openid-configuration (Well-known) y certs (JWKS):

bash
POST http://localhost:8080/realms/{realm}/protocol/openid-connect/token
GET http://localhost:8080/realms/{realm}/protocol/openid-connect/userinfo
POST http://localhost:8080/realms/{realm}/protocol/openid-connect/logout
GET http://localhost:8080/realms/{realm}/.well-known/openid-configuration
GET http://localhost:8080/realms/{realm}/protocol/openid-connect/certs

Nótese que debe pasarse como parámetro realm.

Buenas prácticas para continuar con Keycloak

Para resumir, Keycloack nos ofrece...

  • Usar HTTPS en producción: Nunca exponer Keycloak sin TLS
  • Rotar secrets: Cambiar client secrets regularmente
  • Tokens de corta duración: Configurar TTL apropiado para tokens
  • Refresh tokens: Implementar renovación automática
  • Roles granulares: Definir permisos específicos por funcionalidad
  • Auditoría: Habilitar logs de eventos de seguridad
  • Backup: Respaldar configuración de realms regularmente

Keycloak es esencial para aplicaciones empresariales que requieren autenticación y autorización robusta, proporcionando una solución completa de IAM con soporte para estándares modernos y fácil integración con aplicaciones existentes.