Un sistema que controla las luces de una casa no debería tener registro público: las cuentas las crea alguien de confianza. En Amazon Cognito, ese modelo pasa por usuarios creados por un administrador con contraseña temporal y el estado FORCE_CHANGE_PASSWORD.
En esta parte implementamos el login en React con el SDK de AWS, sin Amplify: crear usuarios por invitación, el primer inicio de sesión, la recuperación de contraseña y los detalles de Cognito que más confunden.
Las piezas de Cognito
- User Pool: el directorio de usuarios. Gestiona contraseñas, verificación de correo y emite los tokens (ID, acceso y refresh).
- App client: la configuración con la que tu app habla con el User Pool (flujos permitidos, atributos que puede leer).
- Identity Pool: cambia un token del User Pool por credenciales temporales de AWS. Solo la necesitas si la app llama directamente a servicios de AWS (parte 10).
El login, sin Amplify
Con el SDK v3 y el flujo USER_PASSWORD_AUTH:
import {
CognitoIdentityProviderClient,
InitiateAuthCommand,
} from "@aws-sdk/client-cognito-identity-provider";
const client = new CognitoIdentityProviderClient({ region: REGION });
export async function iniciarSesion(usuario: string, password: string) {
const r = await client.send(
new InitiateAuthCommand({
AuthFlow: "USER_PASSWORD_AUTH",
ClientId: APP_CLIENT_ID,
AuthParameters: { USERNAME: usuario, PASSWORD: password },
})
);
if (r.ChallengeName === "NEW_PASSWORD_REQUIRED") {
// Primer login con contraseña temporal: ver más abajo
throw new NuevaPasswordRequerida(usuario, r.Session!);
}
return r.AuthenticationResult; // IdToken, AccessToken, RefreshToken
}
Para que funcione, el app client debe tener habilitado ALLOW_USER_PASSWORD_AUTH y no tener un client secret (una app web no puede guardarlo). Como no se usa la interfaz alojada de Cognito, no hay redirecciones: la app funciona igual en localhost que en producción.
Para desactivar el registro público, activa en el User Pool la opción de que solo los administradores creen usuarios (AllowAdminCreateUserOnly). Aunque alguien llame a la API de registro directamente, Cognito la rechaza.
Crear usuarios por invitación
Desde una Lambda de administración, o desde la CLI:
import { AdminCreateUserCommand } from "@aws-sdk/client-cognito-identity-provider";
await cognito.send(
new AdminCreateUserCommand({
UserPoolId: USER_POOL_ID,
Username: email,
TemporaryPassword: passwordTemporal, // si lo omites, Cognito genera una
UserAttributes: [
{ Name: "email", Value: email },
{ Name: "email_verified", Value: "true" },
{ Name: "custom:casaId", Value: "casa-demo" },
],
})
);
Por defecto, Cognito envía un correo de invitación con la contraseña temporal. Si prefieres enviarlo tú (con tu propio diseño, por SES), usa MessageAction: "SUPPRESS".
El usuario queda en estado FORCE_CHANGE_PASSWORD: puede entrar con la contraseña temporal, pero Cognito le exigirá elegir una propia. La contraseña temporal vence (por defecto a los 7 días; se configura en el User Pool). Para reenviar la invitación, vuelve a llamar a AdminCreateUser con MessageAction: "RESEND".
El primer inicio de sesión: NEW_PASSWORD_REQUIRED
Cuando InitiateAuth devuelve ese reto, la app pide una contraseña nueva y responde con el token de sesión del reto:
import { RespondToAuthChallengeCommand } from "@aws-sdk/client-cognito-identity-provider";
export async function completarNuevaPassword(usuario: string, sesion: string, nueva: string) {
const r = await client.send(
new RespondToAuthChallengeCommand({
ClientId: APP_CLIENT_ID,
ChallengeName: "NEW_PASSWORD_REQUIRED",
Session: sesion,
ChallengeResponses: { USERNAME: usuario, NEW_PASSWORD: nueva },
})
);
return r.AuthenticationResult;
}
En la interfaz, el componente de login se resuelve bien como una pequeña máquina de estados: login, nuevaPassword, olvide (pedir el código) y restablecer (código y contraseña nueva).
Recuperar la contraseña
import { ForgotPasswordCommand, ConfirmForgotPasswordCommand } from "@aws-sdk/client-cognito-identity-provider";
await client.send(new ForgotPasswordCommand({ ClientId: APP_CLIENT_ID, Username: usuario }));
// El usuario recibe un código por correo y lo escribe en la app
await client.send(
new ConfirmForgotPasswordCommand({ ClientId: APP_CLIENT_ID, Username: usuario, ConfirmationCode: codigo, Password: nueva })
);
Dos condiciones para que funcione: el usuario necesita un correo o teléfono verificado, y su cuenta debe estar confirmada. Un usuario que sigue en FORCE_CHANGE_PASSWORD (nunca entró) no puede recuperar la contraseña con este flujo: tiene que entrar con la temporal, o el administrador reenvía la invitación o le asigna una nueva con AdminSetUserPassword. Conviene explicarlo en la pantalla de "Olvidé mi contraseña".
Detalles de Cognito que confunden
Los atributos personalizados no aparecen en el token si el app client no puede leerlos. Además de crear custom:casaId en el User Pool, hay que añadirlo a los atributos de lectura del app client.
Actualizar el app client por la CLI reemplaza toda su configuración. update-user-pool-client no es parcial: si no repites un valor (por ejemplo, los flujos de autenticación), vuelve al valor por defecto.
Un atributo con texto vacío se elimina. AdminUpdateUserAttributes con "" no guarda un texto vacío: borra el atributo. Trata "ausente" y "vacío" igual en tu código.
Cerrar la sesión de otro no es instantáneo. AdminUserGlobalSignOut invalida los refresh tokens, pero el ID token y el de acceso ya emitidos siguen siendo válidos hasta que vencen (por defecto, una hora). Si quitas permisos a alguien, quítaselos también en tu backend.
El correo de Cognito tiene límites bajos. El envío integrado de Cognito tiene una cuota diaria pequeña; para producción, configura SES. SES empieza en sandbox y solo entrega a direcciones verificadas hasta que pides salir.
Usar el token en el backend
El ID token es un JWT con los atributos del usuario (email, custom:casaId...). Tus Lambdas deben verificarlo antes de confiar en él, por ejemplo con la librería aws-jwt-verify:
import { CognitoJwtVerifier } from "aws-jwt-verify";
const verificador = CognitoJwtVerifier.create({ userPoolId: USER_POOL_ID, tokenUse: "id", clientId: APP_CLIENT_ID });
const datos = await verificador.verify(idToken); // lanza un error si es inválido o venció
const casaId = datos["custom:casaId"];
Saca del token verificado lo que decide los permisos (a qué casa pertenece el usuario), nunca del cuerpo de la petición.
Preguntas frecuentes
¿Qué es el estado FORCE_CHANGE_PASSWORD?
Es el estado de un usuario creado por un administrador con contraseña temporal. Puede iniciar sesión, pero Cognito responde con el reto NEW_PASSWORD_REQUIRED hasta que elija una contraseña propia. Después pasa a CONFIRMED.
¿Cómo paso un usuario a CONFIRMED sin que inicie sesión?
Con AdminSetUserPassword y Permanent: true. Úsalo con cuidado: el usuario se queda con una contraseña que alguien más conoce.
¿USER_PASSWORD_AUTH es seguro?
Envía la contraseña a Cognito dentro de una conexión HTTPS. El flujo USER_SRP_AUTH evita enviarla, con un protocolo de prueba de conocimiento; es lo que usa Amplify por defecto. Para un proyecto nuevo, considera SRP o el inicio de sesión sin contraseña que ofrece Cognito.
¿Cuánto cuesta Cognito?
Cobra por usuario activo al mes, no por sesión ni por token, con una capa gratuita amplia. Para un proyecto doméstico es gratis. Lo comparo con otras opciones en Cognito vs Firebase Auth vs Auth0.