- 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
14 KiB
14 KiB
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 theissclaim (itsoriginenv in docker-compose). Both must be set — see theapiservice environment block indocker-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 untilexpunless 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). Thepasswordgrant 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, clientinventory-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 adminadmin/123lives in the separatebuilt-inorg.) - User — two records per person:
- Casdoor user (login credential: name + password).
- App
usersrow, linked byusers.casdoor_id = Casdoor user id(the JWTsubclaim). 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 inprojects.pm_user_id.
- Protected vs open endpoints (current split):
/auth,/users,/inventory,/pmrequire 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:
-- 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, appinventory-app, the four users) — re-runnable at any time: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, passwordTypeplainfor dev) → the user logs in through the app once (auto-provisions theusersrow) → 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: …), sodocker logs procurement-apiis the first place to look.