Kế hoạch: Tích hợp Casdoor cho Google / Facebook / Apple Login
Áp dụng cho Cooco App (Expo SDK 54) — cooco_app/. Casdoor đóng vai trò OIDC Identity Provider (IdP): App gọi Casdoor để đăng nhập qua Google/Facebook/Apple, Casdoor trả về token, BFF (.NET) đổi code lấy JWT của Cooco. Toàn bộ kiến trúc auth hiện tại (x-cooco-token, refresh, session restore) giữ nguyên, chỉ thêm 1 luồng đăng nhập mới.
1. Tổng quan kiến trúc
┌──────────────┐ OIDC /authorize ┌────────────────────────┐
│ Cooco App │ ──────────────────▶ │ Casdoor (IdP) │
│ (Expo RN) │ ◀────────────────── │ - Google / Facebook / │
│ │ auth code (deep │ Apple (social IdP) │
│ │ link redirect) │ - Tự lưu user Casdoor │
└──────┬───────┘ └────────────────────────┘
│ POST /api/auth/casdoor { code } (BFF đổi code ↔ Casdoor)
▼
┌──────────────┐ JWT + RefreshToken (giống loginBff hiện tại)
│ BFF (.NET) │ ──────────────────▶ tokenStorage → x-cooco-token
└──────────────┘
- Luồng: App mở trình duyệt (in-app
WebBrowser) → user chọn Google/Facebook/Apple ngay trên trang Casdoor → Casdoor redirect về app bằng deep link kèmcode→ App gọi BFFPOST /api/auth/casdoor { code }→ BFF đổi code với Casdoor (token exchange) → BFF tạo JWT Cooco + RefreshToken → App lưu token như login thường. - PKCE: dùng cho native để chống intercept code.
code_challengesinh bằng SHA-256 (cầnexpo-crypto).
2. Yêu cầu Backend (BFF .NET — bên khác team)
| # | Việc | Endpoint / Ghi chú |
|---|---|---|
| 1 | Đăng ký Casdoor application (Google, Facebook, Apple provider) | Cấu hình trên giao diện Casdoor: redirect URIs, client_id/secret, provider từng nền tảng |
| 2 | Endpoint nhận auth code | POST /api/auth/casdoor — body { code, codeVerifier?, redirectUri, provider } |
| 3 | Token exchange với Casdoor | BFF dùng code để gọi token endpoint Casdoor (/api/login/oauth/access_token) hoặc SDK Casdoor .NET |
| 4 | Liên kết / tạo member | Khớp email (hoặc sub) với member hiện có → tạo member mới nếu chưa có (giống /api/auth/google) |
| 5 | Trả về JWT Cooco | Response đúng định dạng hiện tại: { jwt_token, refreshToken, member_id, member_name, user } |
| 6 | Refresh token | Giữ endpoint refresh hiện có (không phụ thuộc Casdoor) để App không phải sửa interceptor |
Điều kiện tiên quyết: Backend xong mục 2→5 thì App mới test end-to-end được. Phần App có thể làm và test mock trước (mục 7).
3. Cấu hình & Env (App)
Thêm vào core/config/env.ts và .env.example:
| Biến | Ví dụ | Ghi chú |
|---|---|---|
EXPO_PUBLIC_CASDOOR_URL | https://sso.cooco.example.com | Base URL Casdoor |
EXPO_PUBLIC_CASDOOR_CLIENT_ID | abcd1234... | Client ID của app Casdoor |
EXPO_PUBLIC_CASDOOR_ORG | cooco | Organization trên Casdoor (thường nằm trong app name) |
EXPO_PUBLIC_CASDOOR_APP_NAME | cooco-app | Application name Casdoor |
EXPO_PUBLIC_CASDOOR_REDIRECT_URI | coocoapp://callback | Redirect (có thể sinh tự động bằng makeRedirectUri) |
.env.example hiện có: EXPO_PUBLIC_GOOGLE_ANDROID/WEB/IOS_CLIENT_ID — giữ nguyên (dùng khi mở thẳng Google OAuth), nhưng ưu tiên qua Casdoor.
Lưu ý bảo mật:
EXPO_PUBLIC_*là public — không đưaclient_secretvào App. Secret chỉ nằm ở BFF.
4. Dependencies (App)
| Package | Trạng thái | Cần làm |
|---|---|---|
expo-auth-session ~7.0.11 | ✅ Đã có | Dùng AuthSession generic để build Casdoor OIDC flow |
expo-web-browser ~15.0.11 | ✅ Đã có | openAuthSessionAsync hiện trang Casdoor in-app |
expo-linking ~8.0.11 | ✅ Đã có | Deep link coocoapp:// |
expo-crypto | ❌ Chưa có | Cài thêm (npx expo install expo-crypto) — sinh code_verifier + SHA-256 code_challenge cho PKCE |
expo-apple-authentication ~8.0.8 | ✅ Đã có | Không cần dùng nếu đi qua Casdoor (đăng nhập ngay trên trang Casdoor) |
5. Triển khai phía App — từng bước
Bước 1 — Env config
- Thêm biến mục 3 vào
core/config/env.ts(interface + đọc từprocess.env.EXPO_PUBLIC_*).
Bước 2 — API service: api/services/member.service.ts
- Thêm method, cùng nhóm với
loginBff/loginGoogleBff(dòng ~57-61):loginCasdoorBff(payload: { code: string; codeVerifier?: string; redirectUri?: string; provider?: string }) {return http.post('/api/auth/casdoor', payload);} - Bọc qua
features/auth/api.ts→authApi.signInWithCasdoor(...)(bỏ logic mocksignInWithApplekhi Apple chuyển qua Casdoor).
Bước 3 — Auth store: core/auth/session.store.ts
- Thêm method
loginWithCasdoor(payload)— copy patternloginWithGoogle(dòng 97-134):memberService.loginCasdoorBff({ code, codeVerifier, redirectUri, provider })- Trích token:
res.jwt_token || res.data?.jwt_token || res.token || res.data?.token+refreshToken tokenStorage.setTokens(...)memberService.getInfo()→mapMemberToUser()→setUserqueryClient.setQueryData(queryKeys.profile, user)
Bước 4 — Hook React Query: features/auth/hooks.ts
- Thêm
useCasdoorSignIn()giốnguseGoogleSignIn(dòng 32-35).
Bước 5 — OIDC helper: file mới features/auth/casdoor.ts
buildCasdoorAuthRequest(provider: 'google'|'facebook'|'apple'):const redirectUri = makeRedirectUri({ scheme: 'coocoapp' });return new AuthRequest({clientId: env.casdoorClientId,redirectUri,scopes: ['openid', 'profile', 'email'],usePKCE: true, // cần expo-cryptoextraParams: {client_id: env.casdoorClientId,redirect_uri: redirectUri,scope: 'openid profile email',state: randomState,// Casdoor: org/application nằm trong path URL, không cần param},});- URL authorize:
${env.casdoorUrl}/login/oauth/authorize?client_id=...&redirect_uri=...&response_type=code&scope=openid%20profile%20email&state=...&code_challenge=...&code_challenge_method=S256- Bỏ response_type=token/id_token — luôn dùng authorization code để BFF tự exchange (an toàn hơn, secret không lộ).
- Hàm
promptCasdoor(provider):WebBrowser.openAuthSessionAsync(url, redirectUri)→ parsecode+statetừ query.
Bước 6 — UI: features/auth/screens/LoginScreen.tsx
- Button Google (đang dùng
Google.useIdTokenAuthRequest, dòng 30-34) → chuyển sang gọipromptCasdoor('google')rồiuseCasdoorSignIn().mutate(...). - Button Facebook (chưa có) → thêm, gọi
promptCasdoor('facebook'). - Button Apple (đang stub) → thêm, gọi
promptCasdoor('apple'). - Sau thành công → route giống hiện tại: kiểm tra onboarding →
/(tabs)/recipeshoặc/(onboarding)/welcome.
Bước 7 — Xử lý deep link callback
app.json: đăng ký scheme"coocoapp"(nếu chưa có) — mục hiện chỉ cóexpo-web-browserplugin.app/_layout.tsx:48đã gọiWebBrowser.maybeCompleteAuthSession()— giữ nguyên.- Khi
openAuthSessionAsyncresolve,codevề trongAuthSessionResult(không cần route riêng). Fileapp/google-auth.tsxhiện tại có thể dùng cho Casdoor flow khi app bị kill (fallback): nhậncodetừ URL params → gọiuseCasdoorSignIn().
Bước 8 — Đăng ký link Facebook/Apple trên Casdoor
- Là việc của admin Casdoor (đăng ký provider Facebook App ID/Secret, Apple Team/Key ID/Client ID, Google OAuth Client ID/Secret + redirect URI trỏ về Casdoor).
6. Cấu hình nền tảng (mobile vs web)
| Nền tảng | Redirect URI cần đăng ký với Casdoor | Cách hoạt động |
|---|---|---|
| Android | coocoapp://callback | Deep link custom scheme; (tùy chọn) App Links HTTPS nếu cần |
| iOS | coocoapp://callback | Custom scheme trong Info.plist (Expo tự sinh) |
| Web (Expo) | https://cooco.example.com/callback | makeRedirectUri dùng baseUrl /cooco từ app.config.js |
app.config.jscó sẵn logic xóabaseUrltrên iOS — không ảnh hưởng, deep link dùng scheme riêng.
7. Test & Kiểm thử
| Giai đoạn | Cách test |
|---|---|
| Unit (App, mock BFF) | Test loginWithCasdoor trong core/auth/session.store.test.ts — mock memberService.loginCasdoorBff giống test loginWithGoogle hiện có |
| Manual (mock) | EXPO_PUBLIC_USE_MOCK=true → mock loginCasdoorBff trả { jwt_token: 'mock', refreshToken: 'mock' } → kiểm tra session restore hoạt động |
| E2E (sau khi BFF sẵn sàng) | Device thật: chọn Google → trang Casdoor → đăng nhập Google → quay về app → vào (tabs)/recipes. Lặp lại Facebook, Apple |
| Regression | Đăng nhập email/password, admin (getCmsToken), logout, refresh token, restoreSession sau kill app |
QA smoke (scripts/qa-smoke.js) không cần sửa — không đụng cấu trúc feature.
8. Rủi ro & Lưu ý
- BFF chưa có endpoint
/api/auth/casdoor→ chặn hard: cần backend trước khi test thật. - Facebook đăng nhập trên mobile web: một số tài khoản Facebook yêu cầu "Allow apps to use browser" — test nhiều tài khoản.
- Apple Sign-In: nếu dùng Casdoor provider thì không cần
expo-apple-authentication; bỏ stubsignInWithAppletrongfeatures/auth/api.ts:45-56để tránh nhầm lẫn. - PKCE: nếu BFF/Casdoor chưa hỗ trợ, tạm bỏ
usePKCEnhưng KHÔNG được đưaclient_secretvào app. statevalidation: luôn so khớpstatetrả về vớistateđã gửi để chống CSRF.- i18n: thêm text nút "Continue with Facebook/Apple" vào cả
en.json+vi.json(bắt buộc theo convention). expo-crypto: phải cài quanpx expo install expo-crypto(đúng version SDK 54), không dùng npm trực tiếp.
9. Checklist triển khai (App)
- Cài
expo-crypto(npx expo install expo-crypto) - Thêm biến
EXPO_PUBLIC_CASDOOR_*vàocore/config/env.ts+.env.example - Thêm
loginCasdoorBffvàoapi/services/member.service.ts - Thêm
loginWithCasdoorvàocore/auth/session.store.ts - Thêm
useCasdoorSignInvàofeatures/auth/hooks.ts - Tạo
features/auth/casdoor.ts(build authorize URL, PKCE,openAuthSessionAsync) - Sửa
LoginScreen.tsx: Google → Casdoor, thêm Facebook + Apple buttons - Đăng ký scheme
coocoapptrongapp.json(nếu chưa có) - Cập nhật i18n
en.json+vi.json - Test unit + test thủ công mock mode
- Chờ BFF
POST /api/auth/casdoor→ E2E Google/Facebook/Apple
10. Thứ tự triển khai (đề xuất)
- Tuần 1: Env + dependency + service + store + hook (Bước 1-4) → test unit.
- Tuần 2:
casdoor.ts+ UI buttons + deep link (Bước 5-7) → test mock. - Tuần 3: E2E với BFF thật, xử lý lỗi Casdoor (user từ chối, tài khoản trùng email, network).
- Tuần 4: Hoàn thiện i18n, edge cases (app bị kill khi đang xác thực, timeout), release checklist.