Cognito: autenticació sense haver-la de fer tu
Fer un sistema de login propi sembla fàcil fins que hi poses la llista sencera: guardar contrasenyes amb un hash decent, verificar el correu, recuperar la contrasenya, bloquejar intents massius, caducar sessions, MFA. Cada peça és un forat de seguretat possible.
Amazon Cognito ho fa per tu. Guarda els usuaris, gestiona el registre i l'inici de sessió, i reparteix tokens JWT que la teva API pot comprovar sense preguntar a ningú.
En aquesta lliçó traiem la capçalera x-usuari de la llista de la compra, aquella trampa que deixava qualsevol fer-se passar per qualsevol, i la substituïm per un usuari de debò.
Dos «pools» que no s'han de confondre
- User pool: el directori d'usuaris. Registre, inici de sessió, verificació de correu, MFA. Et dona tokens JWT. És el que necessites el 95 % de les vegades.
- Identity pool: canvia un token (d'un user pool, de Google, d'un usuari anònim) per credencials temporals d'AWS, perquè el navegador pugui parlar directament amb S3 o DynamoDB amb un rol d'IAM.
Si el teu frontend parla amb una API teva, només necessites el user pool. L'identity pool és per quan el navegador ha de pujar fitxers directament a S3 o fer servir un servei d'AWS sense passar per cap backend.
Els tres tokens
Quan un usuari inicia sessió, Cognito en retorna tres:
| Token | Per a què | Durada per defecte |
|---|---|---|
| ID token | Qui és l'usuari: sub, email, nom. Per pintar la interfície | 1 hora |
| Access token | Què pot fer. És el que envies a la teva API | 1 hora |
| Refresh token | Demanar tokens nous sense tornar a posar la contrasenya | 30 dies |
Un JWT són tres trossos en Base64 separats per punts: capçalera, dades i signatura. Està signat, no xifrat: qualsevol pot llegir-ne el contingut (prova-ho a jwt.io). El que ningú pot fer és canviar-lo sense que la signatura deixi de quadrar. Per això no s'hi posa mai res secret, i per això la teva API pot confiar en el que hi diu un cop ha verificat la signatura.
El camp important és sub: l'identificador únic i permanent de l'usuari dins del pool. El correu pot canviar; el sub, no. És el que farem servir com a clau de partició de la taula de la lliçó 04.
Crear el pool amb CDK
Afegim-ho a la pila de la lliçó 07:
// lib/llista-stack.ts (afegit)
import * as cognito from "aws-cdk-lib/aws-cognito";
import { HttpUserPoolAuthorizer } from "aws-cdk-lib/aws-apigatewayv2-authorizers";
const usuaris = new cognito.UserPool(this, "Usuaris", {
selfSignUpEnabled: true,
signInAliases: { email: true },
autoVerify: { email: true },
passwordPolicy: { minLength: 12 },
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: RemovalPolicy.DESTROY, // només per al curs
});
const client = usuaris.addClient("Web", {
authFlows: { userSrp: true },
generateSecret: false, // un navegador no pot guardar cap secret
});
const autoritzador = new HttpUserPoolAuthorizer("Usuaris", usuaris, {
userPoolClients: [client],
});
new CfnOutput(this, "UserPoolId", { value: usuaris.userPoolId });
new CfnOutput(this, "ClientId", { value: client.userPoolClientId });
I a l'API, l'autoritzador per defecte, perquè cap ruta quedi oberta per oblit:
const api = new HttpApi(this, "Http", {
defaultAuthorizer: autoritzador,
corsPreflight: { /* el de la lliçó 07 */ },
});
Tres decisions d'aquest codi:
signInAliases: { email: true }: l'usuari entra amb el correu. Les opcions d'inici de sessió no es poden canviar un cop creat el pool; si t'equivoques, toca pool nou i migrar usuaris.generateSecret: false: un client amb secret és per a backends. El codi d'un navegador el pot llegir tothom.defaultAuthorizer: protegeix totes les rutes, també elGET. A la versió de classe, l'autoritzador només era alPOST, elPUTi elDELETE, i elGET /productessense protecció retornava la llista de tothom. La ruta que «només llegeix» és justament la que filtra dades.
L'API: l'usuari surt del token
L'autoritzador de l'HTTP API comprova la signatura, la data de caducitat, l'emissor i el client abans que la petició arribi a la funció. Si alguna cosa falla, respon 401 i la teva Lambda ni s'executa (ni es paga).
A la funció, el sub arriba ja verificat:
// src/api.ts (canvia la primera part del handler)
export const handler = async (event: APIGatewayProxyEventV2WithJWTAuthorizer) => {
const usuari = event.requestContext.authorizer.jwt.claims.sub as string;
// ... la resta, igual que a la lliçó 07
};
El tipus APIGatewayProxyEventV2WithJWTAuthorizer és de @types/aws-lambda. La regla és absoluta: l'identificador de l'usuari surt del token, mai del cos de la petició ni d'una capçalera que pugui escriure el client. Com que el sub és la clau de partició, cada usuari només pot llegir, editar i esborrar els seus productes, sense cap if de permisos.
El frontend
La manera oficial de parlar amb Cognito des del navegador és la llibreria d'Amplify, que fa el protocol SRP (la contrasenya no viatja mai tal qual) i renova els tokens sola:
// src/auth.ts
import { Amplify } from "aws-amplify";
import { signUp, confirmSignUp, signIn, signOut, fetchAuthSession } from "aws-amplify/auth";
Amplify.configure({
Auth: { Cognito: { userPoolId: "eu-west-1_XXXXXXXXX", userPoolClientId: "XXXXXXXXXXXX" } },
});
export const registra = (email: string, password: string) =>
signUp({ username: email, password, options: { userAttributes: { email } } });
export const confirma = (email: string, codi: string) =>
confirmSignUp({ username: email, confirmationCode: codi });
export const entra = (email: string, password: string) => signIn({ username: email, password });
export const surt = () => signOut();
export async function api(ruta: string, opcions: RequestInit = {}) {
const { tokens } = await fetchAuthSession(); // renova el token si ha caducat
return fetch(`${import.meta.env.VITE_API}${ruta}`, {
...opcions,
headers: {
...opcions.headers,
Authorization: `Bearer ${tokens?.accessToken.toString()}`,
"content-type": "application/json",
},
});
}
Els identificadors del pool i del client no són secrets: van al codi del frontend sense problema. Els trobaràs a les sortides del cdk deploy.
Al curs presencial vam fer servir amazon-cognito-identity-js, la llibreria antiga. Funciona, però està en manteniment i l'has de connectar tu a la renovació de tokens. Per a projectes nous, Amplify.
On es guarden els tokens, i el veritable enemic
Per defecte, Amplify guarda els tokens al localStorage. Vol dir que qualsevol JavaScript que s'executi a la teva pàgina els pot llegir. El perill no és el localStorage en si, és el XSS: si algun lloc de la pàgina pinta text de l'usuari amb innerHTML, un producte que es digui <img src=x onerror="..."> executa codi a la sessió de qui el miri.
A la llista de la compra de classe es donaven les tres condicions alhora: noms de producte pintats amb innerHTML, tokens a localStorage i, a sobre, un console.log que imprimia els tres tokens a la consola. Com que el GET sense protecció mostrava els productes de tothom, un usuari podia robar la sessió de qualsevol altre amb un sol producte.
Tres regles:
- Pinta text amb
textContent, o amb React, que ja escapa per defecte.innerHTMLnomés amb HTML que hagis escrit tu. - Mai un
console.logd'un token. Ni «només per provar». - Durada curta per als access tokens (l'hora per defecte està bé) i revoca el refresh token en tancar sessió (
signOut({ global: true })).
Quan Cognito et fa nosa
Cognito és bo en el que fa, però té límits que convé saber abans:
- Els correus. Per defecte, Cognito envia com a molt 50 correus al dia des d'una adreça seva. Per a més, cal connectar-lo a SES amb un domini verificat.
- Les decisions irreversibles. Les opcions d'inici de sessió i els atributs obligatoris no es canvien després. Pensa-ho abans de tenir usuaris.
- Marxar-ne. Els hashes de les contrasenyes no es poden exportar. Si un dia canvies de proveïdor, els usuaris hauran de restablir la contrasenya o caldrà una migració gradual amb un activador de Lambda.
- La interfície. La pàgina d'inici de sessió gestionada (managed login, abans hosted UI) es pot personalitzar, però no és la teva web. Si el login ha de ser idèntic a la resta del disseny, el fas tu amb Amplify.
Si necessites organitzacions, rols complexos o login social amb deu proveïdors, mira també Auth0, Clerk o Supabase Auth. Per a una aplicació amb usuaris i la seva pròpia API a AWS, Cognito és la peça natural.
Què costa això
Des de finals de 2024, Cognito té tres plans: Lite, Essentials (el per defecte dels pools nous) i Plus (amb protecció contra amenaces).
| Concepte | Preu aproximat |
|---|---|
| Usuaris actius al mes (MAU), Lite i Essentials | 10.000 gratuïts cada mes |
| Essentials, a partir de l'usuari 10.001 | ~0,015 $ per MAU |
| Plus | ~0,02 $ per MAU, sense franquícia |
Preus de l'octubre de 2026, en dòlars. Abans del 2024 la franquícia era de 50.000 MAU; ho trobaràs a molts tutorials.
Un MAU és un usuari que fa alguna acció (iniciar sessió, renovar el token, registrar-se) durant el mes. Els usuaris registrats que no entren no compten. Per a un curs o una aplicació petita, pagaràs 0 $.
Errors habituals
- 401 i la funció sense cap log. L'autoritzador ha rebutjat el token abans d'arribar-hi. Comprova que envies l'access token i no l'ID token, que no ha caducat i que el pool i el client són els de l'autoritzador.
- El navegador diu CORS però el problema és el 401. Si la resposta d'error no porta capçaleres CORS, el navegador amaga el codi i et mostra un error de CORS. Prova la mateixa petició amb
curlper veure el codi real. I recorda posarauthorizationa les capçaleres permeses del preflight. UserNotConfirmedException. L'usuari s'ha registrat però no ha introduït el codi del correu. Mostra-li la pantalla de confirmació, no un error genèric.- El correu de confirmació no arriba. Mira la carpeta de correu brossa i el límit de 50 correus diaris del remitent per defecte. A classe, amb vint alumnes registrant-se alhora i esborrant usuaris per provar, s'hi arriba.
- Llegir l'usuari del cos de la petició. Si el codi fa
JSON.parse(event.body).usuari, qualsevol pot escriure el que vulgui. L'usuari surt derequestContext.authorizer, sempre.
Exercici
- Afegeix el user pool i l'autoritzador per defecte a la pila de la llista de la compra.
- Canvia la funció perquè l'usuari surti del
sub. Esborra tot rastre dex-usuari. - Fes un frontend mínim amb registre, confirmació, inici de sessió i la llista. Pinta els noms amb
textContent. - Registra dos usuaris en dos navegadors. Comprova que cadascú només veu els seus productes, i que un
curlsense token aGET /productesrespon 401. - Prova d'afegir un producte que es digui
<img src=x onerror=alert(1)>i comprova que es mostra com a text.
Repàs: quatre preguntes
- Un JWT està xifrat? No, està signat. Qualsevol el pot llegir; ningú el pot modificar sense trencar la signatura.
- Quin token envies a la teva API? L'access token.
- D'on surt l'identificador de l'usuari a la Lambda? De
requestContext.authorizer.jwt.claims.sub, que l'autoritzador ja ha verificat. - Per a què serveix un identity pool? Per canviar un token per credencials temporals d'AWS, quan el navegador ha de parlar directament amb serveis d'AWS.
La IA a l'empresa, amb criteri
Cada setmana, una sola cosa que pots fer servir: la notícia que toca la teva empresa i què en faria jo, una pregunta de client, un cas real o un exercici de quinze minuts. Un correu per setmana com a molt, i te'n pots donar de baixa amb un clic.