From 53c27c4efcd72a4519f4c4ff9e2d707cd5ad6ee1 Mon Sep 17 00:00:00 2001 From: phuongdm Date: Tue, 7 Apr 2026 13:20:46 +0700 Subject: [PATCH] add oauth login --- SSO-OAuth-OIDC.md | 343 ++++++++++++++++++ src/components/forms/login-form.tsx | 32 +- src/config/api.ts | 3 + src/hooks/queries/useAuthQueries.ts | 13 +- src/routeTree.gen.ts | 34 +- .../(auth)/{sso => oauth}/callback/index.tsx | 76 +++- src/services/auth.service.ts | 56 ++- tsconfig.app.json | 1 + 8 files changed, 501 insertions(+), 57 deletions(-) create mode 100644 SSO-OAuth-OIDC.md rename src/routes/(auth)/{sso => oauth}/callback/index.tsx (55%) diff --git a/SSO-OAuth-OIDC.md b/SSO-OAuth-OIDC.md new file mode 100644 index 0000000..0d7f986 --- /dev/null +++ b/SSO-OAuth-OIDC.md @@ -0,0 +1,343 @@ +# Google OAuth (OIDC) + AzureAD Shared SSO Service + +Tai lieu nay tong hop toan bo flow dang nhap OAuth/OIDC theo muc coding cho codebase hien tai. + +Pham vi: +- Giu nguyen login username/password cu. +- Dung chung 1 service cho AzureAD SSO va Google OAuth OIDC. +- Van tao/cap nhat user noi bo trong DB. +- User moi vao role Pending (khong co permission), doi admin cap quyen. + +---------------------------------------- + +## 1) Trang thai implementation hien tai + +Da hoan tat trong code: +- Shared service theo provider key: SsoService. +- Controller da provider: OAuthController. +- Controller AzureAD cu (SsoController) da chay tren shared service. +- Config da provider qua OAuthProviders trong appsettings. + +File chinh: +- TTMT.CompManageWeb/Program.cs +- TTMT.CompManageWeb/Interfaces/ISsoService.cs +- TTMT.CompManageWeb/Services/SsoSerivce.cs +- TTMT.CompManageWeb/Controllers/OAuthController.cs +- TTMT.CompManageWeb/Controllers/SsoController.cs +- TTMT.CompManageWeb/Dtos/Auth/OAuthProviderOptions.cs +- TTMT.CompManageWeb/Dtos/Auth/OAuthProvidersOptions.cs +- TTMT.CompManageWeb/appsettings.json + +---------------------------------------- + +## 2) Kien truc luong dang nhap + +### 2.1 Luong OAuth Google + +1. FE redirect user den: + - GET /api/auth/oauth/google/login?returnUrl= +2. Backend build authorize URL va redirect qua Google. +3. Google callback ve backend: + - GET /api/auth/oauth/google/callback?code=...&state=... +4. Backend: + - Exchange code -> token endpoint. + - Validate id_token. + - Lay email/name. + - Kiem tra domain. + - Upsert user noi bo. + - Issue JWT noi bo. + - Tao one-time code (2 phut). + - Redirect ve returnUrl?code=. +5. FE goi API exchange: + - POST /api/auth/oauth/exchange + - Body: { "code": "..." } +6. Backend consume one-time code va tra payload login (token + role + permission). + +### 2.2 Luong AzureAD cu + +Van giu endpoint cu: +- GET /api/auth/sso/login +- GET /api/auth/sso/callback +- POST /api/auth/sso/exchange + +Nhung ben trong da dung chung service theo provider azuread. + +---------------------------------------- + +## 3) Cau hinh bat buoc + +### 3.1 appsettings.json + +```json +{ + "OAuthProviders": { + "DefaultProvider": "azuread", + "Providers": { + "google": { + "Authority": "https://accounts.google.com", + "AuthorizationEndpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "TokenEndpoint": "https://oauth2.googleapis.com/token", + "ClientId": "", + "ClientSecret": "", + "CallbackPath": "/api/auth/oauth/google/callback", + "AllowedDomain": "", + "PendingRoleName": "Pending", + "Scopes": "openid profile email", + "Issuer": "https://accounts.google.com", + "EmailClaim": "email", + "NameClaim": "name", + "HostedDomainClaimName": "hd", + "EnforceHostedDomainClaim": true + } + } + } +} +``` + +Giai thich nhanh: +- AllowedDomain: + - Rong: cho phep moi domain. + - Co gia tri (vd hust.edu.vn): chi cho email domain nay. +- EnforceHostedDomainClaim=true: + - Neu co claim hd trong token thi bat buoc phai khop AllowedDomain. + - Neu token khong co hd, he thong fallback check theo email domain. +- PendingRoleName: role duoc gan khi user moi dang nhap lan dau. + +### 3.2 Google Cloud Console + +1. Tao OAuth client (Web application). +2. Authorized redirect URIs: + - https:///api/auth/oauth/google/callback + - (dev) https://localhost:/api/auth/oauth/google/callback +3. Lay ClientId, ClientSecret va dien vao appsettings (hoac secrets). + +---------------------------------------- + +## 4) Dang ky DI va options + +Trong Program.cs can co: + +```csharp +builder.Services.Configure( + builder.Configuration.GetSection(AzureAdOptions.SectionName)); + +builder.Services.Configure( + builder.Configuration.GetSection(OAuthProvidersOptions.SectionName)); + +builder.Services.AddScoped(); +``` + +OAuthProvidersOptions dung dictionary provider theo key (vd google, azuread, okta). + +---------------------------------------- + +## 5) Thiet ke interface va service chung + +### 5.1 Interface ISsoService + +Interface da duoc mo rong thanh provider-aware: +- BuildAuthorizeUrl(provider, redirectUri, state) +- ExchangeCodeAsync(provider, code, redirectUri, ct) +- ValidateIdTokenAsync(provider, idToken, ct) +- IsAllowedDomain(provider, email, principal) +- UpsertUserAsync(provider, email, name, ct) + +Va van giu overload cu de backward compatibility. + +### 5.2 SsoService - logic tong + +Core y tuong: +- Resolve config theo provider key. +- Provider nao khong co endpoint explicit thi suy ra tu metadata/authority. +- AzureAD duoc fallback tu AzureAdOptions de khong vo luong cu. + +Phan quan trong: + +1) ResolveProvider(...) +- Doc provider trong OAuthProviders. +- Neu key = azuread ma khong co trong OAuthProviders, fallback AzureAdOptions. + +2) BuildAuthorizeUrl(...) +- Build URL authorize theo endpoint cua provider. +- Them scope, state, redirect_uri. +- Neu bat hosted-domain check thi them hd=. + +3) ExchangeCodeAsync(...) +- Goi token endpoint voi client_id, client_secret, code, redirect_uri. +- Parse OidcTokenResponse. +- Bat buoc co id_token (flow hien tai dang OIDC-centric). + +4) ValidateIdTokenAsync(...) +- Lay metadata OIDC (.well-known/openid-configuration). +- Validate issuer, audience, signing key, lifetime. + +5) IsAllowedDomain(...) +- Neu AllowedDomain rong -> cho phep. +- Neu khong rong -> check duoi email. +- Neu EnforceHostedDomainClaim bat va token co claim hd -> bat buoc hd trung AllowedDomain. + +6) UpsertUserAsync(...) +- Tim user theo email (UserName). +- Neu ton tai: cap nhat ten + metadata update. +- Neu chua ton tai: tao user moi, Password = null, gan role PendingRoleName. + +7) One-time code +- CreateOneTimeCodeAsync(...): tao code, luu bang SsoOneTimeCodes, het han sau 2 phut. +- ExchangeOneTimeCodeForLoginAsync(...): consume code, tra payload login final. + +---------------------------------------- + +## 6) Controller va contract API + +### 6.1 OAuthController (da provider) + +Route base: api/auth/oauth + +1) Login +- GET /api/auth/oauth/{provider}/login?returnUrl=... +- Redirect sang provider authorize URL. + +2) Callback +- GET /api/auth/oauth/{provider}/callback?code=...&state=... +- Xu ly token + domain + upsert + issue token + one-time code. +- Redirect ve FE voi query code. + +3) Exchange +- POST /api/auth/oauth/exchange + +Request: +```json +{ + "code": "one-time-code" +} +``` + +Response thanh cong: +```json +{ + "token": "", + "name": "...", + "username": "email@domain", + "access": [1, 2, 3], + "role": { + "roleName": "Pending", + "priority": 99 + } +} +``` + +### 6.2 SsoController (AzureAD legacy) + +Van giu route cu, nhung da goi service chung voi provider azuread. + +---------------------------------------- + +## 7) Rule user va role Pending + +Rule bat buoc: +- User OAuth lan dau phai duoc tao trong bang UserAccounts. +- User moi phai vao role Pending. +- Role Pending khong co permission nao (PermissionRoles.IsChecked = 0 hoac khong co row). +- Admin se cap role/permission sau. + +Model lien quan: +- UserAccounts +- Roles +- PermissionRoles +- SsoOneTimeCodes + +Kiem tra nhanh trong DB: + +```sql +-- 1) Kiem tra role Pending co ton tai +SELECT Id, RoleName, Priority +FROM "Roles" +WHERE "RoleName" = 'Pending'; + +-- 2) Kiem tra Pending role khong co permission active +SELECT pr.* +FROM "PermissionRoles" pr +JOIN "Roles" r ON r."Id" = pr."RoleId" +WHERE r."RoleName" = 'Pending' + AND pr."IsChecked" = 1; + +-- 3) Kiem tra user tao boi OAuth +SELECT "Id", "UserName", "Password", "RoleId", "CreatedBy", "UpdatedBy" +FROM "UserAccounts" +WHERE "UserName" = ''; +``` + +Ky vong: +- Query #2 tra ve 0 rows. +- User moi co Password = null, CreatedBy = 'SSO'. + +---------------------------------------- + +## 8) Frontend integration (chi tiet) + +1. Nguoi dung bam nut Google Login: + - Window.location -> GET /api/auth/oauth/google/login?returnUrl= +2. Sau callback backend, FE nhan code tu query string. +3. FE goi: + +```http +POST /api/auth/oauth/exchange +Content-Type: application/json + +{ "code": "" } +``` + +4. FE luu token va xu ly permission/role giong login cu. + +---------------------------------------- + +## 9) Luu y bao mat va hardening + +Nen lam tiep: +- Validate returnUrl theo allowlist de tranh open redirect. +- Luu va verify state/nonce server-side (cache/redis). +- Dung IHttpClientFactory thay new HttpClient() de kiem soat timeout/retry. +- Dua ClientSecret sang secret manager/env var. +- Bat HTTPS va secure cookie policy day du. + +---------------------------------------- + +## 10) Troubleshooting + +1) Loi IdToken not found in token response +- Provider dang khong tra OIDC token. +- Kiem tra scope co openid chua. + +2) Loi Email not found in token +- Kiem tra claim trong Google token (email, preferred_username). + +3) Loi Email domain is not allowed +- Check AllowedDomain va claim hd. + +4) Loi Role 'Pending' not found +- Tao role Pending trong bang Roles. + +5) Loi exchange code het han +- One-time code chi song 2 phut va chi dung 1 lan. + +---------------------------------------- + +## 11) Checklist test E2E truoc khi merge + +- Login Google thanh cong voi account hop le. +- User moi duoc tao trong DB va role = Pending. +- Pending role khong co permission active. +- User cu login lai thi khong tao duplicate user. +- One-time code chi dung 1 lan. +- AllowedDomain rong cho phep tat ca domain. +- AllowedDomain co gia tri thi chan dung domain. + +---------------------------------------- + +## 12) Ghi chu pham vi hien tai + +Hien tai flow moi dang OIDC-centric (bat buoc id_token). +Neu can support OAuth thuan (khong co id_token), can bo sung nhanh: +- UserInfo endpoint call. +- Mapping email/name tu userinfo response. +- Branch logic trong callback de fallback userinfo. diff --git a/src/components/forms/login-form.tsx b/src/components/forms/login-form.tsx index d90e6b7..57679aa 100644 --- a/src/components/forms/login-form.tsx +++ b/src/components/forms/login-form.tsx @@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import type { LoginResquest } from "@/types/auth"; import { useMutation } from "@tanstack/react-query"; -import { buildSsoLoginUrl, login } from "@/services/auth.service"; +import { buildGoogleOAuthLoginUrl, login } from "@/services/auth.service"; import { useState } from "react"; import { useNavigate, useRouter } from "@tanstack/react-router"; import { Route } from "@/routes/(auth)/login"; @@ -44,12 +44,12 @@ export function LoginForm({ className }: React.ComponentProps<"form">) { } }); - const handleSsoLogin = () => { - const returnUrl = new URL("/sso/callback", window.location.origin); + const handleGoogleLogin = () => { + const returnUrl = new URL("/oauth/callback", window.location.origin); if (search.redirect) { returnUrl.searchParams.set("redirect", search.redirect); } - window.location.assign(buildSsoLoginUrl(returnUrl.toString())); + window.location.assign(buildGoogleOAuthLoginUrl(returnUrl.toString())); }; const handleSubmit = (e: React.FormEvent) => { @@ -112,14 +112,26 @@ export function LoginForm({ className }: React.ComponentProps<"form">) { )}
Hoặc
- diff --git a/src/config/api.ts b/src/config/api.ts index 0d8dc78..ef855c1 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -11,6 +11,9 @@ export const BASE_MESH_URL = isDev export const API_ENDPOINTS = { AUTH: { LOGIN: `${BASE_URL}/login`, + OAUTH_LOGIN: (provider: string) => + `${BASE_URL}/auth/oauth/${encodeURIComponent(provider)}/login`, + OAUTH_EXCHANGE: `${BASE_URL}/auth/oauth/exchange`, SSO_LOGIN: `${BASE_URL}/auth/sso/login`, SSO_EXCHANGE: `${BASE_URL}/auth/sso/exchange`, LOGOUT: `${BASE_URL}/logout`, diff --git a/src/hooks/queries/useAuthQueries.ts b/src/hooks/queries/useAuthQueries.ts index e4ffb1e..595f32b 100644 --- a/src/hooks/queries/useAuthQueries.ts +++ b/src/hooks/queries/useAuthQueries.ts @@ -115,10 +115,17 @@ export function useCreateAccount() { } /** - * Hook để đổi one-time code SSO lấy payload đăng nhập + * Hook để đổi one-time OAuth code lấy payload đăng nhập */ -export function useExchangeSsoCode() { +export function useExchangeOAuthCode() { return useMutation({ - mutationFn: (code) => authService.exchangeSsoCode(code), + mutationFn: (code) => authService.exchangeOAuthCode(code), }); } + +/** + * Legacy alias for backward compatibility. + */ +export function useExchangeSsoCode() { + return useExchangeOAuthCode(); +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 8c9d499..37decc9 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -28,7 +28,7 @@ import { Route as AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms import { Route as AuthRoleCreateIndexRouteImport } from './routes/_auth/role/create/index' import { Route as AuthProfileChangePasswordIndexRouteImport } from './routes/_auth/profile/change-password/index' import { Route as AuthProfileUserNameIndexRouteImport } from './routes/_auth/profile/$userName/index' -import { Route as authSsoCallbackIndexRouteImport } from './routes/(auth)/sso/callback/index' +import { Route as authOauthCallbackIndexRouteImport } from './routes/(auth)/oauth/callback/index' import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index' import { Route as AuthUserEditUserNameIndexRouteImport } from './routes/_auth/user/edit/$userName/index' import { Route as AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index' @@ -132,9 +132,9 @@ const AuthProfileUserNameIndexRoute = path: '/profile/$userName/', getParentRoute: () => AuthRoute, } as any) -const authSsoCallbackIndexRoute = authSsoCallbackIndexRouteImport.update({ - id: '/(auth)/sso/callback/', - path: '/sso/callback/', +const authOauthCallbackIndexRoute = authOauthCallbackIndexRouteImport.update({ + id: '/(auth)/oauth/callback/', + path: '/oauth/callback/', getParentRoute: () => rootRouteImport, } as any) const AuthUserRoleRoleIdIndexRoute = AuthUserRoleRoleIdIndexRouteImport.update({ @@ -186,7 +186,7 @@ export interface FileRoutesByFullPath { '/role': typeof AuthRoleIndexRoute '/rooms': typeof AuthRoomsIndexRoute '/user': typeof AuthUserIndexRoute - '/sso/callback': typeof authSsoCallbackIndexRoute + '/oauth/callback': typeof authOauthCallbackIndexRoute '/profile/$userName': typeof AuthProfileUserNameIndexRoute '/profile/change-password': typeof AuthProfileChangePasswordIndexRoute '/role/create': typeof AuthRoleCreateIndexRoute @@ -213,7 +213,7 @@ export interface FileRoutesByTo { '/role': typeof AuthRoleIndexRoute '/rooms': typeof AuthRoomsIndexRoute '/user': typeof AuthUserIndexRoute - '/sso/callback': typeof authSsoCallbackIndexRoute + '/oauth/callback': typeof authOauthCallbackIndexRoute '/profile/$userName': typeof AuthProfileUserNameIndexRoute '/profile/change-password': typeof AuthProfileChangePasswordIndexRoute '/role/create': typeof AuthRoleCreateIndexRoute @@ -242,7 +242,7 @@ export interface FileRoutesById { '/_auth/role/': typeof AuthRoleIndexRoute '/_auth/rooms/': typeof AuthRoomsIndexRoute '/_auth/user/': typeof AuthUserIndexRoute - '/(auth)/sso/callback/': typeof authSsoCallbackIndexRoute + '/(auth)/oauth/callback/': typeof authOauthCallbackIndexRoute '/_auth/profile/$userName/': typeof AuthProfileUserNameIndexRoute '/_auth/profile/change-password/': typeof AuthProfileChangePasswordIndexRoute '/_auth/role/create/': typeof AuthRoleCreateIndexRoute @@ -271,7 +271,7 @@ export interface FileRouteTypes { | '/role' | '/rooms' | '/user' - | '/sso/callback' + | '/oauth/callback' | '/profile/$userName' | '/profile/change-password' | '/role/create' @@ -298,7 +298,7 @@ export interface FileRouteTypes { | '/role' | '/rooms' | '/user' - | '/sso/callback' + | '/oauth/callback' | '/profile/$userName' | '/profile/change-password' | '/role/create' @@ -326,7 +326,7 @@ export interface FileRouteTypes { | '/_auth/role/' | '/_auth/rooms/' | '/_auth/user/' - | '/(auth)/sso/callback/' + | '/(auth)/oauth/callback/' | '/_auth/profile/$userName/' | '/_auth/profile/change-password/' | '/_auth/role/create/' @@ -344,7 +344,7 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute AuthRoute: typeof AuthRouteWithChildren authLoginIndexRoute: typeof authLoginIndexRoute - authSsoCallbackIndexRoute: typeof authSsoCallbackIndexRoute + authOauthCallbackIndexRoute: typeof authOauthCallbackIndexRoute } declare module '@tanstack/react-router' { @@ -482,11 +482,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthProfileUserNameIndexRouteImport parentRoute: typeof AuthRoute } - '/(auth)/sso/callback/': { - id: '/(auth)/sso/callback/' - path: '/sso/callback' - fullPath: '/sso/callback' - preLoaderRoute: typeof authSsoCallbackIndexRouteImport + '/(auth)/oauth/callback/': { + id: '/(auth)/oauth/callback/' + path: '/oauth/callback' + fullPath: '/oauth/callback' + preLoaderRoute: typeof authOauthCallbackIndexRouteImport parentRoute: typeof rootRouteImport } '/_auth/user/role/$roleId/': { @@ -592,7 +592,7 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AuthRoute: AuthRouteWithChildren, authLoginIndexRoute: authLoginIndexRoute, - authSsoCallbackIndexRoute: authSsoCallbackIndexRoute, + authOauthCallbackIndexRoute: authOauthCallbackIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/(auth)/sso/callback/index.tsx b/src/routes/(auth)/oauth/callback/index.tsx similarity index 55% rename from src/routes/(auth)/sso/callback/index.tsx rename to src/routes/(auth)/oauth/callback/index.tsx index 682f14d..b8750c5 100644 --- a/src/routes/(auth)/sso/callback/index.tsx +++ b/src/routes/(auth)/oauth/callback/index.tsx @@ -1,36 +1,60 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useEffect, useState } from "react"; -import { useExchangeSsoCode } from "@/hooks/queries"; import { useAuth } from "@/hooks/useAuth"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { LoaderCircle } from "lucide-react"; +import axios from "axios"; +import { exchangeOAuthCode } from "@/services/auth.service"; +import type { LoginResponse } from "@/types/auth"; -export const Route = createFileRoute("/(auth)/sso/callback/")({ - component: SsoCallbackPage, +const inFlightExchanges = new Map>(); +const consumedCodes = new Set(); + +export const Route = createFileRoute("/(auth)/oauth/callback/")({ + component: OAuthCallbackPage, }); -function SsoCallbackPage() { +function OAuthCallbackPage() { const auth = useAuth(); const navigate = useNavigate(); - const exchangeMutation = useExchangeSsoCode(); const search = Route.useSearch() as { code?: string; redirect?: string }; const [errorMessage, setErrorMessage] = useState(null); + const [isExchanging, setIsExchanging] = useState(false); useEffect(() => { - if (!search.code) { - setErrorMessage("SSO code is missing."); + const code = search.code; + if (!code) { + setErrorMessage("OAuth code is missing."); + return; + } + + if (consumedCodes.has(code)) { + setErrorMessage("Mã đăng nhập đã được sử dụng. Vui lòng đăng nhập lại."); return; } setErrorMessage(null); - exchangeMutation.mutate(search.code, { - onSuccess: async (data) => { + setIsExchanging(true); + + let cancelled = false; + let exchangePromise = inFlightExchanges.get(code); + if (!exchangePromise) { + exchangePromise = exchangeOAuthCode(code); + inFlightExchanges.set(code, exchangePromise); + } + + exchangePromise + .then(async (data) => { + if (cancelled) return; + if (!data.token) { - setErrorMessage("SSO response missing token."); + setErrorMessage("OAuth response missing token."); return; } + consumedCodes.add(code); + localStorage.setItem("token", data.token); localStorage.setItem("username", data.username || ""); localStorage.setItem("name", data.name || ""); @@ -45,22 +69,38 @@ function SsoCallbackPage() { auth.login(data.username || ""); await navigate({ to: search.redirect || "/dashboard" }); - }, - onError: () => { - setErrorMessage("SSO exchange failed."); - }, - }); - }, [auth, exchangeMutation, navigate, search.code, search.redirect]); + }) + .catch((error) => { + if (cancelled) return; + + if (axios.isAxiosError(error) && error.response?.status === 401) { + consumedCodes.add(code); + setErrorMessage("Mã đăng nhập đã hết hạn hoặc đã được sử dụng. Vui lòng đăng nhập lại."); + return; + } + setErrorMessage("OAuth exchange failed."); + }) + .finally(() => { + inFlightExchanges.delete(code); + if (!cancelled) { + setIsExchanging(false); + } + }); + + return () => { + cancelled = true; + }; + }, [auth, navigate, search.code, search.redirect]); return (
- Đang xác thực SSO + Đang xác thực OAuth Vui lòng đợi trong giây lát. - {exchangeMutation.isPending && ( + {isExchanging && (
Đang trao đổi mã đăng nhập diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 7800234..8988992 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,5 +1,6 @@ import axios from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; +import rawAxios from "axios"; import type { LoginResquest, LoginResponse, CreateAccountRequest } from "@/types/auth"; /** @@ -16,25 +17,62 @@ export async function login(credentials: LoginResquest): Promise } /** - * Build SSO login URL + * Build OAuth login URL by provider + * @param provider - OAuth provider key (e.g. google, azuread) * @param returnUrl - FE callback url */ -export function buildSsoLoginUrl(returnUrl: string): string { - const base = API_ENDPOINTS.AUTH.SSO_LOGIN; +export function buildOAuthLoginUrl(provider: string, returnUrl: string): string { + const base = API_ENDPOINTS.AUTH.OAUTH_LOGIN(provider); const encoded = encodeURIComponent(returnUrl); return `${base}?returnUrl=${encoded}`; } /** - * Exchange one-time code for login payload + * Build Google OAuth login URL + * @param returnUrl - FE callback url + */ +export function buildGoogleOAuthLoginUrl(returnUrl: string): string { + return buildOAuthLoginUrl("google", returnUrl); +} + +/** + * Exchange one-time OAuth code for login payload * @param code - one-time code */ +export async function exchangeOAuthCode(code: string): Promise { + try { + const response = await rawAxios.post( + API_ENDPOINTS.AUTH.OAUTH_EXCHANGE, + { code } + ); + return response.data; + } catch (error) { + if (rawAxios.isAxiosError(error)) { + const status = error.response?.status; + if (status === 401 || status === 404 || status === 405) { + const fallbackResponse = await rawAxios.post( + API_ENDPOINTS.AUTH.SSO_EXCHANGE, + { code } + ); + return fallbackResponse.data; + } + } + throw error; + } +} + +/** + * Legacy AzureAD SSO URL builder kept for backward compatibility. + */ +export function buildSsoLoginUrl(returnUrl: string): string { + return buildOAuthLoginUrl("azuread", returnUrl); +} + +/** + * Legacy SSO exchange alias kept for backward compatibility. + */ export async function exchangeSsoCode(code: string): Promise { - const response = await axios.post( - API_ENDPOINTS.AUTH.SSO_EXCHANGE, - { code } - ); - return response.data; + return exchangeOAuthCode(code); } /** diff --git a/tsconfig.app.json b/tsconfig.app.json index 0034770..6d0c62c 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,6 +4,7 @@ "outDir": "./dist", "composite": true, "noEmit": false, + "ignoreDeprecations": "6.0", "types": [] }, "include": ["src/**/*.ts", "src/**/*.tsx"],