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

14 KiB
Raw Blame History

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:

-- 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:
    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.