Módulo 1 · Lección 02

Keycloak en Docker y el realm tienda

Levantas Keycloak 26.8 con PostgreSQL, recorres la consola para entender cómo se organiza y escribes tu primer programa Go que habla con Keycloak.

≈ 60 min Carpeta: tienda/pasos/paso-02 Requiere Docker

En este capítulo

Trabajas en
Keycloak (el realm tienda) y un primer programa Go, discover.
Carpeta
tienda/pasos/paso-02
Archivos
Todos nuevos: infra/docker-compose.yml, infra/realm/tienda-realm.json, cmd/discover/main.go.
En marcha
Docker. Al final de la lección, Keycloak del paso 02.
Comprueba
go -C tools/comprobar run . -paso 2 (desde course/); no descarga nada, solo usa la biblioteca estándar de Go.

Al terminar sabrás

  • Arrancar Keycloak en modo desarrollo con Docker Compose y PostgreSQL.
  • Qué son un realm, un client, un rol y un usuario, y cómo se relacionan.
  • Importar un realm desde JSON al arrancar y exportarlo después.
  • Leer el documento de descubrimiento y el JWKS desde Go.

Cómo se organiza Keycloak

Un servidor Keycloak con el realm master y el realm tienda, que contiene clients, roles y usuarios Servidor Keycloak · localhost:8080 realm master usuario admin Solo para administrar Keycloak. Nunca para usuarios de tus apps. realm tienda CLIENTS (aplicaciones) tienda-web account-console api-pedidos · lección 5 facturacion · lección 7 ROLES DE REALM cliente admin default-roles-tienda USUARIOS ana carlos las flechas son «role mappings»: qué roles tiene cada usuario
Los recuadros discontinuos son elementos que añadirás en lecciones posteriores o que Keycloak crea solo.
ConceptoQué esEn la tienda
RealmUn espacio aislado (un tenant) con sus propios usuarios, clients, roles y claves de firma. Es el issuer de los tokens.tienda
ClientUna aplicación o servicio registrado que puede pedir tokens o recibirlos.tienda-web; luego api-pedidos, facturacion…
UsuarioUna persona (o la service account de un client) que se autentica.ana, carlos
Rol de realmPermiso válido en todo el realm. Aparece en realm_access.roles.cliente, admin
Rol de clientPermiso que solo tiene sentido para un client. Aparece en resource_access.<client>.roles.Lección 6
GrupoConjunto de usuarios que heredan roles.Lección 9
Client scopePaquete reutilizable de claims (mappers) y roles que se añade a los tokens.Lecciones 5–6
¿Por qué no usar el realm master?

master contiene a los administradores de Keycloak. Mezclar ahí a tus clientes significa que un fallo de configuración podría dar a un cliente poderes sobre el propio servidor. Crea siempre un realm por producto o entorno.

1. Levantar Keycloak

Todo está en tienda/pasos/paso-02/infra/:

# Keycloak + PostgreSQL para el curso "Go + Keycloak".
# Solo para desarrollo: usa start-dev (HTTP, cachés locales, sin hostname fijo).
#
#   docker compose up -d          # arrancar
#   docker compose logs -f keycloak
#   docker compose down -v        # parar y BORRAR datos (fuerza reimportar el realm)

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
      interval: 5s
      timeout: 3s
      retries: 10

  keycloak:
    image: quay.io/keycloak/keycloak:26.8.0
    # --import-realm importa cada .json de /opt/keycloak/data/import
    # (si el realm ya existe, la importación se omite).
    command: start-dev --import-realm
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
      KC_DB: postgres
      KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak
      KC_HEALTH_ENABLED: "true"
    ports:
      - "127.0.0.1:8080:8080"   # consola y endpoints OIDC
      - "127.0.0.1:9000:9000"   # puerto de gestión: /health/ready
    volumes:
      - ./realm:/opt/keycloak/data/import
    # «host.docker.internal» = tu máquina, vista desde el contenedor. Docker
    # Desktop (Windows, macOS) ya lo define; esta línea lo añade en Linux.
    # Lo usa el back-channel logout (lección 4) para llamar a tienda-web.
    extra_hosts:
      - "host.docker.internal:host-gateway"
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  pgdata:
PiezaPor qué
start-devModo desarrollo: HTTP sin TLS, hostname deducido de cada petición, temas sin caché. Nunca en producción (allí: start).
--import-realmAl arrancar importa los .json de /opt/keycloak/data/import. Si el realm ya existe, no lo toca.
KC_BOOTSTRAP_ADMIN_*Crea el administrador temporal del realm master solo la primera vez (base de datos vacía). Sustituyen a las antiguas KEYCLOAK_ADMIN*, obsoletas desde la versión 26.
KC_DB*Usar PostgreSQL en vez de la base H2 embebida: los datos sobreviven a reinicios y el comportamiento se parece al de producción.
KC_HEALTH_ENABLEDExpone /health/ready en el puerto de gestión 9000.
extra_hostsDefine host.docker.internal (tu máquina vista desde el contenedor) también en Linux; Docker Desktop ya lo trae. Solo se usa en la lección 4.
127.0.0.1:8080Publica el puerto solo en tu máquina, no en la red local.

