mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
fix(web-server): make kiro auth routes method-aware
- validate kiroMethod on /start and /start-url with clear errors - route Kiro management start-url requests with upstream method query - accept state-first responses and keep auth_url optional - add start-url guard tests for Kiro social and aws-authcode paths Refs #552 Refs #233
This commit is contained in:
@@ -37,6 +37,11 @@ import { getProviderTokenDir } from '../../cliproxy/auth/token-manager';
|
||||
import {
|
||||
CLIPROXY_CALLBACK_PROVIDER_MAP,
|
||||
CLIPROXY_AUTH_URL_PROVIDER_MAP,
|
||||
isKiroAuthMethod,
|
||||
isKiroDeviceCodeMethod,
|
||||
KiroAuthMethod,
|
||||
normalizeKiroAuthMethod,
|
||||
toKiroManagementMethod,
|
||||
} from '../../cliproxy/auth/auth-types';
|
||||
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
|
||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||
@@ -47,7 +52,38 @@ const router = Router();
|
||||
// Valid providers list - derived from canonical CLIPROXY_PROFILES
|
||||
const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
||||
|
||||
export function getStartUrlUnsupportedReason(provider: CLIProxyProvider): string | null {
|
||||
function parseKiroMethod(raw: unknown): { method: KiroAuthMethod; invalid: boolean } {
|
||||
if (raw === undefined || raw === null) {
|
||||
return { method: normalizeKiroAuthMethod(), invalid: false };
|
||||
}
|
||||
if (typeof raw !== 'string') {
|
||||
return { method: normalizeKiroAuthMethod(), invalid: true };
|
||||
}
|
||||
if (raw.trim() === '') {
|
||||
return { method: normalizeKiroAuthMethod(), invalid: false };
|
||||
}
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (!isKiroAuthMethod(normalized)) {
|
||||
return { method: normalizeKiroAuthMethod(), invalid: true };
|
||||
}
|
||||
return { method: normalizeKiroAuthMethod(normalized), invalid: false };
|
||||
}
|
||||
|
||||
export function getStartUrlUnsupportedReason(
|
||||
provider: CLIProxyProvider,
|
||||
options?: { kiroMethod?: KiroAuthMethod }
|
||||
): string | null {
|
||||
if (provider === 'kiro') {
|
||||
const kiroMethod = options?.kiroMethod ?? normalizeKiroAuthMethod();
|
||||
if (kiroMethod === 'aws-authcode') {
|
||||
return "Kiro method 'aws-authcode' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
|
||||
}
|
||||
if (isKiroDeviceCodeMethod(kiroMethod)) {
|
||||
return "Kiro method 'aws' uses Device Code flow. Use /api/cliproxy/auth/kiro/start instead.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (getOAuthFlowType(provider) === 'device_code') {
|
||||
return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
|
||||
}
|
||||
@@ -351,9 +387,14 @@ router.post('/accounts/:provider/:accountId/resume', (req: Request, res: Respons
|
||||
*/
|
||||
router.post('/:provider/start', async (req: Request, res: Response): Promise<void> => {
|
||||
const { provider } = req.params;
|
||||
const { nickname: nicknameRaw, noIncognito: noIncognitoBody } = req.body;
|
||||
const {
|
||||
nickname: nicknameRaw,
|
||||
noIncognito: noIncognitoBody,
|
||||
kiroMethod: kiroMethodRaw,
|
||||
} = req.body;
|
||||
// Trim nickname for consistency with CLI (oauth-handler.ts trims input)
|
||||
const nickname = typeof nicknameRaw === 'string' ? nicknameRaw.trim() : nicknameRaw;
|
||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||
|
||||
// Validate provider
|
||||
if (!validProviders.includes(provider as CLIProxyProvider)) {
|
||||
@@ -361,6 +402,14 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'kiro' && invalidKiroMethod) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github',
|
||||
code: 'INVALID_KIRO_METHOD',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// For kiro/ghcp: nickname is required
|
||||
if (PROVIDERS_WITHOUT_EMAIL.includes(provider as CLIProxyProvider)) {
|
||||
if (!nickname) {
|
||||
@@ -408,6 +457,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
||||
add: true, // Always add mode from UI
|
||||
headless: false, // Force interactive mode
|
||||
nickname: nickname || undefined,
|
||||
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
|
||||
fromUI: true, // Enable project selection prompt in UI
|
||||
noIncognito, // Kiro: use normal browser if enabled
|
||||
});
|
||||
@@ -552,6 +602,8 @@ router.post('/kiro/import', async (_req: Request, res: Response): Promise<void>
|
||||
*/
|
||||
router.post('/:provider/start-url', async (req: Request, res: Response): Promise<void> => {
|
||||
const { provider } = req.params;
|
||||
const { kiroMethod: kiroMethodRaw } = req.body ?? {};
|
||||
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
|
||||
|
||||
// Check remote mode
|
||||
const target = getProxyTarget();
|
||||
@@ -565,7 +617,18 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
res.status(400).json({ error: `Invalid provider: ${provider}` });
|
||||
return;
|
||||
}
|
||||
const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider);
|
||||
|
||||
if (provider === 'kiro' && invalidKiroMethod) {
|
||||
res.status(400).json({
|
||||
error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github',
|
||||
code: 'INVALID_KIRO_METHOD',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider, {
|
||||
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
|
||||
});
|
||||
if (unsupportedReason) {
|
||||
res.status(400).json({ error: unsupportedReason });
|
||||
return;
|
||||
@@ -574,11 +637,15 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
try {
|
||||
const authUrlProvider =
|
||||
CLIPROXY_AUTH_URL_PROVIDER_MAP[provider as CLIProxyProvider] || provider;
|
||||
const kiroQuery =
|
||||
provider === 'kiro'
|
||||
? `&method=${encodeURIComponent(toKiroManagementMethod(kiroMethod))}`
|
||||
: '';
|
||||
|
||||
// Call CLIProxyAPI to start OAuth and get auth URL
|
||||
// CLIProxyAPI management routes are under /v0/management prefix
|
||||
const response = await fetch(
|
||||
buildProxyUrl(target, `/v0/management/${authUrlProvider}-auth-url?is_webui=true`),
|
||||
buildProxyUrl(target, `/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}`),
|
||||
{ headers: buildManagementHeaders(target) }
|
||||
);
|
||||
|
||||
@@ -588,18 +655,27 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { url?: string; auth_url?: string; state?: string };
|
||||
const data = (await response.json()) as {
|
||||
url?: string;
|
||||
auth_url?: string;
|
||||
state?: string;
|
||||
method?: string;
|
||||
};
|
||||
const authUrl = data.url || data.auth_url;
|
||||
|
||||
if (!authUrl) {
|
||||
res.status(500).json({ error: 'No authorization URL received from CLIProxyAPI' });
|
||||
// Some upstream flows return state first and provide auth_url in subsequent status polling.
|
||||
if (!authUrl && !data.state) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: 'No OAuth state or authorization URL received from CLIProxyAPI' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
authUrl,
|
||||
state: data.state,
|
||||
authUrl: authUrl || null,
|
||||
state: data.state || null,
|
||||
method: data.method || null,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to start OAuth';
|
||||
|
||||
@@ -3,11 +3,22 @@ import { getStartUrlUnsupportedReason } from '../../../src/web-server/routes/cli
|
||||
|
||||
describe('cliproxy-auth-routes start-url guard', () => {
|
||||
it('rejects device code providers', () => {
|
||||
expect(getStartUrlUnsupportedReason('kiro')).toContain("Provider 'kiro' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('kiro')).toContain("Kiro method 'aws' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
||||
});
|
||||
|
||||
it('allows Kiro social methods on start-url', () => {
|
||||
expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'google' })).toBeNull();
|
||||
expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'github' })).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects Kiro aws-authcode method on start-url', () => {
|
||||
expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'aws-authcode' })).toContain(
|
||||
"Kiro method 'aws-authcode' uses CLI auth flow"
|
||||
);
|
||||
});
|
||||
|
||||
it('allows authorization code providers', () => {
|
||||
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
|
||||
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
||||
|
||||
Reference in New Issue
Block a user