Files
ERP/docs/casdoor-auth.md
T
ali_mohaghegh c59f5ad1a3 docs: Casdoor auth guide + README login/known-behavior update
- docs/casdoor-auth.md: addresses and credentials (Casdoor console
  admin/123, seeded users), in-page login flow, status-code table, the
  User/Organization/Role/Permission/Application model, the 9 failure
  scenarios with debug steps, Test 1-6 results, 23-item checklist, and how
  to manage Casdoor (console, seeding, first-admin bootstrap)
- README: replace the 'no authentication' and 'Casdoor not seeded' notes
  with the current login flow, developer-mode toggle and a link to the
  auth guide
2026-08-29 14:44:27 +03:30

217 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Casdoor Authentication & Authorization Guide (v2.6)
This document explains how the procurement + inventory system uses Casdoor:
addresses, credentials, the login flow, the authorization model, failure
scenarios, test results, and how to manage the system yourself.
---
## 1. Addresses & credentials
| What | Address | Credentials |
|---|---|---|
| App (frontend) | http://localhost:3000 | — |
| API (backend) | http://localhost:8000 (Swagger: `/docs`) | — |
| Casdoor console | http://localhost:8080 | **admin / 123** (built-in org) |
| Casdoor OIDC discovery | http://localhost:8080/.well-known/openid-configuration | — |
| Casdoor public JWKS | http://localhost:8080/.well-known/jwks | — |
| In-network Casdoor (from the api container) | http://casdoor:8000 | — |
### Seeded application users (org `inventory`)
| Username | Password | Casdoor org | App role (our `users` table) |
|---|---|---|---|
| `razaei` | `123456` | inventory | `admin` (super user) |
| `mousavi` | `123456` | inventory | `procurement_manager` (مدیر تدارکات) |
| `mohaghegh` | `123456` | inventory | `pm` (مدیر پروژه، بدون پروژه) |
| `tester` | `123456` | inventory | `pm` (مدیر پروژه — پروژه «تست») |
These users are created by `backend/tools/seed_casdoor.py` (idempotent — safe
to re-run).
---
## 2. Login flow (in-page, no redirect)
```
Browser ──► POST /api/v1/auth/login {username, password, organization?}
│
▼
Backend ──► POST http://casdoor:8000/api/login/oauth/access_token
grant_type=password, client_id/secret, username, password
│
▼ access_token (JWT)
Backend ──► verifies locally: JWKS (/.well-known/jwks, by kid),
iss == http://localhost:8080, aud == inventory-app-client
──► auto-provisions the app `users` row (role NULL on first login)
──► returns {access_token, user: {role, role_label, project_id, ...}}
│
▼
Frontend stores the token (localStorage) + profile, navigates by role:
pm → /inventory (store panel)
admin / procurement_manager → / (procurement office)
```
- **Two Casdoor URLs exist on purpose:** `PROCUREMENT_CASDOOR_ENDPOINT`
(`http://casdoor:8000`) is the in-network service URL used for token exchange
and JWKS fetching; `PROCUREMENT_CASDOOR_ISSUER` (`http://localhost:8080`) is
the browser-facing origin that Casdoor embeds in the `iss` claim (its
`origin` env in docker-compose). Both must be set — see the `api` service
environment block in `docker-compose.yml`.
- **Logout:** JWTs are stateless; logout clears the token + profile on the
client. The next protected call then gets `401`. There is no server-side
session to revoke (a token stays valid until `exp` unless a blacklist is
introduced — out of scope).
- The old browser-redirect (PKCE / authorization-code) flow was removed
together with the public `casdoor-config.js` (which exposed the client
secret). The `password` grant is what powers the in-page form.
---
## 3. Authentication — status codes
| Case | Status | Code | Message |
|---|---|---|---|
| Valid token | 200 | — | data |
| Missing Authorization header | 401 | `UNAUTHORIZED` | احراز هویت لازم است — توکن Casdoor یا حالت توسعه را ارسال کنید |
| Corrupted / unreadable token | 401 | `UNAUTHORIZED` | توکن نامعتبر است |
| Unknown kid / bad signature | 401 | `UNAUTHORIZED` | امضای توکن نامعتبر است |
| Expired / wrong iss / wrong aud | 401 | `UNAUTHORIZED` | توکن نامعتبر یا منقضی شده است |
| Valid token, user has no role yet | 403 | `FORBIDDEN` | نقش شما هنوز در سیستم تعیین نشده است |
| Valid token, wrong role for the section | 403 | `FORBIDDEN` | دسترسی به این بخش برای نقش شما مجاز نیست |
| Casdoor unreachable | 503 | `UPSTREAM_UNAVAILABLE` | سرویس احراز هویت (Casdoor) در دسترس نیست |
| Wrong Casdoor client config | 503 | `UPSTREAM_UNAVAILABLE` | سرویس احراز هویت به درستی پیکربندی نشده است |
| Wrong username/password | 401 | `UNAUTHORIZED` | نام کاربری یا رمز عبور نادرست است |
---
## 4. Authorization — how User / Organization / Role / Permission / Application relate
**Decision (agreed): roles stay in the app's own database. Casdoor is used for
identity only.** No new authorization model was introduced.
- **Casdoor Application** (`inventory-app`, client `inventory-app-client`):
the OIDC client. Grant types: `authorization_code`, `refresh_token`,
`password`. Token format JWT (so the backend can verify via JWKS).
- **Casdoor Organization** (`inventory`): the tenant; every app user belongs
to it. (The Casdoor console admin `admin/123` lives in the separate
`built-in` org.)
- **User — two records per person:**
1. Casdoor user (login credential: name + password).
2. App `users` row, linked by `users.casdoor_id = Casdoor user id` (the JWT
`sub` claim). Auto-provisioned on first login.
- **Role** lives in `users.role` (not in Casdoor): `pm` | `procurement_manager`
| `admin` (NULL until assigned). Assigned from the app's Organization panel
(`/inventory/org`) by an admin / procurement manager.
- **Permission** is enforced per-endpoint in the backend routers, not in
Casdoor permission objects:
- `admin` (super user): everything; can create users and assign roles/projects
(adds PMs); can use Developer Mode to act as either of the two main roles.
- `procurement_manager` (مدیر تدارکات): runs the procurement office; can
also add/manage users and project managers (same Organization panel rights
as admin); read-only view of the inventory panel.
- `pm` (مدیر پروژه): store only — inventory dashboard, stock changes, daily
control, warehouses, arrivals. Scoped to the single project in
`projects.pm_user_id`.
- **Protected vs open endpoints (current split):** `/auth`, `/users`,
`/inventory`, `/pm` require authentication (Casdoor JWT, or dev headers in
Developer Mode). The procurement CRUD endpoints (quotes, suppliers,
materials, …) are still open — documented known behavior, not part of this
phase.
### Bootstrapping the first admin
A user is auto-provisioned with `role = NULL` on first login, and assigning
roles requires an admin — so bootstrap the very first admin directly in the DB:
```sql
-- list provisioned users
SELECT id, casdoor_id, first_name, last_name, role FROM users;
-- promote one of them (casdoor_id = the Casdoor user id)
UPDATE users SET role = 'admin' WHERE casdoor_id = '<casdoor-user-id>';
```
After that, log in as the admin and use the Organization panel
(`/inventory/org`) for everything else.
---
## 5. Failure scenarios — where the error happens, what the user sees, how to debug
| # | Scenario | Where it fails | What the user sees | Backend log (api container) | How to debug |
|---|---|---|---|---|---|
| 1 | Casdoor unavailable | `POST /auth/login` (token exchange) or JWKS fetch on protected endpoints | Login page: «سرویس احراز هویت (Casdoor) در دسترس نیست»; API: 503 | `Casdoor token endpoint unreachable (…): …` or `JWKS fetch failed (…): …` | `docker compose ps` — casdoor must be `Up`; from the api container: `python -c "import urllib.request; print(urllib.request.urlopen('http://casdoor:8000/').status)"` |
| 2 | Backend unavailable | nginx `/api/` proxy | App shows «اتصال به سرور برقرار نشد» (frontend toast) | none (api is down) | `docker compose ps api`; `docker logs procurement-api`; curl `http://localhost:8000/health` |
| 3 | Invalid client (wrong client_id/secret) | Casdoor token endpoint | 503 «سرویس احراز هویت به درستی پیکربندی نشده است» | `Casdoor rejected password grant: error=invalid_client description=…` | Compare `PROCUREMENT_CASDOOR_CLIENT_ID/SECRET` in docker-compose with the application in the Casdoor console (Applications → inventory-app) |
| 4 | Invalid redirect URI | Casdoor authorization endpoint (browser flows only) | Casdoor's own error page in the browser | — (backend not involved) | The app no longer uses redirects. In the Casdoor console, the app's Redirect URIs must contain `http://localhost:3000/login` |
| 5 | Invalid / corrupted JWT | Backend JWT verification | 401 «توکن نامعتبر است» / «امضای توکن نامعتبر است» | `JWT unreadable: …` or `JWT kid '…' not found in JWKS` | Decode the token at jwt.io; check `kid` against `http://localhost:8080/.well-known/jwks` |
| 6 | Expired JWT | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … Signature has expired` | Token `exp` claim; lifetime = Application → `expireInHours` (24h) |
| 7 | Missing JWT | Protected endpoint dependency | 401 «احراز هویت لازم است…» | normal access log, 401 | Send `Authorization: Bearer <token>`; in the app this means the session was cleared — log in again |
| 8 | Wrong audience | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … audience` | Token `aud` must contain `inventory-app-client` (= `PROCUREMENT_CASDOOR_AUDIENCE`) |
| 9 | Wrong issuer | Backend JWT verification | 401 «توکن نامعتبر یا منقضی شده است» | `JWT verification failed: … issuer` | Token `iss` must equal `PROCUREMENT_CASDOOR_ISSUER` (`http://localhost:8080`); Casdoor's `origin` env must stay `http://localhost:8080` |
| 10 | User without permission | Role checks in `auth.py` / routers | 403 «نقش شما هنوز در سیستم تعیین نشده است» or «دسترسی به این بخش برای نقش شما مجاز نیست» | access log with 403 | Organization panel → assign the user a role; PMs additionally need a project via `/users/{id}/project` |
---
## 6. Authentication test results (run 2026-08-29, Docker compose stack)
| Test | Action | Result | Evidence |
|---|---|---|---|
| 1 | Login: `POST /api/v1/auth/login` (tester/123456) | **PASS** | 200, JWT + profile returned |
| 2 | Authenticated API: `GET /users/me` with token | **PASS** | 200, role `pm` + project «تست» |
| 3 | No token: `GET /inventory/warehouses` | **PASS** | 401 `UNAUTHORIZED` |
| 4 | Invalid token: corrupted Bearer | **PASS** | 401 `UNAUTHORIZED` «توکن نامعتبر است» |
| 5 | Expired token (app lifetime set to ~1s, then restored to 24h) | **PASS** | 401 «توکن نامعتبر یا منقضی شده است» |
| 6 | Logout: token cleared client-side → protected API | **PASS** | 200 before, 401 after cleanup |
---
## 7. Final checklist (re-verified after the login-page redesign, 2026-08-29)
| # | Item | Result |
|---|---|---|
| 1 | Casdoor container works | ✅ Up; console + REST API respond |
| 2 | Casdoor PostgreSQL works | ✅ Up; queried directly (orgs/users/apps) |
| 3 | Backend can reach Casdoor | ✅ api container does token exchange + JWKS fetch (`casdoor:8000`) |
| 4 | Frontend can reach Casdoor | ✅ network path open — though with the in-page flow the frontend only talks to the backend, which talks to Casdoor |
| 5 | Seeder works | ✅ ran twice; idempotent (existing objects reported, nothing breaks) |
| 6 | Organization exists | ✅ `inventory` |
| 7 | Application exists | ✅ `inventory-app` |
| 8 | Client exists | ✅ `inventory-app-client` (grants incl. `password`) |
| 9 | Test users exist | ✅ razaei, mousavi, mohaghegh, tester (password 123456) |
| 10 | Redirect URI matches | ✅ `http://localhost:3000/login` registered (unused by the new flow, kept for console flows) |
| 11 | OIDC works | ✅ discovery doc + token endpoint + JWT issuance verified |
| 12 | PKCE works | ➖ **N/A** — the PKCE/redirect flow was replaced by the in-page password-grant login (per the redesign spec); nothing to verify |
| 13 | Login works | ✅ form login in the browser (tester → `/inventory`) |
| 14 | Token is received | ✅ JWT returned by `POST /auth/login` |
| 15 | Backend validates JWT | ✅ signature/kid/iss/aud/exp — wrong values each tested → 401 |
| 16 | JWKS works | ✅ public `/.well-known/jwks` (kid `cert-built-in`) |
| 17 | Authenticated API returns 200 | ✅ `GET /users/me` with Bearer token |
| 18 | Missing token returns 401 | ✅ |
| 19 | Invalid token returns 401 | ✅ |
| 20 | Logout works | ✅ button clears the session → back to `/login`; protected API → 401 |
| 21 | Authorization/Roles are understood | ✅ documented in §4 (roles in app DB, per-role rules) |
| 22 | No secrets accidentally exposed | ✅ `casdoor-config.js` (with clientSecret) removed from public files; secret only in backend env + seeder |
| 23 | Documentation is complete | ✅ this document (addresses, credentials, flows, failure scenarios, management) |
---
## 8. Managing Casdoor yourself
- **Console:** http://localhost:8080 — log in as **admin / 123**. From there
you can manage Organizations, Users, Applications, Providers, Certificates.
- **Seeding** (org `inventory`, app `inventory-app`, the four users) —
re-runnable at any time:
```powershell
python backend/tools/seed_casdoor.py # from the host
# or:
docker compose run --rm -e CASDOOR_BASE=http://casdoor:8000 api python tools/seed_casdoor.py
```
- **Adding a user end-to-end:** add the user in the Casdoor console (org
`inventory`, set a password, passwordType `plain` for dev) → the user logs in
through the app once (auto-provisions the `users` row) → assign the role and,
for PMs, the project from the app's Organization panel (`/inventory/org`).
- **Debugging tokens:** use the discovery + JWKS endpoints above; the api
container logs every rejected token with the reason (`JWT verification
failed: …`), so `docker logs procurement-api` is the first place to look.