Arranca y espera a que esté listo:

cd tienda/pasos/paso-02/infra
docker compose up -d
docker compose logs -f keycloak     # espera a ver "Listening on: http://0.0.0.0:8080" y sal con Ctrl+C

curl -s http://localhost:9000/health/ready
# {"status": "UP", ...}

La primera vez tarda: descarga las imágenes, crea las tablas e importa el realm. En los logs verás una línea que dice que el realm tienda se ha importado.

2. El realm como código

En lugar de configurar todo con clics, el realm vive en un archivo versionable. Este es el estado inicial:

{
  "realm": "tienda",
  "displayName": "Tienda Go",
  "enabled": true,
  "sslRequired": "external",
  "registrationAllowed": false,
  "loginWithEmailAllowed": true,
  "accessTokenLifespan": 300,
  "ssoSessionIdleTimeout": 1800,
  "roles": {
    "realm": [
      {
        "name": "cliente",
        "description": "Puede ver el catálogo y gestionar sus propios pedidos"
      },
      {
        "name": "admin",
        "description": "Puede ver y gestionar todos los pedidos"
      },
      {
        "name": "offline_access",
        "description": "${role_offline-access}"
      },
      {
        "name": "uma_authorization",
        "description": "${role_uma_authorization}"
      },
      {
        "name": "default-roles-tienda",
        "description": "${role_default-roles}",
        "composite": true,
        "composites": {
          "realm": ["offline_access", "uma_authorization"],
          "client": {
            "account": ["view-profile", "manage-account"]
          }
        }
      }
    ]
  },
  "defaultRole": {
    "name": "default-roles-tienda",
    "description": "${role_default-roles}",
    "composite": true
  },
  "users": [
    {
      "username": "ana",
      "enabled": true,
      "email": "ana@tienda.test",
      "emailVerified": true,
      "firstName": "Ana",
      "lastName": "Cliente",
      "credentials": [
        { "type": "password", "value": "ana123", "temporary": false }
      ],
      "realmRoles": ["default-roles-tienda", "cliente"]
    },
    {
      "username": "carlos",
      "enabled": true,
      "email": "carlos@tienda.test",
      "emailVerified": true,
      "firstName": "Carlos",
      "lastName": "Admin",
      "credentials": [
        { "type": "password", "value": "carlos123", "temporary": false }
      ],
      "realmRoles": ["default-roles-tienda", "cliente", "admin"]
    }
  ],
  "clients": [
    {
      "clientId": "tienda-web",
      "name": "Tienda Web",
      "description": "Aplicación web en Go (login con Authorization Code + PKCE)",
      "enabled": true,
      "protocol": "openid-connect",
      "publicClient": false,
      "clientAuthenticatorType": "client-secret",
      "secret": "tienda-web-secret",
      "standardFlowEnabled": true,
      "implicitFlowEnabled": false,
      "directAccessGrantsEnabled": false,
      "serviceAccountsEnabled": false,
      "rootUrl": "http://localhost:3000",
      "baseUrl": "/",
      "redirectUris": ["http://localhost:3000/callback"],
      "webOrigins": ["http://localhost:3000"],
      "attributes": {
        "pkce.code.challenge.method": "S256",
        "post.logout.redirect.uris": "http://localhost:3000/"
      }
    }
  ]
}

