fix(auth): separate Pireon clients for authorization code and password
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>
This commit is contained in:
@@ -38,29 +38,42 @@ sequenceDiagram
|
||||
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.
|
||||
|
||||
| Método | Necesita | Flujo |
|
||||
| ------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Entrar con Pireon** (Authorization Code + PKCE) | `PIREON_CLIENT_ID`, `PIREON_CLIENT_SECRET` y la redirect URI registrada | Web → `/oauth/authorize` de Pireon → `/auth/callback` → `POST /api/auth/token` (`authorization_code` + `code_verifier`) |
|
||||
| **Email y contraseña** | El mismo cliente, con `PIREON_PASSWORD_GRANT=true` | `POST /api/auth/token` (`password`) |
|
||||
Cada cliente OAuth de Pireon admite **un único tipo de grant**, así que cada forma de entrar usa su cliente:
|
||||
|
||||
Las dos opciones necesitan el cliente OAuth: sin `PIREON_CLIENT_ID` la API responde `503` y el login avisa de que no
|
||||
está configurado. 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`.)
|
||||
| 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
|
||||
|
||||
1. Crea el cliente: `POST https://oauth.pireon.dev/api/clients` (con un token tuyo de Pireon, scope `account:write`).
|
||||
2. Registra como redirect URIs:
|
||||
- `http://localhost:4200/auth/callback` (desarrollo)
|
||||
- `http://localhost:8080/auth/callback` (Docker)
|
||||
- la de producción
|
||||
- para el botón «Authorize» de Swagger UI: `http://localhost:3000/api/docs/oauth2-redirect.html` y la de
|
||||
producción (ver [API REST](03-api.md#33-rest-con-openapi-para-otras-apps))
|
||||
3. Pon `PIREON_CLIENT_ID` y `PIREON_CLIENT_SECRET` en `.env` (o en el entorno del contenedor).
|
||||
Con un token tuyo de Pireon (scope `account:write`), `POST https://oauth.pireon.dev/api/clients`:
|
||||
|
||||
`GET /api/auth/config` devuelve a la web lo necesario para iniciar el flujo (`authorizeUrl`, `clientId`, `scopes`,
|
||||
`passwordGrant`), o `null` si no hay cliente configurado; en ese caso el login muestra el aviso y no deja entrar.
|
||||
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
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ docker compose up -d --build
|
||||
```
|
||||
|
||||
En **Dokploy** (despliegue con Docker Compose): pestaña _Environment_ del servicio → `NEXUS_TOKEN=<token>`, junto a
|
||||
`PIREON_CLIENT_ID`, `PIREON_CLIENT_SECRET` y `PIREON_WEBHOOK_SECRET`. Dokploy los escribe en el `.env` del proyecto, de
|
||||
`PIREON_CLIENT_ID`, `PIREON_CLIENT_SECRET`, `PIREON_PASSWORD_CLIENT_ID`, `PIREON_PASSWORD_CLIENT_SECRET` y
|
||||
`PIREON_WEBHOOK_SECRET`. Dokploy los escribe en el `.env` del proyecto, de
|
||||
donde los lee Compose.
|
||||
|
||||
## 7.2 Variables de entorno de la API
|
||||
@@ -79,7 +80,8 @@ donde los lee Compose.
|
||||
| `EVENT_STREAM_MAX_LENGTH` | `1000000` | Longitud aproximada que se conserva en el stream |
|
||||
| `EVENTS_BRIDGE_ENABLED` | `true` | Si esta instancia proyecta eventos hacia Mongo (ver escalado) |
|
||||
| `PIREON_BASE_URL` | `https://oauth.pireon.dev` | Servidor de identidad |
|
||||
| `PIREON_CLIENT_ID` / `PIREON_CLIENT_SECRET` | — | Cliente OAuth para el login desde la web |
|
||||
| `PIREON_CLIENT_ID` / `PIREON_CLIENT_SECRET` | — | Cliente OAuth `authorization_code`: «Entrar con Pireon» y Swagger UI |
|
||||
| `PIREON_PASSWORD_CLIENT_ID` / `_SECRET` | el anterior | Cliente OAuth `password`: login con email y contraseña |
|
||||
| `PIREON_SCOPES` | `users:read` | Scopes que se piden al iniciar sesión |
|
||||
| `PIREON_PASSWORD_GRANT` | `true` | Permite el login con email y contraseña |
|
||||
| `PIREON_WEBHOOK_SECRET` | — | Activa y firma los webhooks de Pireon |
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user