Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado integrar el cliente ya construido en servidor. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
Ya has definido quién debería acceder a cada operación. Spring Security aplicará controles de acceso antes del controlador. Un hash de contraseña permite comprobarla sin guardar su texto; BCrypt incorpora sal y un coste de cálculo. Hoy conectarás esas piezas con reglas de acceso explícitas.
Cifrado reversible frente a Hash unidireccional
Conviene fijar con máxima claridad conceptual estos dos términos:
- Texto en claro ("secreto") → Función Hash irreversible → Hash ("$2a$12$...")
- Hash ("$2a$12$...") → Imposible recuperar matemáticamente → "secreto"
- Cifrado (Criptografía reversible): Tiene un camino de ida y vuelta. Si tienes el texto cifrado y la clave secreta, puedes descifrarlo para obtener el texto original. Se usa para transmitir mensajes secretos o guardar números de tarjeta de crédito que luego necesitas cobrar.
- Hash (Función unidireccional): Solo tiene camino de ida. A partir de una entrada genera una huella digital matemática de longitud fija. Es matemáticamente imposible revertir el hash para obtener la contraseña original.
¿Cómo se comprueba entonces el login si el servidor no conoce la contraseña?
- El usuario introduce
"MiPassword123". - El servidor le aplica la misma función hash a lo que el usuario acaba de escribir.
- Si el hash resultante coincide con el hash guardado en la base de datos, el usuario conoce la contraseña. El servidor jamás necesita saber cuál era la contraseña original.
Por qué SHA-256 no sirve para contraseñas: La necesidad de funciones lentas
Muchos estudiantes preguntan: «¿Por qué no usamos SHA-256 si es un hash seguro?».
SHA-256 es un algoritmo excelente para verificar la integridad de un archivo de 4 GB, porque fue diseñado para ser ultrarrápido.
- Una tarjeta gráfica (GPU) moderna para videojuegos puede calcular más de 10.000 millones de hashes SHA-256 por segundo.
- Si un atacante roba tu base de datos con contraseñas en SHA-256, puede probar todas las combinaciones posibles de 8 caracteres en cuestión de minutos (fuerza bruta).
Para almacenar contraseñas necesitamos funciones de derivación de claves deliberadamente lentas y costosas en CPU y memoria: BCrypt, Argon2 o PBKDF2.
- Si el cálculo de un hash BCrypt tarda 250 milisegundos, un usuario legítimo al hacer login ni siquiera nota el retardo de un cuarto de segundo.
- Para el atacante, probar 1.000 millones de contraseñas ya no tarda minutos: tarda miles de años.
El rol del Salt (Sal criptográfica) y la anatomía de BCrypt
Para evitar que dos usuarios con la misma contraseña tengan el mismo hash en la base de datos (lo que permitiría usar tablas arcoíris precalculadas), se añade un Salt: un conjunto de bytes aleatorios únicos generados para cada usuario.
BCrypt empaqueta todo en una cadena modular estándar de 60 caracteres:
- Algoritmo ($2a$)
- Coste ($12$)
- Salt aleatorio (22 chars)
- Hash de la contraseña (31 chars)
$2a$12$KIXQ0zv4pO3mR7uYbA1c.eW9tHnL5sD2fG8jV4xZ6qC1aB3dE5gHi
| | \____________________/\_____________________________/
Id Cost Salt Hash
Ese ejemplo sirve para ver la estructura, no para copiarlo: cada hash lleva su propia sal, así que el tuyo será distinto aunque la contraseña sea la misma. En la sesión 37 generarás los tuyos.
$2a$: Versión del algoritmo BCrypt.$12$: Factor de coste (Work Factor). Significa 2¹² = 4.096 rondas de estiramiento de clave (Key Stretching).- Primeros 22 caracteres: El Salt aleatorio generado automáticamente en el momento del registro.
- Últimos 31 caracteres: El hash resultante de combinar la contraseña con ese Salt.
El cerrojo automático de Spring Security
En el instante en que añades esta dependencia a tu pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Spring Boot activa el principio de seguridad por defecto (Secure by Default):
- Todos los endpoints quedan blindados: Cualquier petición a
/api/v1/proyectoso a/swagger-ui.htmles inmediatamente rechazada con código401 Unauthorized. - Genera un usuario provisional: En la terminal de arranque de la aplicación aparece un mensaje como este:
El usuario por defecto esUsing generated security password: 4a8b1c2d-9e3f-4123-b890-abcdef123456usery la contraseña es esa clave aleatoria efímera. - Inserta la Cadena de Filtros de Seguridad: Cada petición HTTP entrante es interceptada por una serie de filtros en cascada antes de llegar siquiera al
DispatcherServlet.
La arquitectura de filtros
Spring Security no vive dentro de tus controladores; vive en la frontera de red.
El DelegatingFilterProxy desvía la petición a la SecurityFilterChain. Si un filtro detecta que la petición no aporta credenciales válidas, corta la ejecución de inmediato y responde al cliente sin que tu código de negocio llegue a enterarse.
La cadena de filtros (SecurityFilterChain) en Spring Security 6
En versiones antiguas de Spring Security se heredaba de WebSecurityConfigurerAdapter. En Spring Boot 3 esa clase fue eliminada definitivamente.
La configuración moderna se realiza mediante un @Bean que construye un SecurityFilterChain utilizando programación funcional basada en lambdas:
- Petición HTTP entrante
- Filtro CORS (WebConfig)
- Filtro CSRF / ExceptionTranslation
- BasicAuthenticationFilter
- AuthorizationFilter (requestMatchers)
- DispatcherServlet / Controller
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Paso 1 · Retomar el proyecto y preparar la comprobación
- Abre la matriz de permisos,
pom.xmly la configuración actual. Guarda una petición pública y otra que deberá quedar protegida. - Localiza el modelo de usuario o prepara el indicado en el ejercicio, diferenciándolo de una entidad de negocio que solo represente a una persona.
- Prepara contraseñas ficticias de prueba y anota las rutas públicas de registro o acceso previstas. No utilices contraseñas personales en ejemplos o commits.
Paso 2 · Comprobar por qué no se guardan contraseñas en texto plano
Una filtración de una tabla con contraseñas en texto plano permitiría leerlas directamente. Guardar un hash evita esa lectura, pero un atacante todavía puede probar contraseñas candidatas y comparar resultados. BCrypt incorpora una sal aleatoria y un coste de cálculo para dificultar esas pruebas.
- Analiza la seguridad del acceso y dibuja dos recorridos: registro (contraseña → hash → base de datos) y acceso (contraseña recibida + hash guardado → comparación).
- Marca qué dato se guarda y qué dato se descarta. El hash también es sensible y no se incluye en las respuestas públicas ni en logs.
- En el siguiente paso ejecutarás una prueba con contraseñas ficticias: dos hashes diferentes pueden verificar la misma contraseña. Esa observación explica por qué no se compara
encode(entrada).equals(hashGuardado).
Al terminar debes poder explicar qué aporta la sal, qué aporta el coste y por qué sigue siendo necesaria una contraseña difícil de adivinar.
Paso 3 · Integrar BCrypt con Spring Security
Antes de copiar estas clases añade una sola vez spring-boot-starter-security a pom.xml y sincroniza Maven: de ahí proceden PasswordEncoder y BCryptPasswordEncoder. Crea config/SecurityBeansConfig.java y después el test bajo src/test/java. El test instancia el encoder directamente y no necesita arrancar el backend; ejecútalo antes de configurar HTTP. Al reiniciar la aplicación después de añadir seguridad cambiará el acceso a sus rutas: observarás ese comportamiento en el paso 6.
package com.ejemplo.gestor.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityBeansConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// Factor de coste 12: equilibrio óptimo entre seguridad y tiempo de respuesta (~200ms)
return new BCryptPasswordEncoder(12);
}
}
Creamos una prueba unitaria para experimentar cómo opera PasswordEncoder:
package com.ejemplo.gestor;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.*;
class PasswordEncoderTest {
private final PasswordEncoder encoder = new BCryptPasswordEncoder(10);
@Test
void bcryptGeneraHashesDistintosParaMismaPassword() {
String passwordPlana = "MiSuperClave2026!";
// Generamos dos hashes de la misma contraseña exacta
String hash1 = encoder.encode(passwordPlana);
String hash2 = encoder.encode(passwordPlana);
System.out.println("Hash 1: " + hash1);
System.out.println("Hash 2: " + hash2);
// 1. Demostración de Salt: los hashes son totalmente distintos en cada llamada
assertNotEquals(hash1, hash2);
// 2. Verificación de login con matches(): ambos validan con éxito
assertTrue(encoder.matches(passwordPlana, hash1));
assertTrue(encoder.matches(passwordPlana, hash2));
// 3. Contraseña incorrecta rechazada
assertFalse(encoder.matches("ClaveErronea!", hash1));
}
}
Paso 4 · Medir el coste computacional del Work Factor
Ejecuta este benchmark en terminal para entender cómo cada incremento en el factor de coste duplica el tiempo de cálculo de la CPU:
@Test
void medirTiemposSegunFactorDeCoste() {
String password = "PruebaRendimiento!";
for (int coste = 10; coste <= 14; coste++) {
PasswordEncoder enc = new BCryptPasswordEncoder(coste);
long inicio = System.currentTimeMillis();
enc.encode(password);
long fin = System.currentTimeMillis();
System.out.printf("Coste %d (2^%d rondas) -> Tiempo: %d ms%n", coste, coste, (fin - inicio));
}
}
Observa los resultados típicos en una CPU moderna:
- Coste 10 (2¹⁰ = 1.024 iteraciones): ~60 ms.
- Coste 11 (2¹¹ = 2.048 iteraciones): ~120 ms.
- Coste 12 (2¹² = 4.096 iteraciones): ~240 ms. (Recomendado en servidores modernos).
- Coste 13 (2¹³ = 8.192 iteraciones): ~490 ms.
- Coste 14 (2¹⁴ = 16.384 iteraciones): ~980 ms.
Cada incremento duplica exactamente el coste para el atacante. El coste 12 ofrece una resistencia excepcional sin degradar la experiencia de usuario.
Paso 5 · Servicio de registro de usuario con hash seguro
Reutiliza UsuarioRepository y crea o amplía UsuarioService, recibiendo el repositorio y PasswordEncoder por constructor. El DTO público de registro contiene username y contraseña, no un rol que el visitante pueda elegir. Valida la entrada, rechaza username duplicado, codifica la contraseña y asigna desde el servidor el rol inicial menos privilegiado. Devuelve un DTO sin contraseña ni hash. Comprueba la fila guardada con una cuenta ficticia y reserva la asignación de roles elevados para una operación administrativa protegida.
- Inyecta
PasswordEncoderenUsuarioService. - Al recibir
RegistroUsuarioRequest(username, password):- Valida que la contraseña tenga al menos 8 caracteres y complejidad mínima.
- Codifica la contraseña antes de asignarla a la entidad:
usuario.setPassword(passwordEncoder.encode(request.password())); - Asigna
usuario.setRol(Rol.ROLE_DESARROLLADOR)desde el servidor y guarda el usuario medianteUsuarioRepository.
- Abre pgAdmin o tu cliente de PostgreSQL y haz un
SELECT * FROM usuarios;. - Comprueba visualmente que la columna
passwordalmacena una cadena que empieza por$2a$12$...y jamás la clave en texto claro.
Spring Security básico
Paso 6 · Configurar SecurityConfig con rutas públicas y privadas
La dependencia ya se añadió en el paso 3: comprueba su presencia sin duplicarla. Observa primero la respuesta sin SecurityConfig y después crea esa clase en config con una sola cadena. Conserva el bean PasswordEncoder en SecurityBeansConfig; no declares otro con el mismo nombre. Para las pruebas de esta sesión utiliza la identidad temporal indicada y verifica rutas públicas y protegidas por separado. En la sesión 37 la fuente de identidades pasará a PostgreSQL.
- Abre tu
pom.xmly añade elstarterde seguridad dentro de<dependencies>, junto a los que ya tienes:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> - Recarga las dependencias de Maven en tu IDE (o ejecuta
./mvnw clean compileen la terminal). - Arranca la aplicación con
./mvnw spring-boot:runy no toques nada más. - Busca en la terminal, entre los mensajes de arranque, una línea como esta:
Using generated security password: 4a8b1c2d-9e3f-4123-b890-abcdef123456 This generated password is for development use only. Your security configuration must be updated before running your application in production. - Lanza
GET http://localhost:8080/api/v1/proyectossin ninguna credencial.
- Qué acaba de pasar
- No has escrito ni una línea de código y tu API entera ha dejado de responder con
401. Eso es Secure by Default: Spring Security prefiere que una aplicación nazca cerrada y que seas tú quien abra puertas conscientemente, antes que nacer abierta y depender de que te acuerdes de cerrarlas. - Por qué esa contraseña no sirve
- Cambia en cada arranque, es la misma para todo el mundo y el usuario se llama
user. Sirve para comprobar que el cerrojo está puesto y para nada más. Sustituirla es justo el trabajo del paso 3.
Crea el archivo src/main/java/com/ejemplo/gestor/config/SecurityConfig.java. Es una clase de configuración normal: no hereda de nada y no implementa ninguna interfaz; solo publica un @Bean.
package com.ejemplo.gestor.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. Desactivamos CSRF solo porque estamos construyendo una API REST sin cookies de sesión clásicas
.csrf(csrf -> csrf.disable())
// 2. Definimos las reglas de autorización sobre las rutas HTTP
.authorizeHttpRequests(auth -> auth
// Lo único público es la documentación: sin ella, quien todavía
// no sabe cómo autenticarse no tiene por dónde empezar
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
// Todo lo demás exige credenciales, tal y como declara la
// matriz de control de acceso de la sesión 35
.anyRequest().authenticated()
)
// 3. Habilitamos autenticación HTTP Basic estándar para pruebas de API
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
@EnableWebSecurity- Le dice a Spring que tú te haces cargo de la configuración de seguridad. En cuanto publicas tu propio
SecurityFilterChain, la configuración automática se retira: ya no hay usuariouserni contraseña generada en la terminal. httpy el punto al final de cada líneaHttpSecurityaplica un patrón de construcción encadenada: cada método devuelve el mismo objeto, de modo que las llamadas se leen en cascada. El orden entre bloques (csrf,authorizeHttpRequests,httpBasic) es indiferente; el orden dentro deauthorizeHttpRequestses crítico.- Por qué
anyRequest()va siempre la última - Las reglas se evalúan de arriba abajo y gana la primera que encaje. Si pusieras
anyRequest().authenticated()arriba, engulliría todas las peticiones y las reglas de debajo no se aplicarían nunca. Spring, de hecho, se niega a arrancar si detecta una regla inalcanzable. /**frente a/*/swagger-ui/*encaja con/swagger-ui/index.htmlpero no con/swagger-ui/css/tema.css./**atraviesa cuantos niveles haga falta, y por eso es lo que se usa para árboles de recursos.
Con tu SecurityFilterChain publicado ya no hay contraseña generada en la terminal, así que necesitas unas credenciales propias. Para las primeras pruebas, antes de conectar la base de datos en el trabajo siguiente, valen unas estáticas en el archivo de propiedades:
# Usuario provisional para pruebas iniciales de Spring Security
spring.security.user.name=desarrollador
spring.security.user.password=Password123!
spring.security.user.roles=DESARROLLADOR
Esto es un andamio, y se cae en la sesión 37
Un usuario en un archivo de propiedades no tiene roles múltiples, no se puede dar de alta desde la API, no se puede desactivar y su contraseña viaja en texto plano dentro del repositorio. Está aquí por una única razón: para que puedas probar la cadena de filtros hoy sin arrastrar todavía la base de datos. En la sesión 37 estas tres líneas se borran.
Paso 7 · Pruebas de autorización con Bruno
Arranca tu aplicación y ejecuta estas comprobaciones desde tu cliente HTTP (Bruno o Postman):
- Ruta pública sin credenciales:
- Abre en el navegador
http://localhost:8080/swagger-ui.html. - Resultado esperado: Código
200 OK. La documentación carga sin pedir usuario, que es justo lo que necesita quien todavía no sabe cómo autenticarse.
- Abre en el navegador
- Lectura sin credenciales:
- Lanza
GET http://localhost:8080/api/v1/proyectos. - Resultado esperado: Código
401 Unauthorized, que es la filaANONde la matriz de la sesión 35. Esta API no publica nada: hasta para leer hay que identificarse. - Revisa la pestaña Headers: el servidor ha devuelto la cabecera
WWW-Authenticate: Basic realm="Realm".
- Lanza
- Escritura con credenciales válidas:
- Abre la pestaña Auth de la petición (existe igual en Bruno y en Postman) y selecciona Basic Auth.
- Introduce Usuario:
desarrolladory Contraseña:Password123!. - Lanza de nuevo el
POST. - Resultado esperado: Código
201 Createdcon la cabeceraLocation. - Inspecciona en Headers enviados cómo viaja:
Authorization: Basic ZGVzYXJyb2xsYWRvcjpQYXNzd29yZDEyMyE=(cadena codificada en Base64).
Paso 8 · Si algo no sale como dice el guion
Los cuatro tropiezos de esta sesión, en orden de frecuencia:
| Síntoma | Causa casi segura | Qué mirar |
|---|---|---|
Sigue apareciendo Using generated security password al arrancar |
Sigue activa la identidad automática de desarrollo | Comprueba las propiedades de usuario y, desde la sesión 37, que se detecta tu UserDetailsService dentro de donde vive GestorApplication |
GET /swagger-ui.html devuelve 401 |
La ruta real no es la que has escrito en requestMatchers |
Mira en la terminal a qué ruta redirige Springdoc; suele hacer falta /swagger-ui/** y /v3/api-docs/** |
Con usuario y contraseña correctos sigues recibiendo 401 |
El cliente no está enviando la cabecera | Comprueba en la pestaña de cabeceras enviadas que aparece Authorization: Basic …; si no está, la pestaña Auth no se aplicó a esa petición |
La aplicación no arranca: Cannot configure an AuthenticationProvider o una regla inalcanzable |
Una regla más general tapa a otra más concreta | Reordena: anyRequest() siempre al final |
Paso 9 · Contrastar la configuración con la matriz de permisos
Todavía no puedes distinguir un DESARROLLADOR de un ADMINISTRADOR —eso llega en la sesión 38—, pero la primera columna de la matriz ya la puedes auditar entera:
- Recorre la tabla de la sesión 35 y comprueba, sin credenciales, que las nueve filas de la columna
ANONresponden401. - Añade a
SecurityConfigla única excepción que esta API sí quiere publicar: la ruta de login que construirás en la sesión 39,POST /api/v1/auth/**. Déjala escrita conpermitAll()aunque el endpoint todavía no exista. - Repite
GET /api/v1/proyectoscon las credenciales deapplication.propertiesy comprueba que ahora pasa: la ruta no ha cambiado, lo que ha cambiado es quién la pide. - Guarda las tres peticiones —documentación pública, lectura anónima rechazada, lectura autenticada— como una carpeta
09-seguridaddentro de la colección que arrastras desde la UD2. A partir de aquí cada sesión de esta unidad añade peticiones a esa carpeta, y en la sesión 40 tendrás que poder ejecutarla entera de una pasada. - Documenta en tu cuaderno, en tres líneas, qué ruta has abierto y por qué esa y no otra. Es la primera decisión de seguridad que tomas tú y es la que tendrás que defender.
- Cómo saber que lo has terminado
- Las nueve filas de la columna
ANONresponden401;/swagger-ui.htmlresponde200sin credenciales; la misma petición que fallaba pasa a200solo añadiendo Basic Auth; y la terminal de arranque ya no imprime ninguna contraseña generada.
Paso 10 · Comprobar y registrar el resultado del proyecto
- Comprueba con PasswordEncoder que una contraseña correcta coincide con su hash y otra no. Verifica que nunca se devuelve el hash en el DTO público.
- Prueba ruta pública, ruta protegida sin identidad y acceso permitido. Contrasta el resultado con la matriz y no resuelvas un rechazo abriendo todas las rutas.
Ampliación si has completado el trabajo
Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.
Reto · Migración transparente de factores de coste (upgradeEncoding)
Con el paso de los años las computadoras se vuelven más rápidas y los factores de coste antiguos (ej: coste 10 de hace 5 años) quedan desfasados.
Spring Security proporciona el método:
passwordEncoder.upgradeEncoding(hashActual)
- ¿Cómo permite este método detectar si un hash guardado en base de datos se generó con un factor de coste inferior al estándar actual de la empresa?
- Diseña el flujo durante el login: si el login tiene éxito y
upgradeEncodingdevuelvetrue, ¿cómo actualiza la aplicación el hash en la base de datos con el nuevo coste sin pedirle al usuario que vuelva a escribir su contraseña?
Formato de entrega
Incluye esta explicación en el registro de la sesión dentro del repositorio de GitHub, junto al código y las comprobaciones. La entrega es el enlace al repositorio y al commit de la sesión.
BCryptPasswordEncoder configurado.matches() en verde.upgradeEncoding) diseñado.Ver respuestas
1 · Porque si la clave de cifrado se ve comprometida o filtrada, todas las contraseñas de todos los usuarios de la plataforma quedan expuestas de forma inmediata.
2 · Garantiza que dos contraseñas idénticas generen hashes completamente diferentes, neutralizando los ataques basados en diccionarios y tablas arcoíris precalculadas.
3 · Porque SHA-256 fue diseñado para ser extremadamente rápido (vulnerable a fuerza bruta con GPUs), mientras que BCrypt es deliberadamente lento y configurable en coste de CPU.
4 · Porque los primeros 22 caracteres del propio hash guardado en base de datos contienen el Salt codificado, extrayéndolo de forma transparente para computar la comparación.
Reto · Manejo personalizado de respuestas 401 (RFC 7807)
Por defecto, cuando Spring Security rechaza una petición con 401, emite una respuesta vacía o el error básico de Tomcat.
Investiga la interfaz AuthenticationEntryPoint:
- Crea una clase
CustomAuthenticationEntryPointque implementeAuthenticationEntryPoint. - Sobrescribe
commence()para que ante accesos no autenticados, el servidor devuelva un JSON estructurado con el estándar RFC 7807 (Problem Details):{"status": 401, "title": "No autenticado", "detail": "Debes aportar credenciales válidas para acceder a este recurso"}. - Regístralo en tu
filterChaincon.exceptionHandling(ex -> ex.authenticationEntryPoint(...)).
SecurityConfig con filterChain y rutas públicas/privadas operativas.AuthenticationEntryPoint) emitiendo respuestas RFC 7807 ante rechazos 401.Ver respuestas
1 · Por el principio de seguridad por defecto: previene que endpoints sensibles queden expuestos accidentalmente por olvido del desarrollador.
2 · El método permitAll() encadenado a una regla de ruta con requestMatchers().
3 · Porque la cabecera Authorization solo codifica el usuario y la contraseña en Base64; Base64 no es cifrado (cualquiera puede decodificarlo al instante en texto claro).
4 · La cabecera WWW-Authenticate (ej: WWW-Authenticate: Basic realm="Realm").
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión:
No se almacenan ni devuelven contraseñas en claro y los rechazos se distinguen de los errores de negocio.
Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.