Lo esencial:

  • Roles cliente y admin. Los otros tres (default-roles-tienda, offline_access, uma_authorization) son los que Keycloak crea en todo realm; los declaramos para poder asignar el rol por defecto a los usuarios importados, que si no, no podrían usar su consola de cuenta.
  • Usuarios con contraseña no temporal y sus roles.
  • Client tienda-web: confidencial (publicClient: false) con secreto fijo, solo Authorization Code (standardFlowEnabled), PKCE obligatorio con S256, y las URLs exactas de vuelta tras el login y el logout.
  • accessTokenLifespan: 300 (5 min) y ssoSessionIdleTimeout: 1800 (30 min) son los valores por defecto; están explícitos para que sepas dónde cambiarlos.

3. Recorrido por la consola

Abre http://localhost:8080/admin y entra con admin / admin.

Página de login de la consola de administración de Keycloak
La consola usa su propio login, el del realm master.

Verás un aviso amarillo: «You are logged in as a temporary admin user». Es el administrador que crearon las variables KC_BOOTSTRAP_ADMIN_*; en producción crearías un administrador permanente y borrarías este. Para el curso, ignóralo.

Estás en el realm master. Cambia a Tienda Go (el display name del realm tienda) desde Manage realms en el menú lateral, o desde el selector de la parte superior del menú.

El client tienda-web

En Clients aparecen el client que importamos y los que Keycloak crea en todo realm (account, account-console, admin-cli, broker, realm-management, security-admin-console):

Lista de clients del realm tienda con tienda-web resaltado
Clients → lista. tienda-web es el nuestro.

Entra en tienda-web. La pestaña Settings está dividida en secciones; las dos importantes son Capability config (qué puede hacer el client) y Access settings (sus URLs):

Sección Capability config de tienda-web
Capability config: client confidencial (Client authentication On), solo Standard flow y PKCE obligatorio con S256.
Sección Access settings de tienda-web
Access settings: las URLs exactas a las que Keycloak puede devolver al usuario tras el login y el logout, y los orígenes permitidos para CORS.

La pestaña Credentials guarda el secreto del client. En un proyecto real lo regenerarías (botón Regenerate) y se lo pasarías a la app por una variable de entorno, nunca en el código:

Pestaña Credentials de tienda-web con el client secret
Clients → tienda-web → Credentials.

Del JSON a la pantalla

JSONConsola (Clients → tienda-web → Settings)
"publicClient": falseCapability config → Client authentication: On
"standardFlowEnabled": trueCapability config → Authentication flow → Standard flow
"directAccessGrantsEnabled": falseCapability config → Direct access grants desmarcado (el flujo Password)
"pkce.code.challenge.method": "S256"Capability config → Require PKCE: On y PKCE Method: S256
"rootUrl", "baseUrl"Access settings → Root URL, Home URL
"redirectUris"Access settings → Valid redirect URIs
"post.logout.redirect.uris"Access settings → Valid post logout redirect URIs
"webOrigins"Access settings → Web origins (CORS)
"secret"Pestaña Credentials → Client Secret

Roles y usuarios

Realm roles lista los roles del realm: los nuestros (cliente, admin) y los que crea Keycloak.

Lista de roles de realm de tienda
Realm roles.

En Users → carlos → Role mapping ves qué roles tiene asignados directamente. Si desmarcas Hide inherited roles, aparecen también los que hereda de default-roles-tienda:

Pestaña Role mapping del usuario carlos
Users → carlos → Role mapping: cliente, admin y default-roles-tienda.

Tokens y claves del realm

Realm settings → Tokens controla cuánto viven los tokens. Fíjate en Access Token Lifespan (5 minutos) y en la recomendación de Keycloak: que sea menor que la inactividad de la sesión SSO (30 minutos).

Realm settings, pestaña Tokens, sección Access tokens
Realm settings → Tokens → Access tokens.

Realm settings → Keys muestra las claves del realm. La clave RS256 firma los tokens; su Kid es el que verás en el header de cada JWT y en el JWKS.

Realm settings, pestaña Keys, con la clave RS256 resaltada
Realm settings → Keys.

