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>
104 lines
6.0 KiB
Markdown
104 lines
6.0 KiB
Markdown
# 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
|
|
|
|
```mermaid
|
|
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`:
|
|
|
|
1. **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) y `http://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.html` y
|
|
`https://api.taskstream.pireon.dev/api/docs/oauth2-redirect.html` (ver
|
|
[API REST](03-api.md#33-rest-con-openapi-para-otras-apps))
|
|
|
|
→ `PIREON_CLIENT_ID` y `PIREON_CLIENT_SECRET`.
|
|
|
|
2. **Cliente para email y contraseña**: `{"name": "TaskStream (password)", "grant_type": "password"}`
|
|
→ `PIREON_PASSWORD_CLIENT_ID` y `PIREON_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 en `localStorage` y 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 `state` del flujo PKCE protege contra CSRF; el `code_verifier` vive en `sessionStorage` solo 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é):
|
|
|
|
1. Registra `https://<tu-api>/api/webhooks/pireon` con los eventos `user.deactivated` y `token.revoked`
|
|
(`POST /api/clients/{client}/webhooks`).
|
|
2. 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 `users` de 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 scope `users: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.
|