auth.ts
cli/handlers/auth.ts
No strong subsystem tag
331
Lines
10756
Bytes
4
Exports
18
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 331 lines, 18 detected imports, and 4 detected exports.
Important relationships
Detected exports
installOAuthTokensauthLoginauthStatusauthLogout
Keywords
processtokenswriteprofileexitutilsstdoutelseauthmethodservices
Detected imports
../../commands/logout/logout.js../../services/analytics/index.js../../services/api/errorUtils.js../../services/api/firstTokenDate.js../../services/oauth/client.js../../services/oauth/getOauthProfile.js../../services/oauth/index.js../../services/oauth/types.js../../utils/auth.js../../utils/config.js../../utils/debug.js../../utils/envUtils.js../../utils/errors.js../../utils/log.js../../utils/model/providers.js../../utils/settings/settings.js../../utils/slowOperations.js../../utils/status.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/* eslint-disable custom-rules/no-process-exit -- CLI subcommand handler intentionally exits */
import {
clearAuthRelatedCaches,
performLogout,
} from '../../commands/logout/logout.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import { getSSLErrorHint } from '../../services/api/errorUtils.js'
import { fetchAndStoreClaudeCodeFirstTokenDate } from '../../services/api/firstTokenDate.js'
import {
createAndStoreApiKey,
fetchAndStoreUserRoles,
refreshOAuthToken,
shouldUseClaudeAIAuth,
storeOAuthAccountInfo,
} from '../../services/oauth/client.js'
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js'
import { OAuthService } from '../../services/oauth/index.js'
import type { OAuthTokens } from '../../services/oauth/types.js'
import {
clearOAuthTokenCache,
getAnthropicApiKeyWithSource,
getAuthTokenSource,
getOauthAccountInfo,
getSubscriptionType,
isUsing3PServices,
saveOAuthTokensIfNeeded,
validateForceLoginOrg,
} from '../../utils/auth.js'
import { saveGlobalConfig } from '../../utils/config.js'
import { logForDebugging } from '../../utils/debug.js'
import { isRunningOnHomespace } from '../../utils/envUtils.js'
import { errorMessage } from '../../utils/errors.js'
import { logError } from '../../utils/log.js'
import { getAPIProvider } from '../../utils/model/providers.js'
import { getInitialSettings } from '../../utils/settings/settings.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import {
buildAccountProperties,
buildAPIProviderProperties,
} from '../../utils/status.js'
/**
* Shared post-token-acquisition logic. Saves tokens, fetches profile/roles,
* and sets up the local auth state.
*/
export async function installOAuthTokens(tokens: OAuthTokens): Promise<void> {
// Clear old state before saving new credentials
await performLogout({ clearOnboarding: false })
// Reuse pre-fetched profile if available, otherwise fetch fresh
const profile =
tokens.profile ?? (await getOauthProfileFromOauthToken(tokens.accessToken))
if (profile) {
storeOAuthAccountInfo({
accountUuid: profile.account.uuid,
emailAddress: profile.account.email,
organizationUuid: profile.organization.uuid,
displayName: profile.account.display_name || undefined,
hasExtraUsageEnabled:
profile.organization.has_extra_usage_enabled ?? undefined,
billingType: profile.organization.billing_type ?? undefined,
subscriptionCreatedAt:
profile.organization.subscription_created_at ?? undefined,
accountCreatedAt: profile.account.created_at,
})
} else if (tokens.tokenAccount) {
// Fallback to token exchange account data when profile endpoint fails
storeOAuthAccountInfo({
accountUuid: tokens.tokenAccount.uuid,
emailAddress: tokens.tokenAccount.emailAddress,
organizationUuid: tokens.tokenAccount.organizationUuid,
})
}
const storageResult = saveOAuthTokensIfNeeded(tokens)
clearOAuthTokenCache()
if (storageResult.warning) {
logEvent('tengu_oauth_storage_warning', {
warning:
storageResult.warning as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
// Roles and first-token-date may fail for limited-scope tokens (e.g.
// inference-only from setup-token). They're not required for core auth.
await fetchAndStoreUserRoles(tokens.accessToken).catch(err =>
logForDebugging(String(err), { level: 'error' }),
)
if (shouldUseClaudeAIAuth(tokens.scopes)) {
await fetchAndStoreClaudeCodeFirstTokenDate().catch(err =>
logForDebugging(String(err), { level: 'error' }),
)
} else {
// API key creation is critical for Console users — let it throw.
const apiKey = await createAndStoreApiKey(tokens.accessToken)
if (!apiKey) {
throw new Error(
'Unable to create API key. The server accepted the request but did not return a key.',
)
}
}
await clearAuthRelatedCaches()
}
export async function authLogin({
email,
sso,
console: useConsole,
claudeai,
}: {
email?: string
sso?: boolean
console?: boolean
claudeai?: boolean
}): Promise<void> {
if (useConsole && claudeai) {
process.stderr.write(
'Error: --console and --claudeai cannot be used together.\n',
)
process.exit(1)
}
const settings = getInitialSettings()
// forceLoginMethod is a hard constraint (enterprise setting) — matches ConsoleOAuthFlow behavior.
// Without it, --console selects Console; --claudeai (or no flag) selects claude.ai.
const loginWithClaudeAi = settings.forceLoginMethod
? settings.forceLoginMethod === 'claudeai'
: !useConsole
const orgUUID = settings.forceLoginOrgUUID
// Fast path: if a refresh token is provided via env var, skip the browser
// OAuth flow and exchange it directly for tokens.
const envRefreshToken = process.env.CLAUDE_CODE_OAUTH_REFRESH_TOKEN
if (envRefreshToken) {
const envScopes = process.env.CLAUDE_CODE_OAUTH_SCOPES
if (!envScopes) {
process.stderr.write(
'CLAUDE_CODE_OAUTH_SCOPES is required when using CLAUDE_CODE_OAUTH_REFRESH_TOKEN.\n' +
'Set it to the space-separated scopes the refresh token was issued with\n' +
'(e.g. "user:inference" or "user:profile user:inference user:sessions:claude_code user:mcp_servers").\n',
)
process.exit(1)
}
const scopes = envScopes.split(/\s+/).filter(Boolean)
try {
logEvent('tengu_login_from_refresh_token', {})
const tokens = await refreshOAuthToken(envRefreshToken, { scopes })
await installOAuthTokens(tokens)
const orgResult = await validateForceLoginOrg()
if (!orgResult.valid) {
process.stderr.write(orgResult.message + '\n')
process.exit(1)
}
// Mark onboarding complete — interactive paths handle this via
// the Onboarding component, but the env var path skips it.
saveGlobalConfig(current => {
if (current.hasCompletedOnboarding) return current
return { ...current, hasCompletedOnboarding: true }
})
logEvent('tengu_oauth_success', {
loginWithClaudeAi: shouldUseClaudeAIAuth(tokens.scopes),
})
process.stdout.write('Login successful.\n')
process.exit(0)
} catch (err) {
logError(err)
const sslHint = getSSLErrorHint(err)
process.stderr.write(
`Login failed: ${errorMessage(err)}\n${sslHint ? sslHint + '\n' : ''}`,
)
process.exit(1)
}
}
const resolvedLoginMethod = sso ? 'sso' : undefined
const oauthService = new OAuthService()
try {
logEvent('tengu_oauth_flow_start', { loginWithClaudeAi })
const result = await oauthService.startOAuthFlow(
async url => {
process.stdout.write('Opening browser to sign in…\n')
process.stdout.write(`If the browser didn't open, visit: ${url}\n`)
},
{
loginWithClaudeAi,
loginHint: email,
loginMethod: resolvedLoginMethod,
orgUUID,
},
)
await installOAuthTokens(result)
const orgResult = await validateForceLoginOrg()
if (!orgResult.valid) {
process.stderr.write(orgResult.message + '\n')
process.exit(1)
}
logEvent('tengu_oauth_success', { loginWithClaudeAi })
process.stdout.write('Login successful.\n')
process.exit(0)
} catch (err) {
logError(err)
const sslHint = getSSLErrorHint(err)
process.stderr.write(
`Login failed: ${errorMessage(err)}\n${sslHint ? sslHint + '\n' : ''}`,
)
process.exit(1)
} finally {
oauthService.cleanup()
}
}
export async function authStatus(opts: {
json?: boolean
text?: boolean
}): Promise<void> {
const { source: authTokenSource, hasToken } = getAuthTokenSource()
const { source: apiKeySource } = getAnthropicApiKeyWithSource()
const hasApiKeyEnvVar =
!!process.env.ANTHROPIC_API_KEY && !isRunningOnHomespace()
const oauthAccount = getOauthAccountInfo()
const subscriptionType = getSubscriptionType()
const using3P = isUsing3PServices()
const loggedIn =
hasToken || apiKeySource !== 'none' || hasApiKeyEnvVar || using3P
// Determine auth method
let authMethod: string = 'none'
if (using3P) {
authMethod = 'third_party'
} else if (authTokenSource === 'claude.ai') {
authMethod = 'claude.ai'
} else if (authTokenSource === 'apiKeyHelper') {
authMethod = 'api_key_helper'
} else if (authTokenSource !== 'none') {
authMethod = 'oauth_token'
} else if (apiKeySource === 'ANTHROPIC_API_KEY' || hasApiKeyEnvVar) {
authMethod = 'api_key'
} else if (apiKeySource === '/login managed key') {
authMethod = 'claude.ai'
}
if (opts.text) {
const properties = [
...buildAccountProperties(),
...buildAPIProviderProperties(),
]
let hasAuthProperty = false
for (const prop of properties) {
const value =
typeof prop.value === 'string'
? prop.value
: Array.isArray(prop.value)
? prop.value.join(', ')
: null
if (value === null || value === 'none') {
continue
}
hasAuthProperty = true
if (prop.label) {
process.stdout.write(`${prop.label}: ${value}\n`)
} else {
process.stdout.write(`${value}\n`)
}
}
if (!hasAuthProperty && hasApiKeyEnvVar) {
process.stdout.write('API key: ANTHROPIC_API_KEY\n')
}
if (!loggedIn) {
process.stdout.write(
'Not logged in. Run claude auth login to authenticate.\n',
)
}
} else {
const apiProvider = getAPIProvider()
const resolvedApiKeySource =
apiKeySource !== 'none'
? apiKeySource
: hasApiKeyEnvVar
? 'ANTHROPIC_API_KEY'
: null
const output: Record<string, string | boolean | null> = {
loggedIn,
authMethod,
apiProvider,
}
if (resolvedApiKeySource) {
output.apiKeySource = resolvedApiKeySource
}
if (authMethod === 'claude.ai') {
output.email = oauthAccount?.emailAddress ?? null
output.orgId = oauthAccount?.organizationUuid ?? null
output.orgName = oauthAccount?.organizationName ?? null
output.subscriptionType = subscriptionType ?? null
}
process.stdout.write(jsonStringify(output, null, 2) + '\n')
}
process.exit(loggedIn ? 0 : 1)
}
export async function authLogout(): Promise<void> {
try {
await performLogout({ clearOnboarding: false })
} catch {
process.stderr.write('Failed to log out.\n')
process.exit(1)
}
process.stdout.write('Successfully logged out from your Anthropic account.\n')
process.exit(0)
}