4. Tu primer login (sin escribir código)

Cada realm trae un client account-console: la página donde un usuario gestiona su cuenta. Es un client público que usa exactamente el flujo de la lección 1, así que sirve para verlo en directo.

  1. Abre una ventana privada y las DevTools (F12) en la pestaña Network con «Preserve log» activado.
  2. Visita http://localhost:8080/realms/tienda/account.
  3. En la petición a …/protocol/openid-connect/auth busca los parámetros client_id, redirect_uri, state, nonce, code_challenge y code_challenge_method=S256.
  4. Entra como ana / ana123. Después del login, busca la redirección con ?code=…&state=… (en este client va en el fragmento # de la URL) y la petición POST …/token con code_verifier.
Login del realm tienda
El login lo pinta Keycloak, con el nombre del realm.
Consola de cuenta de ana
Tras el login: la consola de cuenta de ana.
Lo que acabas de ver

Los pasos 3 a 10 del diagrama de la lección 1. Como account-console es una SPA (client público), el canje del código lo hace el propio navegador y no hay client_secret. En tienda-web lo hará tu servidor Go.

5. Primer programa Go: descubrimiento

Las librerías OIDC solo necesitan la URL del issuer: el resto lo leen del documento de descubrimiento. Vamos a hacer a mano lo que ellas harán por ti, para que no sea magia.

// Command discover consulta el documento de descubrimiento OIDC de un realm
// de Keycloak y muestra los endpoints y las claves públicas (JWKS).
//
// Uso:
//
//	go run ./cmd/discover
//	go run ./cmd/discover -issuer http://localhost:8080/realms/tienda
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

// discovery contiene los campos del documento
// /.well-known/openid-configuration que nos interesan en el curso.
type discovery struct {
	Issuer                        string   `json:"issuer"`
	AuthorizationEndpoint         string   `json:"authorization_endpoint"`
	TokenEndpoint                 string   `json:"token_endpoint"`
	UserinfoEndpoint              string   `json:"userinfo_endpoint"`
	EndSessionEndpoint            string   `json:"end_session_endpoint"`
	IntrospectionEndpoint         string   `json:"introspection_endpoint"`
	JWKSURI                       string   `json:"jwks_uri"`
	GrantTypesSupported           []string `json:"grant_types_supported"`
	CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
	IDTokenSigningAlgs            []string `json:"id_token_signing_alg_values_supported"`
}

// jwks es el conjunto de claves públicas con las que Keycloak firma los tokens.
type jwks struct {
	Keys []struct {
		Kid string `json:"kid"`
		Kty string `json:"kty"`
		Alg string `json:"alg"`
		Use string `json:"use"`
	} `json:"keys"`
}

func main() {
	issuer := flag.String("issuer", "http://localhost:8080/realms/tienda", "URL del realm (issuer)")
	flag.Parse()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	client := &http.Client{Timeout: 5 * time.Second}

	var doc discovery
	wellKnown := strings.TrimSuffix(*issuer, "/") + "/.well-known/openid-configuration"
	if err := getJSON(ctx, client, wellKnown, &doc); err != nil {
		log.Fatalf("descubrimiento: %v", err)
	}

	// OIDC exige que el issuer del documento sea idéntico al que usamos para pedirlo.
	// Si no coincide (por ejemplo, localhost frente a 127.0.0.1), las librerías
	// de validación rechazarán los tokens más adelante.
	if doc.Issuer != *issuer {
		fmt.Fprintf(os.Stderr, "AVISO: el issuer del documento (%s) no coincide con %s\n", doc.Issuer, *issuer)
	}

	fmt.Println("== Endpoints del realm ==")
	fmt.Printf("issuer:        %s\n", doc.Issuer)
	fmt.Printf("authorization: %s\n", doc.AuthorizationEndpoint)
	fmt.Printf("token:         %s\n", doc.TokenEndpoint)
	fmt.Printf("userinfo:      %s\n", doc.UserinfoEndpoint)
	fmt.Printf("end_session:   %s\n", doc.EndSessionEndpoint)
	fmt.Printf("introspection: %s\n", doc.IntrospectionEndpoint)
	fmt.Printf("jwks_uri:      %s\n", doc.JWKSURI)
	fmt.Printf("grant types:   %s\n", strings.Join(doc.GrantTypesSupported, ", "))
	fmt.Printf("PKCE:          %s\n", strings.Join(doc.CodeChallengeMethodsSupported, ", "))
	fmt.Printf("firmas ID tok: %s\n", strings.Join(doc.IDTokenSigningAlgs, ", "))

	var keys jwks
	if err := getJSON(ctx, client, doc.JWKSURI, &keys); err != nil {
		log.Fatalf("jwks: %v", err)
	}

	fmt.Println("\n== Claves públicas (JWKS) ==")
	for _, k := range keys.Keys {
		fmt.Printf("kid=%s  kty=%s  alg=%s  use=%s\n", k.Kid, k.Kty, k.Alg, k.Use)
	}
}

// getJSON hace un GET a url y decodifica la respuesta JSON en v.
func getJSON(ctx context.Context, client *http.Client, url string, v any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Accept", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("GET %s: estado %s", url, resp.Status)
	}
	return json.NewDecoder(resp.Body).Decode(v)
}

