From 0a28c99aae270b68fdc1d26c7b59477d7457768a Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Fri, 7 Aug 2026 13:08:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(grok):=20=E7=AE=A1=E7=90=86=E7=AB=AF?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=20SSO/=E5=AF=86=E7=A0=81=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 sso-token 与 password API 封装及 composable 方法;buildCredentials 丢弃 sso/password 字段且不再强行固定 base_url,交由系统 CLI/API 模式选择主机。 --- frontend/src/api/admin/grok.ts | 41 +++++++++++- .../__tests__/useGrokOAuth.spec.ts | 19 ++++-- frontend/src/composables/useGrokOAuth.ts | 65 ++++++++++++++++++- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/admin/grok.ts b/frontend/src/api/admin/grok.ts index f59c05c1d7..0ae181365e 100644 --- a/frontend/src/api/admin/grok.ts +++ b/frontend/src/api/admin/grok.ts @@ -170,4 +170,43 @@ export async function createFromSSO(payload: GrokSSOToOAuthRequest): Promise { + const payload: Record = { sso_token: ssoToken } + if (proxyId) payload.proxy_id = proxyId + const { data } = await apiClient.post('/admin/grok/oauth/sso-token', payload) + return data +} + +/** + * Password login → ephemeral SSO → Build OAuth. + * Password is only sent over the wire for this call; never persist it in credentials. + */ +export async function authorizePassword( + emailAndPassword: string, + proxyId?: number | null +): Promise { + // Format: email----password (password may contain dashes). + const sep = '----' + const idx = emailAndPassword.indexOf(sep) + const email = (idx >= 0 ? emailAndPassword.slice(0, idx) : emailAndPassword).trim() + const password = idx >= 0 ? emailAndPassword.slice(idx + sep.length) : '' + const payload: Record = { email, password } + if (proxyId) payload.proxy_id = proxyId + const { data } = await apiClient.post('/admin/grok/oauth/password', payload) + return data +} + +export default { + generateAuthUrl, + exchangeCode, + refreshGrokToken, + queryQuota, + resetQuota, + createFromSSO, + validateSSOToken, + authorizePassword, +} diff --git a/frontend/src/composables/__tests__/useGrokOAuth.spec.ts b/frontend/src/composables/__tests__/useGrokOAuth.spec.ts index 958a71e67c..30c7c00944 100644 --- a/frontend/src/composables/__tests__/useGrokOAuth.spec.ts +++ b/frontend/src/composables/__tests__/useGrokOAuth.spec.ts @@ -55,7 +55,7 @@ describe('useGrokOAuth.exchangeAuthCode', () => { }) describe('useGrokOAuth.buildCredentials', () => { - it('persists the Grok CLI subscription proxy for OAuth inference', () => { + it('builds OAuth credentials without forcing base_url or leaking sso/password', () => { const oauth = useGrokOAuth() const credentials = oauth.buildCredentials({ @@ -64,9 +64,20 @@ describe('useGrokOAuth.buildCredentials', () => { expires_at: 1_900_000_000, client_id: 'client-id', scope: 'openid grok-cli:access', - email: 'grok@example.com' - }) + email: 'grok@example.com', + password: 'super-secret', + sso_token: 'sso-cookie', + sso: 'sso-cookie', + 'sso-rw': 'sso-cookie' + } as any) - expect(credentials.base_url).toBe('https://cli-chat-proxy.grok.com/v1') + expect(credentials.access_token).toBe('access-token') + expect(credentials.email).toBe('grok@example.com') + // System/CLI mode chooses the correct host; do not pin public API URL. + expect(credentials.base_url).toBeUndefined() + expect(credentials).not.toHaveProperty('password') + expect(credentials).not.toHaveProperty('sso_token') + expect(credentials).not.toHaveProperty('sso') + expect(credentials).not.toHaveProperty('sso-rw') }) }) diff --git a/frontend/src/composables/useGrokOAuth.ts b/frontend/src/composables/useGrokOAuth.ts index 0e9f8b7e91..5f593b8b26 100644 --- a/frontend/src/composables/useGrokOAuth.ts +++ b/frontend/src/composables/useGrokOAuth.ts @@ -113,6 +113,8 @@ export function useGrokOAuth() { } } + // Build account credentials for create/re-auth. Never persist raw SSO cookies + // or passwords: those exist only for the one-shot authorize API call. const buildCredentials = (tokenInfo: GrokTokenInfo): Record => { const credentials: Record = { access_token: tokenInfo.access_token, @@ -125,11 +127,16 @@ export function useGrokOAuth() { team_id: tokenInfo.team_id, subscription_tier: tokenInfo.subscription_tier, entitlement_status: tokenInfo.entitlement_status, - base_url: 'https://cli-chat-proxy.grok.com/v1' + // Leave base_url unset so system/CLI mode can choose the correct host. } if (tokenInfo.refresh_token) credentials.refresh_token = tokenInfo.refresh_token if (tokenInfo.id_token) credentials.id_token = tokenInfo.id_token - return Object.fromEntries(Object.entries(credentials).filter(([, value]) => value !== undefined && value !== '')) + const blocked = new Set(['sso_token', 'password', 'sso', 'sso-rw']) + return Object.fromEntries( + Object.entries(credentials).filter( + ([key, value]) => !blocked.has(key) && value !== undefined && value !== '' + ) + ) } const buildExtraInfo = (tokenInfo: GrokTokenInfo): Record => { @@ -140,6 +147,58 @@ export function useGrokOAuth() { return extra } + const validateSSOToken = async ( + ssoToken: string, + proxyId?: number | null + ): Promise => { + if (!ssoToken.trim()) { + error.value = t('admin.accounts.oauth.grok.pleaseEnterSSOToken', 'Please enter an SSO token') + return null + } + loading.value = true + error.value = '' + try { + return await adminAPI.grok.validateSSOToken(ssoToken.trim(), proxyId) + } catch (err: any) { + error.value = extractI18nErrorMessage( + err, + t, + 'admin.accounts.oauth.grok.errors', + t('admin.accounts.oauth.grok.failedToValidateSSO', 'Failed to validate SSO token') + ) + appStore.showError(error.value) + return null + } finally { + loading.value = false + } + } + + const authorizePassword = async ( + emailAndPassword: string, + proxyId?: number | null + ): Promise => { + if (!emailAndPassword.trim()) { + error.value = t('admin.accounts.oauth.grok.pleaseEnterPassword', 'Please enter email----password') + return null + } + loading.value = true + error.value = '' + try { + return await adminAPI.grok.authorizePassword(emailAndPassword, proxyId) + } catch (err: any) { + error.value = extractI18nErrorMessage( + err, + t, + 'admin.accounts.oauth.grok.errors', + t('admin.accounts.oauth.grok.failedToAuthorizePassword', 'Password authorization failed') + ) + appStore.showError(error.value) + return null + } finally { + loading.value = false + } + } + return { authUrl, sessionId, @@ -150,6 +209,8 @@ export function useGrokOAuth() { generateAuthUrl, exchangeAuthCode, validateRefreshToken, + validateSSOToken, + authorizePassword, buildCredentials, buildExtraInfo }