Skip to main content

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èm code → App gọi BFF POST /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_challenge sinh bằng SHA-256 (cần expo-crypto).

2. Yêu cầu Backend (BFF .NET — bên khác team)

#ViệcEndpoint / 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
2Endpoint nhận auth codePOST /api/auth/casdoor — body { code, codeVerifier?, redirectUri, provider }
3Token exchange với CasdoorBFF dùng code để gọi token endpoint Casdoor (/api/login/oauth/access_token) hoặc SDK Casdoor .NET
4Liên kết / tạo memberKhớ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)
5Trả về JWT CoocoResponse đúng định dạng hiện tại: { jwt_token, refreshToken, member_id, member_name, user }
6Refresh tokenGiữ 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.env.example:

BiếnVí dụGhi chú
EXPO_PUBLIC_CASDOOR_URLhttps://sso.cooco.example.comBase URL Casdoor
EXPO_PUBLIC_CASDOOR_CLIENT_IDabcd1234...Client ID của app Casdoor
EXPO_PUBLIC_CASDOOR_ORGcoocoOrganization trên Casdoor (thường nằm trong app name)
EXPO_PUBLIC_CASDOOR_APP_NAMEcooco-appApplication name Casdoor
EXPO_PUBLIC_CASDOOR_REDIRECT_URIcoocoapp://callbackRedirect (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 đưa client_secret vào App. Secret chỉ nằm ở BFF.


4. Dependencies (App)

PackageTrạng tháiCầ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.tsauthApi.signInWithCasdoor(...) (bỏ logic mock signInWithApple khi Apple chuyển qua Casdoor).

Bước 3 — Auth store: core/auth/session.store.ts

  • Thêm method loginWithCasdoor(payload)copy pattern loginWithGoogle (dòng 97-134):
    1. memberService.loginCasdoorBff({ code, codeVerifier, redirectUri, provider })
    2. Trích token: res.jwt_token || res.data?.jwt_token || res.token || res.data?.token + refreshToken
    3. tokenStorage.setTokens(...)
    4. memberService.getInfo()mapMemberToUser()setUser
    5. queryClient.setQueryData(queryKeys.profile, user)

Bước 4 — Hook React Query: features/auth/hooks.ts

  • Thêm useCasdoorSignIn() giống useGoogleSignIn (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-crypto
    extraParams: {
    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) → parse code + state từ 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ọi promptCasdoor('google') rồi useCasdoorSignIn().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)/recipes hoặc /(onboarding)/welcome.
  • app.json: đăng ký scheme "coocoapp" (nếu chưa có) — mục hiện chỉ có expo-web-browser plugin.
  • app/_layout.tsx:48 đã gọi WebBrowser.maybeCompleteAuthSession() — giữ nguyên.
  • Khi openAuthSessionAsync resolve, code về trong AuthSessionResult (không cần route riêng). File app/google-auth.tsx hiện tại có thể dùng cho Casdoor flow khi app bị kill (fallback): nhận code từ URL params → gọi useCasdoorSignIn().
  • 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ảngRedirect URI cần đăng ký với CasdoorCách hoạt động
Androidcoocoapp://callbackDeep link custom scheme; (tùy chọn) App Links HTTPS nếu cần
iOScoocoapp://callbackCustom scheme trong Info.plist (Expo tự sinh)
Web (Expo)https://cooco.example.com/callbackmakeRedirectUri dùng baseUrl /cooco từ app.config.js
  • app.config.js có sẵn logic xóa baseUrl trên iOS — không ảnh hưởng, deep link dùng scheme riêng.

7. Test & Kiểm thử

Giai đoạnCá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 ý

  1. BFF chưa có endpoint /api/auth/casdoor → chặn hard: cần backend trước khi test thật.
  2. 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.
  3. Apple Sign-In: nếu dùng Casdoor provider thì không cần expo-apple-authentication; bỏ stub signInWithApple trong features/auth/api.ts:45-56 để tránh nhầm lẫn.
  4. PKCE: nếu BFF/Casdoor chưa hỗ trợ, tạm bỏ usePKCE nhưng KHÔNG được đưa client_secret vào app.
  5. state validation: luôn so khớp state trả về với state đã gửi để chống CSRF.
  6. i18n: thêm text nút "Continue with Facebook/Apple" vào cả en.json + vi.json (bắt buộc theo convention).
  7. expo-crypto: phải cài qua npx 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ào core/config/env.ts + .env.example
  • Thêm loginCasdoorBff vào api/services/member.service.ts
  • Thêm loginWithCasdoor vào core/auth/session.store.ts
  • Thêm useCasdoorSignIn vào features/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 coocoapp trong app.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)

  1. Tuần 1: Env + dependency + service + store + hook (Bước 1-4) → test unit.
  2. Tuần 2: casdoor.ts + UI buttons + deep link (Bước 5-7) → test mock.
  3. 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).
  4. Tuần 4: Hoàn thiện i18n, edge cases (app bị kill khi đang xác thực, timeout), release checklist.