Ejecútalo desde tienda/pasos/paso-02:

cd tienda/pasos/paso-02
go run ./cmd/discover

Esta es la salida real contra Keycloak 26.8.0 (tus kid serán otros, porque cada realm genera sus propias claves):

== Endpoints del realm ==
issuer:        http://localhost:8080/realms/tienda
authorization: http://localhost:8080/realms/tienda/protocol/openid-connect/auth
token:         http://localhost:8080/realms/tienda/protocol/openid-connect/token
userinfo:      http://localhost:8080/realms/tienda/protocol/openid-connect/userinfo
end_session:   http://localhost:8080/realms/tienda/protocol/openid-connect/logout
introspection: http://localhost:8080/realms/tienda/protocol/openid-connect/token/introspect
jwks_uri:      http://localhost:8080/realms/tienda/protocol/openid-connect/certs
grant types:   authorization_code, client_credentials, implicit, password, refresh_token, urn:ietf:params:oauth:grant-type:device_code, urn:ietf:params:oauth:grant-type:jwt-bearer, urn:ietf:params:oauth:grant-type:token-exchange, urn:ietf:params:oauth:grant-type:uma-ticket, urn:openid:params:grant-type:ciba
PKCE:          plain, S256
firmas ID tok: PS384, RS384, EdDSA, ES384, HS256, HS512, ES256, RS256, HS384, ES512, PS256, PS512, RS512

== Claves públicas (JWKS) ==
kid=hZj8DLzfzhlzMB6w7asAJRwH6lLgW-aIOKUPeeuFo1s  kty=RSA  alg=RS256  use=sig
kid=36oKxvEgvZX2ON4uhApY6x3dulQpQgZJUa6Tim6H9CE  kty=RSA  alg=RSA-OAEP  use=enc

Observa dos cosas: el servidor anuncia flujos que tu client tiene desactivados (implicit, password); lo que manda es la configuración de cada client. Y hay una clave use=enc para cifrar, que no se usa para verificar firmas: un validador debe elegir la clave por kid, no «la primera».

6. Exportar e importar el realm

Cuando cambies algo en la consola, querrás llevarlo al JSON. Hay dos maneras:

Exportación parcial (consola)Exportación completa (CLI)
DóndeRealm settings → menú Action → Partial exportComando kc.sh export
UsuariosNoSí
SecretosEnmascarados con *Incluidos
ServidorEn marchaParado
UsoCopiar un client o un rol concreto al JSONCopias de seguridad, mover realms entre entornos

Exportación completa con Docker Compose (desde infra/):

mkdir -p export && chmod 777 export     # Linux y WSL: el contenedor escribe como el usuario 1000
docker compose stop keycloak
docker compose run --rm -v "$PWD/export:/tmp/export" keycloak \
  export --dir /tmp/export --realm tienda --users realm_file
docker compose start keycloak

