+ Tóm tắt một câu: thôi customize trang login của từng app - middleware chuyển user chưa đăng nhập thẳng sang id.dos.me (đăng nhập + đăng ký ở một nơi), trang login cũ giữ làm đường dự phòng
+
+ ?direct=1. Mỗi app chỉ cần 3 mảnh: gate ở middleware + trang cầu nối + escape hatch, không đụng UI login nữa.
+ 1. Flow hoạt động
+
+User vào /auth/login (chưa đăng nhập)
+ |
+ v
+[MIDDLEWARE] dos-id đã cấu hình (OIDC client id + secret) và KHÔNG có ?direct=1?
+ | có | không (chưa cấu hình / direct=1)
+ v v
+ 302 -> /auth/sso Hiện trang login cũ (form email, magic link...)
+ | = đường break-glass cho admin
+ v
+[TRANG CẦU NỐI /auth/sso]
+ gọi signIn("dos-id", { callbackUrl })
+ (auth library lo CSRF / state / nonce)
+ |
+ v
+ 302 -> id.dos.me (đăng nhập / đăng ký)
+ |
+ v
+ callback về app -> JIT tạo user/org nếu user mới -> vào app
+
+
+ 2. Bốn nguyên tắc bắt buộc (không được bỏ)
+| # | Nguyên tắc | Vì sao |
|---|---|---|
| 1 | Luôn đi qua auth library (next-auth sign-in endpoint / tương đương), không tự viết redirect thẳng tới authorize endpoint của IdP | State/nonce/CSRF phải do thư viện xử lý. Tự chế = tự mở lỗổng login CSRF. |
| 2 | Gate redirect theo đúng điều kiện đăng ký provider - chỉ redirect khi OIDC client id + secret thật sự được cấu hình | Redirect tới provider chưa đăng ký = lỗi "unknown provider" cho mọi user. Ở Crove-Cal, điều kiện middleware mirror y hệt điều kiện if (oidcId && oidcSecret) trong phần đăng ký provider. |
| 3 | Giữ break-glass: /auth/login?direct=1 (hoặc tương đương) luôn hiện form login cũ | id.dos.me chết = không ai vào được app. Admin cần đường vào local khi IdP unreachable. |
| 4 | Logout không được auto-login lại - trang sau logout phải là trang dừng có nút bấm, không tự kích hoạt sign-in | Session IdP còn sống mà logout xong bị đẩy ngay vào sign-in = "đăng xuất không được" - bug kinh điển của SSO auto-redirect. |
3. Chuẩn bị cho mỗi app (một lần)
+-
+
- Đăng ký OIDC client trên id.dos.me cho app:
client_id+client_secret, redirect/callback URL trỏ về app (vdhttps://desk.crove.com/api/auth/callback/dos-id).
+ - Khai báo env cho app:
OIDC_CLIENT_ID,OIDC_CLIENT_SECRET,OIDC_WELL_KNOWN_URL(cùng bộ id.dos.me).
+ - Bảo đảm app có sẵn OIDC provider đăng ký theo env (gate nếu thiếu env - xem Crove-Cal
packages/features/auth/lib/next-auth-options.ts, hàmDosIdProvider).
+
+ Lưu ý env trên Docker: biến env client-side phải nằm trong
+
+ turbo.json globalEnv (nếu dùng turbo) và trong Dockerfile/compose đúng giai đoạn, nếu không build cache sẽ trả giá trị sai - đây là finding MD-29 Crove-Cal từng dính.
+ 4. Ba mảnh code cần thêm (Next.js + next-auth)
+Tham khảo trực tiếp Crove-Cal PR #78 (đã chạy production):
+-
+
- Mảnh 1 - Middleware gate (
apps/web/proxy.ts): hàmshouldRedirectToDosIdSso(url)+ redirect 302 sang/auth/sso(forwardcallbackUrlnếu có). Chỉ ~15 dòng.
+ - Mảnh 2 - Trang cầu nối (
apps/web/modules/auth/sso-view.tsx+app/(use-page-wrapper)/auth/sso/page.tsx): client component gọisignIn("dos-id", { callbackUrl, redirect: true })tronguseEffect, UI chỉ có spinner + nút "dùng cách đăng nhập khác" trỏ về/auth/login?direct=1. Nếu signIn fail (IdP chết) hiện thông báo lỗi + nút fallback.
+ - Mảnh 3 - Escape hatch: tham số
?direct=1trong gate (mảnh 1) - không cần code riêng, trang login cũ giữ nguyên.
+
+// Mảnh 1 - middleware (rút gọn từ proxy.ts Crove-Cal)
+const isDosIdProviderConfigured = () =>
+ !!((process.env.OIDC_CLIENT_ID || "").trim() &&
+ (process.env.OIDC_CLIENT_SECRET || "").trim());
+
+const shouldRedirectToDosIdSso = (url) =>
+ isDosIdProviderConfigured() &&
+ (url.pathname === "/auth/login" || url.pathname === "/login") &&
+ url.searchParams.get("direct") !== "1";
+
+// trong middleware handler:
+if (shouldRedirectToDosIdSso(url)) {
+ const sso = new URL("/auth/sso", req.url);
+ const cb = url.searchParams.get("callbackUrl");
+ if (cb) sso.searchParams.set("callbackUrl", cb);
+ return NextResponse.redirect(sso);
+}
+
+
+// Mảnh 2 - trang cầu nối (rút gọn từ sso-view.tsx Crove-Cal)
+useEffect(() => {
+ signIn("dos-id", { callbackUrl, redirect: true }).catch(() => setFailed(true));
+}, [callbackUrl]);
+// UI: failed ? "Không kết nối được DOS.Me ID" : "Đang chuyển tới DOS.Me ID..."
+// + nút fallback: /auth/login?direct=1&callbackUrl=...
+
+
+ Stack không phải next-auth (Crove CRM, Desk, Sign...)
+Nguyên tắc y hệt, chỉ đổi tên công cụ: tìm nơi auth library nhận "bắt đầu sign-in với provider X" (một endpoint/hàm chính thức - Laravel: Socialite::driver('dos')->stateless()-KHÔNG, luôn dùng redirect() có state; Passport-SAML/OIDC: authorize URL do thư viện sinh) và gọi nó từ trang cầu nối. Cấm tự nối chuỗi authorize URL bằng tay.
5. Test bắt buộc trước khi merge
+-
+
- Unit test middleware: redirect khi đủ env; KHÔNG redirect khi thiếu env / có
direct=1/ path khác login; forward callbackUrl. (Xem 6 test trongapps/web/proxy.test.tscủa Crove-Cal.)
+ - Chạy app với env đủ: vào
/auth/loginphải bật qua/auth/ssotới id.dos.me.
+ /auth/login?direct=1phải hiện form cũ.
+ - Logout xong phải dừng ở trang logout, KHÔNG tự vào lại app. +
- User mới (chưa có trong app) đăng nhập lần đầu qua id.dos.me phải được JIT provisioning đúng org/permission. +
6. Runbook deploy lên server crove (theo cách Crove-Cal đang chạy)
+-
+
- Merge PR -> CI xanh (type-check + lint + test) -> deploy-docker build image
:latestmới lên GHCR (sau ci-gate).
+ - SSH server:
gcloud compute ssh crove-server --zone=asia-southeast1-b --project=crove-os.
+ cd /opt/crove/<app> && docker compose pull <service> && docker compose up -d --no-deps <service>
+ - Theo dõi:
docker ps --filter name=<app>(chờ healthy) +docker logs <app> --tail 30.
+ - Verify ngoài: mở app production, vào
/auth/loginphải chuyển sang id.dos.me.
+
+ Rollback 30 giây khi container crash-loop: tìm image cũ vẫn còn trên server
+
+ docker images (dangling), gán lại tag rồi recreate:
+ docker tag <old-image-id> ghcr.io/dos/<app>:latest +cd /opt/crove/<app> && docker compose up -d --no-deps <service>+ Lần 18/09 Crove-Cal crash-loop vì permission (xem mục 7) - rollback bằng đúng lệnh này, prod sống lại ngay khi chờ image fix. +
7. Bài học từ lần đầu ship (đọc trước khi deploy app kế tiếp)
+-
+
- Chuyển container sang user non-root phải rà mọi chỗ ghi runtime. Crove-Cal thêm
USER node(PR #74) -> image khởi động chết vì turbo cần tạo/calcom/.turbotrong thư mục root-owned. Fix:RUN mkdir -p &.turbo && chown node:node &.turbo(PR #79). Khi app các project khác chuyển non-root: liệt kê mọi thư mục app ghi lúc runtime (cache, tmp, upload) và chown hết trước khiUSER.
+ - Luôn có rollback sẵn trước khi pull. Image cũ vẫn nằm trên server (dangling) - chỉ cần tag lại là quay về trong 30 giây. +
- Đừng tin "image build xanh = chạy được". Build không phát hiện lỗi permission runtime. Bắt buộc theo dõi
docker ps+ logs sau mỗi lần up, và chỉ xác nhận xong khi healthcheck healthy.
+ - Redirect phải im lặng khi env thiếu. Môi trường dev/staging chưa có OIDC env phải giữ hành vi cũ (hiện form) - nhờ gate mirror điều kiện đăng ký provider. +
8. Liên kết tham chiếu trong Crove-Cal
+| Thành phần | Vị trí |
|---|---|
| Middleware gate + redirect | apps/web/proxy.ts (hàm shouldRedirectToDosIdSso) |
| Trang cầu nối | apps/web/modules/auth/sso-view.tsx + app/(use-page-wrapper)/auth/sso/page.tsx |
| Điều kiện đăng ký provider (middleware phải mirror cái này) | packages/features/auth/lib/next-auth-options.ts (hàm DosIdProvider) |
| Unit test | apps/web/proxy.test.ts (describe "DOS ID auto-SSO redirect") |
| i18n keys | packages/i18n/locales/en/common.json: sso_redirecting_title, sso_redirecting_body, sso_redirect_failed, sso_use_other_login |
| PR tham chiếu | #78 (feature) + #79 (hotfix turbo cache dir) |
+ Định nghĩa xong với mỗi app: (1) OIDC client trên id.dos.me + env cấu hình, (2) 3 mảnh code theo mục 4, (3) 5 bài test mục 5 pass, (4) deploy theo runbook mục 6 kèm verify production, (5) ghi chú lại bất kỳ bài học mới vào guide này (đặt file guide trong repo của app đó).
+
+