Pireon OAuth clients allow a single grant type. With one PIREON_CLIENT_ID of type password, email + password worked but "Sign in with Pireon" failed with `unauthorized_client`. - PIREON_CLIENT_ID/SECRET: authorization_code client (Sign in with Pireon, Swagger UI). New PIREON_PASSWORD_CLIENT_ID/SECRET: password client, falling back to the former so existing setups keep working. - Refresh tries both clients: a refresh token only works with its issuer. - PIREON_PASSWORD_GRANT=false now also rejects password grants in the API (before it only hid the form). - /api/auth/config returns clientId: null when there is no authorization_code client, and the web hides "Sign in with Pireon". - docker-compose passes the new variables; docs 05/07, .env.example, openapi.json; unit tests for the client selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.0 KiB
5. Autenticación (Pireon)
Los usuarios pertenecen a Pireon (https://oauth.pireon.dev, OAuth2 con Laravel Passport). TaskStream no guarda
contraseñas: solo valida tokens y guarda una copia local de nombre y email para mostrarlos.
5.1 Cómo valida la API un token
sequenceDiagram
participant C as Cliente
participant G as AuthGuard (presenters)
participant Q as ResolveSessionQuery
participant R as Redis (caché)
participant P as Pireon
C->>G: Authorization: Bearer <token>
G->>Q: resolveSession(token)
Q->>R: GET auth:session:sha256(token)
alt en caché
R-->>Q: usuario
else no está
Q->>P: GET /api/user
P-->>Q: usuario + roles
Q->>R: SET (AUTH_SESSION_TTL_SECONDS)
end
Q-->>G: User (o 401)
- Se cachea el hash SHA-256 del token, nunca el token en claro.
- Si Pireon marca al usuario como inactivo, el token se rechaza.
- El guard es global: todo requiere token salvo lo marcado con
@Public()(health, configuración de login, proxy de tokens y webhook). - En subscriptions, el token se valida una vez al abrir el WebSocket (
connection_init). @RequireAppRoles('…')permite exigir roles del cliente OAuth en Pireon (los admins globales de Pireon los tienen todos).
5.2 Iniciar sesión desde el navegador
Pireon no permite CORS en /oauth/token y el client_secret no debe llegar al navegador, así que la web pasa por
la API: POST /api/auth/token hace de proxy hacia Pireon.
Cada cliente OAuth de Pireon admite un único tipo de grant, así que cada forma de entrar usa su cliente:
| Método | Cliente de Pireon (grant_type) |
Variables | Flujo |
|---|---|---|---|
| Entrar con Pireon (Authorization Code + PKCE) | authorization_code |
PIREON_CLIENT_ID, PIREON_CLIENT_SECRET |
Web → /oauth/authorize de Pireon → /auth/callback → POST /api/auth/token (authorization_code + code_verifier) |
| Email y contraseña | password |
PIREON_PASSWORD_CLIENT_ID, PIREON_PASSWORD_CLIENT_SECRET |
POST /api/auth/token (password); se desactiva con PIREON_PASSWORD_GRANT=false |
Si no se definen las variables PIREON_PASSWORD_CLIENT_*, el login con contraseña usa PIREON_CLIENT_ID (sirve si
solo hay un cliente de tipo password, pero entonces «Entrar con Pireon» falla con unauthorized_client). Al renovar
la sesión, la API prueba los dos clientes: un refresh_token solo vale con el cliente que lo emitió.
El email y la contraseña son los de la cuenta de Pireon; TaskStream no los guarda, solo los reenvía. (El login con
token de acceso personal se ha retirado de la web por ahora; la API sigue aceptando esos tokens como Bearer.)
Registrar TaskStream como cliente en Pireon
Con un token tuyo de Pireon (scope account:write), POST https://oauth.pireon.dev/api/clients:
-
Cliente para «Entrar con Pireon»:
{"name": "TaskStream", "grant_type": "authorization_code", "confidential": true, "redirect_uris": [...]}con estas redirect URIs:http://localhost:4200/auth/callback(desarrollo) yhttp://localhost:8080/auth/callback(Docker)https://taskstream.pireon.dev/auth/callback(producción)- para el «Authorize» de Swagger UI:
http://localhost:3000/api/docs/oauth2-redirect.htmlyhttps://api.taskstream.pireon.dev/api/docs/oauth2-redirect.html(ver API REST)
→
PIREON_CLIENT_IDyPIREON_CLIENT_SECRET. -
Cliente para email y contraseña:
{"name": "TaskStream (password)", "grant_type": "password"}→PIREON_PASSWORD_CLIENT_IDyPIREON_PASSWORD_CLIENT_SECRET.
Ponlos en .env (o en el entorno del contenedor; en Dokploy, en Environment) y reinicia la API.
GET /api/auth/config le dice a la web qué formas de entrar hay: clientId del cliente authorization_code (o null:
la web oculta «Entrar con Pireon») y passwordGrant. Devuelve null si no hay ningún cliente; en ese caso el login
muestra el aviso y no deja entrar.
Sesión en la web
- La sesión (
accessToken,refreshToken, caducidad) se guarda enlocalStoragey se restaura al arrancar. - Un minuto antes de caducar se renueva con
refresh_token. Si llegan varias peticiones a la vez, se renueva una sola vez. - Si la API responde
UNAUTHENTICATED, se cierra la sesión y se vuelve al login. - El parámetro
statedel flujo PKCE protege contra CSRF; elcode_verifiervive ensessionStoragesolo durante la redirección.
5.3 Webhooks de Pireon (opcional)
Para que la desactivación de un usuario o la revocación de un token surtan efecto al instante (sin esperar a que caduque la caché):
- Registra
https://<tu-api>/api/webhooks/pireoncon los eventosuser.deactivatedytoken.revoked(POST /api/clients/{client}/webhooks). - Pon el secret que devuelve en
PIREON_WEBHOOK_SECRET.
La API verifica la firma X-Webhook-Signature (HMAC-SHA256 del cuerpo), marca al usuario como inactivo y borra sus
sesiones cacheadas. Sin PIREON_WEBHOOK_SECRET, el endpoint rechaza todas las llamadas.
5.4 Usuarios en TaskStream
- Al validar un token, el usuario se guarda (nombre, email, activo) en la colección
usersde MongoDB. users(search)sincroniza antes el directorio de Pireon con el token del usuario, así que se puede añadir a gente que aún no ha entrado en TaskStream (requiere el scopeusers:read).- Los read models guardan el nombre de los usuarios desnormalizado: si alguien cambia su nombre en Pireon, las entradas antiguas mantienen el nombre anterior.