ls export/    # tienda-realm.json, con usuarios incluidos
En Windows: PowerShell o Git Bash (probado)
mkdir export                       # sin chmod: Docker Desktop gestiona los permisos
docker compose stop keycloak
docker compose run --rm -v "${PWD}\export:/tmp/export" keycloak `
  export --dir /tmp/export --realm tienda --users realm_file
docker compose start keycloak

# En Git Bash, los comandos de bash valen, pero sin MSYS_NO_PATHCONV=1 Git Bash
# convierte /tmp/export en una ruta de Windows y la exportación falla:
#   MSYS_NO_PATHCONV=1 docker compose run --rm -v "$PWD/export:/tmp/export" keycloak \
#     export --dir /tmp/export --realm tienda --users realm_file

docker compose run crea un contenedor temporal con la misma configuración del servicio (base de datos incluida) pero sustituye el comando: en vez de start-dev ejecuta export. Con --users realm_file los usuarios van dentro del mismo archivo.

Para reimportar desde cero (por ejemplo, después de editar el JSON):

docker compose down -v    # -v borra el volumen de PostgreSQL: se pierde todo
docker compose up -d      # base de datos vacía → importa el realm de nuevo
La exportación completa es muy larga

Para este realm tan pequeño ocupa unas 2.200 líneas, porque incluye todos los valores por defecto (flujos de autenticación, client scopes, mappers…). Para el curso mantenemos un JSON mínimo escrito a mano e importamos solo lo que cambia. Úsala para copias de seguridad o para buscar el nombre exacto de un campo.

7. Comprobar que todo está en orden

Cada lección termina con el mismo gesto: ejecutar tools/comprobar, un programa Go del curso que revisa que Keycloak (y, desde la lección 3, tus programas) están como los deja esa lección. Hace un login real como ana, mira los tokens y llama a los servicios. Ejecútalo desde la carpeta course/:

go -C tools/comprobar run . -paso 2
# ✔ Keycloak responde y el realm tienda existe
# ✔ ana entra en tienda-web (Authorization Code + PKCE)
# ✔ ana tiene el rol de realm cliente; carlos, admin
#
# Todo en orden para el paso 2.

go -C carpeta ejecuta Go como si estuvieras en esa carpeta: así funciona igual en Windows, macOS y Linux. Si algo falla, el mensaje dice qué (por ejemplo, «login de ana rechazado») y conviene arreglarlo antes de seguir. Desde la lección 3 añade -servicios para que compruebe también los programas que tengas en marcha.

Ejercicios

1. Constrúyelo tú: el realm practica · fácil

Sin tocar tienda, crea desde la consola un realm practica que reproduzca lo esencial: rol cliente, usuario pepe con ese rol y contraseña no temporal, y un client confidencial practica-web con Standard flow, PKCE S256 y redirect http://localhost:3000/callback. Después entra como pepe en http://localhost:8080/realms/practica/account.

Ver solución
  1. Manage realms → Create realm: nombre practica → Create.
  2. Realm roles → Create role: cliente → Save.
  3. Users → Create new user: pepe → Create. En Credentials → Set password, desactiva Temporary. En Role mapping → Assign role, filtra por roles de realm y elige cliente.
  4. Clients → Create client: tipo OpenID Connect, Client ID practica-web → Next. Activa Client authentication, deja solo Standard flow, activa Require PKCE y elige S256 → Next. En Valid redirect URIs pon http://localhost:3000/callback → Save.

Al crearlo a mano, Keycloak le asigna a pepe default-roles-practica automáticamente; por eso en el JSON de tienda tuvimos que declararlo nosotros para los usuarios importados.

2. Añade una clienta por JSON · fácil

Añade al JSON a lucia (contraseña lucia123, rol cliente), reimporta el realm y comprueba que puede entrar en su consola de cuenta.

Ver solución

Añade este objeto al array "users":

{
  "username": "lucia",
  "enabled": true,
  "email": "lucia@tienda.test",
  "emailVerified": true,
  "firstName": "Lucía",
  "lastName": "Cliente",
  "credentials": [
    { "type": "password", "value": "lucia123", "temporary": false }
  ],
  "realmRoles": ["default-roles-tienda", "cliente"]
}

Y reimporta con docker compose down -v && docker compose up -d. Un simple restart no sirve: el realm ya existe y la importación se omite.

3. Más campos del descubrimiento · fácil

Amplía discover para mostrar también scopes_supported y token_endpoint_auth_methods_supported. ¿Qué métodos de autenticación de clients ofrece Keycloak?

Ver solución

Añade dos campos al struct y dos líneas de salida:

type discovery struct {
	// ... campos existentes ...
	ScopesSupported          []string `json:"scopes_supported"`
	TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"`
}

