fix(grok): tighten OAuth SSO flow and hide password login

Require oauth state/redirect consistency, fail closed on missing proxy,
and remove password login from create/reauth UI (admin-only password path stays off by default).
This commit is contained in:
IanShaw027
2026-08-08 01:07:19 +08:00
parent d0767eab9d
commit 6d632eec45
13 changed files with 153 additions and 121 deletions
@@ -3212,7 +3212,7 @@
:show-agent-identity-option="form.platform === 'openai'"
:show-codex-pat-option="form.platform === 'openai'"
:show-sso-option="form.platform === 'grok'"
:show-email-password-option="form.platform === 'grok'"
:show-email-password-option="false"
:show-manual-option="true"
:initial-input-method="'manual'"
:platform="form.platform"
@@ -3316,8 +3316,8 @@
<BaseDialog
:show="showGeminiHelpDialog"
:title="t('admin.accounts.gemini.helpDialog.title')"
width="wide"
@close="showGeminiHelpDialog = false"
max-width="max-w-3xl"
>
<div class="space-y-6">
<!-- Setup Guide Section -->
@@ -24,18 +24,12 @@ describe('CreateAccountModal Grok account types', () => {
})
it('validates and applies upstream config on Grok OAuth create paths', () => {
// 授权码兑换 / RT 批量 / SSO 批量 / 密码登录(4 条路径)
expect(source.match(/validateGrokOAuthUpstreamConfig\(\)/g)?.length).toBeGreaterThanOrEqual(4)
expect(source.match(/applyGrokOAuthUpstreamConfig\(credentials\)/g)?.length).toBeGreaterThanOrEqual(4)
// 授权码兑换 / RT 批量 / SSO 批量(密码授权已隐藏)
expect(source.match(/validateGrokOAuthUpstreamConfig\(\)/g)?.length).toBeGreaterThanOrEqual(3)
expect(source.match(/applyGrokOAuthUpstreamConfig\(credentials\)/g)?.length).toBeGreaterThanOrEqual(3)
})
it('wires Grok password authorize path without storing password/SSO fields', () => {
expect(source).toContain('show-email-password-option')
expect(source).toContain('@authorize-password="handleGrokAuthorizePassword"')
expect(source).toContain('handleGrokAuthorizePassword')
expect(source).toContain('grokOAuth.authorizePassword')
expect(source).toContain('grokOAuth.buildCredentials')
// Password only for authorize call; credentials come from buildCredentials
expect(source).toContain('email----password')
it('hides Grok password authorize option in the create flow', () => {
expect(source).toContain(':show-email-password-option="false"')
})
})
@@ -132,18 +132,16 @@
:show-cookie-option="isAnthropic"
:show-refresh-token-option="isOpenAI || isAntigravity || isGrok"
:show-sso-option="isGrok"
:show-email-password-option="isGrok"
:show-email-password-option="false"
:allow-multiple="false"
:method-label="t('admin.accounts.inputMethod')"
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : isGrok ? 'grok' : 'anthropic'"
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
:initial-input-method="grokInitialInputMethod"
:initial-email-password="grokPrefillEmailPassword"
@generate-url="handleGenerateUrl"
@cookie-auth="handleCookieAuth"
@validate-refresh-token="handleGrokValidateRefreshToken"
@import-sso="handleGrokImportSSO"
@authorize-password="handleGrokAuthorizePassword"
/>
</div>
@@ -257,38 +255,19 @@ const isAnthropic = computed(() => props.account?.platform === 'anthropic')
const isAntigravity = computed(() => props.account?.platform === 'antigravity')
const isGrok = computed(() => props.account?.platform === 'grok')
/** Stored Grok email for reauth prefill (password is never stored). */
const grokAccountEmail = computed(() => {
if (!isGrok.value || !props.account) return ''
const creds = (props.account.credentials || {}) as Record<string, unknown>
const email = typeof creds.email === 'string' ? creds.email.trim() : ''
return email
})
/**
* Prefill "email----" so the operator only types the password.
* Empty when no email is known (full email----password required).
*/
const grokPrefillEmailPassword = computed(() => {
const email = grokAccountEmail.value
return email ? `${email}----` : ''
})
/**
* Grok reauth default tab:
* - password first when we know the email (common reauth path)
* - refresh_token when email unknown but RT may still work
* - email_password otherwise
* Grok reauth default tab (password auth is hidden):
* - refresh_token when RT may still work
* - SSO cookie otherwise
*/
const grokInitialInputMethod = computed<AuthInputMethod>(() => {
if (!isGrok.value) return 'manual'
if (grokAccountEmail.value) return 'email_password'
const creds = (props.account?.credentials || {}) as Record<string, unknown>
const hasRT =
(typeof creds.refresh_token === 'string' && creds.refresh_token.trim() !== '') ||
(typeof creds.has_refresh_token === 'boolean' && creds.has_refresh_token)
if (hasRT) return 'refresh_token'
return 'email_password'
return 'sso_cookie'
})
// Computed - current OAuth state based on platform
@@ -673,38 +652,6 @@ const handleGrokImportSSO = async (ssoInput: string) => {
}
}
/** Re-auth with email----password (single line; password never persisted). */
const handleGrokAuthorizePassword = async (emailPasswordInput: string) => {
if (!props.account || !isGrok.value) return
const line = emailPasswordInput
.split('\n')
// Email is normalized in the API helper, but password whitespace is valid.
.find((item) => item.trim() && item.includes('----'))
if (!line) {
grokOAuth.error.value = t(
'admin.accounts.oauth.grok.pleaseEnterPassword',
'Please enter email----password'
)
return
}
grokOAuth.loading.value = true
grokOAuth.error.value = ''
try {
const tokenInfo = await grokOAuth.authorizePassword(line, props.account.proxy_id)
if (!tokenInfo) return
await applyGrokReauthTokenInfo(tokenInfo)
} catch (error: any) {
grokOAuth.error.value =
error.response?.data?.detail ||
error.message ||
t('admin.accounts.oauth.grok.failedToAuthorizePassword', 'Password authorization failed')
appStore.showError(grokOAuth.error.value)
} finally {
grokOAuth.loading.value = false
}
}
/** Re-auth with a single refresh token. */
const handleGrokValidateRefreshToken = async (refreshTokenInput: string) => {
if (!props.account || !isGrok.value) return
@@ -8,18 +8,16 @@ const source = readFileSync(
)
describe('ReAuthAccountModal Grok re-auth paths', () => {
it('exposes SSO cookie, email-password, and refresh-token options for Grok', () => {
it('exposes SSO cookie and refresh-token options; password auth stays hidden', () => {
expect(source).toContain(':show-sso-option="isGrok"')
expect(source).toContain(':show-email-password-option="isGrok"')
expect(source).toContain(':show-email-password-option="false"')
expect(source).toContain(':show-refresh-token-option="isOpenAI || isAntigravity || isGrok"')
expect(source).not.toContain('@authorize-password=')
})
it('wires password and SSO handlers without batch account create', () => {
expect(source).toContain('@authorize-password="handleGrokAuthorizePassword"')
it('wires SSO and RT reauth without batch account create', () => {
expect(source).toContain('@import-sso="handleGrokImportSSO"')
expect(source).toContain('@validate-refresh-token="handleGrokValidateRefreshToken"')
expect(source).toContain('handleGrokAuthorizePassword')
expect(source).toContain('grokOAuth.authorizePassword')
expect(source).toContain('grokOAuth.validateSSOToken')
expect(source).toContain('grokOAuth.buildCredentials')
// Re-auth updates the existing account; must not call createFromSSO batch create
@@ -27,18 +25,17 @@ describe('ReAuthAccountModal Grok re-auth paths', () => {
expect(source).toContain('applyOAuthCredentials')
})
it('hides footer code-exchange button for SSO/password/RT input methods', () => {
it('hides footer code-exchange button for SSO/RT input methods', () => {
expect(source).toContain("method === 'sso_cookie'")
expect(source).toContain("method === 'email_password'")
expect(source).toContain("method === 'refresh_token'")
})
it('prefills email---- and defaults to password method when email is known', () => {
expect(source).toContain('grokPrefillEmailPassword')
it('defaults reauth to refresh_token or sso_cookie (not password)', () => {
expect(source).toContain('grokInitialInputMethod')
expect(source).toContain(':initial-email-password="grokPrefillEmailPassword"')
expect(source).toContain(':initial-input-method="grokInitialInputMethod"')
expect(source).toContain('email----')
expect(source).toContain("return 'email_password'")
expect(source).toContain("return 'sso_cookie'")
expect(source).toContain("return 'refresh_token'")
expect(source).not.toContain("return 'email_password'")
expect(source).not.toContain('grokPrefillEmailPassword')
})
})