Add support for ANTHROPIC_API_KEY environment variable authentication detection (#346)

* Add support for ANTHROPIC_API_KEY environment variable authentication detection

This commit enhances Claude authentication detection to support both the
ANTHROPIC_API_KEY environment variable and the OAuth credentials file,
matching the authentication priority order used by the Claude Agent SDK.

- Updated checkClaudeCredentials() function in server/routes/cli-auth.js
  to check ANTHROPIC_API_KEY environment variable first, then fall back
  to ~/.claude/.credentials.json OAuth tokens

- Modified /api/cli-auth/claude/status endpoint to return authentication
  method indicator ('api_key' or 'credentials_file')

- Added comprehensive JSDoc documentation with priority order explanation
  and official Claude documentation citations

1. ANTHROPIC_API_KEY environment variable (highest priority)
2. ~/.claude/.credentials.json OAuth tokens (fallback)

This priority order matches the Claude Agent SDK's authentication behavior,
ensuring consistency between how we detect authentication and how the SDK
actually authenticates.

The /api/cli-auth/claude/status endpoint now returns:
- method: 'api_key' when using ANTHROPIC_API_KEY environment variable
- method: 'credentials_file' when using OAuth credentials file
- method: null when not authenticated

This is backward compatible as existing code checking the 'authenticated'
field will continue to work.

- https://support.claude.com/en/articles/12304248-managing-api-key-environment-variables-in-claude-code
  Claude Agent SDK prioritizes environment variables over subscriptions

- https://platform.claude.com/docs/en/agent-sdk/overview
  Official Claude Agent SDK authentication documentation

When ANTHROPIC_API_KEY is set, API calls are charged via pay-as-you-go
rates instead of subscription rates, even if the user is logged in with
a claude.ai subscription.

* UI: hide Claude login when API key auth is used

An API key overrides anything else with Claude SDK (and claude-code). There is no need to show this button in API key cases.
This commit is contained in:
Menny Even Danan
2026-03-04 04:01:28 -05:00
committed by GitHub
parent b0a3fdf95f
commit 453a1452bb
4 changed files with 47 additions and 23 deletions

View File

@@ -14,13 +14,14 @@ router.get('/claude/status', async (req, res) => {
return res.json({
authenticated: true,
email: credentialsResult.email || 'Authenticated',
method: 'credentials_file'
method: credentialsResult.method // 'api_key' or 'credentials_file'
});
}
return res.json({
authenticated: false,
email: null,
method: null,
error: credentialsResult.error || 'Not authenticated'
});
@@ -29,6 +30,7 @@ router.get('/claude/status', async (req, res) => {
res.status(500).json({
authenticated: false,
email: null,
method: null,
error: error.message
});
}
@@ -115,6 +117,20 @@ router.get('/gemini/status', async (req, res) => {
* - method: 'api_key' for env var, 'credentials_file' for OAuth tokens
*/
async function checkClaudeCredentials() {
// Priority 1: Check for ANTHROPIC_API_KEY environment variable
// The SDK checks this first and uses it if present, even if OAuth tokens exist.
// When set, API calls are charged via pay-as-you-go rates instead of subscription.
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) {
return {
authenticated: true,
email: 'API Key Auth',
method: 'api_key'
};
}
// Priority 2: Check ~/.claude/.credentials.json for OAuth tokens
// This is the standard authentication method used by Claude CLI after running
// 'claude /login' or 'claude setup-token' commands.
try {
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
const content = await fs.readFile(credPath, 'utf8');
@@ -127,19 +143,22 @@ async function checkClaudeCredentials() {
if (!isExpired) {
return {
authenticated: true,
email: creds.email || creds.user || null
email: creds.email || creds.user || null,
method: 'credentials_file'
};
}
}
return {
authenticated: false,
email: null
email: null,
method: null
};
} catch (error) {
return {
authenticated: false,
email: null
email: null,
method: null
};
}
}

View File

@@ -41,6 +41,7 @@ type StatusApiResponse = {
authenticated?: boolean;
email?: string | null;
error?: string | null;
method?: string;
};
type JsonResult = {
@@ -267,6 +268,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
email: data.email || null,
loading: false,
error: data.error || null,
method: data.method,
});
} catch (error) {
console.error(`Error checking ${provider} auth status:`, error);

View File

@@ -23,6 +23,7 @@ export type AuthStatus = {
email: string | null;
loading: boolean;
error: string | null;
method?: string;
};
export type KeyValueMap = Record<string, string>;

View File

@@ -107,28 +107,30 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="flex items-center justify-between">
<div>
<div className={`font-medium ${config.textClass}`}>
{authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
</div>
<div className={`text-sm ${config.subtextClass}`}>
{authStatus.authenticated
? t('agents.login.reAuthDescription')
: t('agents.login.description', { agent: config.name })}
{authStatus.method !== 'api_key' && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="flex items-center justify-between">
<div>
<div className={`font-medium ${config.textClass}`}>
{authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')}
</div>
<div className={`text-sm ${config.subtextClass}`}>
{authStatus.authenticated
? t('agents.login.reAuthDescription')
: t('agents.login.description', { agent: config.name })}
</div>
</div>
<Button
onClick={onLogin}
className={`${config.buttonClass} text-white`}
size="sm"
>
<LogIn className="w-4 h-4 mr-2" />
{authStatus.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
</Button>
</div>
<Button
onClick={onLogin}
className={`${config.buttonClass} text-white`}
size="sm"
>
<LogIn className="w-4 h-4 mr-2" />
{authStatus.authenticated ? t('agents.login.reLoginButton') : t('agents.login.button')}
</Button>
</div>
</div>
)}
{authStatus.error && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">