// en main(), tras las otras líneas:
fmt.Printf("scopes:        %s\n", strings.Join(doc.ScopesSupported, ", "))
fmt.Printf("auth clients:  %s\n", strings.Join(doc.TokenEndpointAuthMethods, ", "))

Verás, entre otros, client_secret_basic y client_secret_post (secreto compartido, lo que usará tienda-web), client_secret_jwt, private_key_jwt (el client firma un JWT con su clave privada; más seguro) y tls_client_auth (mTLS).

4. localhost frente a 127.0.0.1 · media

Ejecuta go run ./cmd/discover -issuer http://127.0.0.1:8080/realms/tienda. ¿Qué issuer devuelve Keycloak? Si tienda-web obtiene tokens usando localhost y api-pedidos se configura con 127.0.0.1, ¿qué pasará?

Ver solución

En start-dev sin --hostname, Keycloak construye el issuer a partir del host de cada petición, así que devuelve http://127.0.0.1:8080/realms/tienda y el programa no avisa. Pero los tokens emitidos a través de localhost llevan iss: http://localhost:8080/realms/tienda, y la API, configurada con 127.0.0.1, los rechazará con un error de issuer inválido aunque la firma sea correcta. Regla: usa en todas partes exactamente la misma URL de issuer; en producción fíjala con --hostname.

Errores comunes

docker: command not found / permission denied … docker.sock

Según tu sistema: Docker Desktop no está arrancado (Windows, macOS); en WSL falta activar Settings → Resources → WSL integration para tu distro; en Linux o WSL, tu usuario no está en el grupo docker o la terminal es anterior a añadirlo (sudo usermod -aG docker $USER y abre una sesión nueva). Detalles en Preparar tu equipo.

address already in use El puerto 8080 está ocupado

Otro proceso usa el 8080. Arreglo: cambia el mapeo a "127.0.0.1:8180:8080" y usa http://localhost:8180/realms/tienda como issuer en todo el curso (también en las URLs de redirección del client si cambia el puerto de la app).

import skipped Mis cambios en el JSON no aparecen

--import-realm no sobrescribe un realm que ya existe. Arreglo: docker compose down -v y up -d para empezar con la base de datos vacía, o aplica el cambio en la consola.

Invalid username or password No puedo entrar como admin

Dos causas típicas: estás en la página de login de tienda en vez de en la de /admin (el admin vive en master), o cambiaste KC_BOOTSTRAP_ADMIN_* después del primer arranque; esas variables solo actúan con la base de datos vacía. Arreglo: usa /admin, o down -v para regenerar.

HTTPS required Keycloak exige HTTPS

Con sslRequired: external, Keycloak acepta HTTP solo desde direcciones locales o privadas. Si entras por una IP pública o algunos proxies, lo rechaza. Arreglo: usa http://localhost:8080. No pongas sslRequired: none salvo para una prueba puntual.

connection refused discover falla al arrancar

Keycloak tarda en arrancar (sobre todo la primera vez). Arreglo: espera a que curl -s localhost:9000/health/ready devuelva UP.

AccessDeniedException La exportación no puede escribir

El contenedor corre con el usuario 1000 y la carpeta montada no le pertenece. Pasa en Linux y WSL. Arreglo: crea la carpeta antes y dale permisos (mkdir -p export && chmod 777 export).

404 /realms/tienda/… no existe

El realm no se importó: el JSON tiene un error o el volumen no está montado. Arreglo: busca en docker compose logs keycloak el mensaje de la importación y comprueba que lanzas docker compose desde la carpeta infra/ (el montaje ./realm es relativo).

Resumen y siguiente paso

Tienes Keycloak 26.8 en marcha con el realm tienda versionado en JSON, sabes moverte por la consola y has visto un login OIDC real en las DevTools. En el módulo 2 escribirás tienda-web: el mismo flujo, pero con tu propio servidor Go como client confidencial.