PKCE 흐름, 토큰 저장, 자동/수동 인증, 프로필 가져오기, 토큰 갱신.
Claude Code는 OAuth 2.0 + PKCE(Proof Key for Code Exchange) 흐름을 사용하여 사용자를 인증합니다. API 키 대신 OAuth를 채택한 이유는 키 유출 위험 제거, 범위 기반 권한 관리, 그리고 구독 상태와의 자동 연동입니다.
auth/oauth.ts → auth/listener.ts → auth/tokenStore.ts → auth/profile.ts → auth/refresh.ts
인증 시스템의 핵심 구성 요소:
전체 OAuth PKCE 인증 흐름은 17개 단계로 구성됩니다.
// auth/oauth.ts — PKCE 챌린지 생성
function generatePKCE(): { verifier: string, challenge: string } {
const verifier = base64url(randomBytes(32))
const challenge = base64url(
createHash('sha256').update(verifier).digest()
)
return { verifier, challenge }
}
두 인증 경로는 동시에 시작되어 먼저 완료된 쪽이 사용됩니다.
시스템 기본 브라우저를 열어 인증 URL로 리다이렉트합니다. 로컬에 AuthCodeListener 서버가 실행되어 콜백을 수신합니다. 사용자가 브라우저에서 로그인하면 자동으로 인가 코드가 CLI에 전달됩니다.
브라우저를 열 수 없는 환경(SSH, 컨테이너, 헤드리스 서버)에서는 인증 URL을 터미널에 출력합니다. 사용자가 URL을 복사하여 다른 기기의 브라우저에 붙여넣고, 인증 후 표시되는 코드를 CLI에 입력합니다.
// auth/oauth.ts — 자동/수동 인증 경쟁
async function authenticate(): Promise<TokenSet> {
const { verifier, challenge } = generatePKCE()
const state = randomUUID()
// 두 경로를 동시에 시작
const autoAuth = startAutoAuth(challenge, state) // 브라우저 오픈
const manualAuth = startManualAuth(challenge, state) // URL 표시
// 먼저 완료된 쪽의 코드 사용
const code = await Promise.race([autoAuth, manualAuth])
// 토큰 교환
return exchangeCodeForTokens(code, verifier)
}
SSH 원격 세션에서 open 명령이 성공하더라도 사용자의 로컬 브라우저가 열리지 않을 수 있습니다. 반대로 로컬 환경에서는 수동 모드의 URL 복사가 불필요합니다. 두 경로를 동시에 시작하고 Promise.race로 먼저 완료된 쪽을 사용하면 환경에 관계없이 최적의 UX를 제공합니다. 경쟁에서 진 쪽은 조용히 정리됩니다.
AuthCodeListener는 자동 인증 흐름에서 콜백을 수신하는 로컬 HTTP 서버입니다.
// auth/listener.ts — 로컬 콜백 서버
class AuthCodeListener {
private server: Server
private port: number
async start(): Promise<void> {
this.server = createServer(this.handleCallback)
// 포트 0으로 바인딩 → OS가 사용 가능한 포트 할당
await listen(this.server, 0, '127.0.0.1')
this.port = (this.server.address() as AddressInfo).port
}
private handleCallback(req, res): void {
const url = new URL(req.url, `http://localhost:${this.port}`)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
// state 검증 후 코드 반환
if (state === this.expectedState) {
this.resolve(code)
res.end('인증 완료! 터미널로 돌아가세요.')
}
}
}
포트 0 바인딩은 OS가 사용 가능한 임시 포트를 자동 할당합니다. 이를 통해 포트 충돌을 피하고, 할당된 포트 번호는 리다이렉트 URI에 동적으로 포함됩니다.
Claude Code는 6개의 OAuth 범위를 요청합니다. 각 범위는 특정 기능에 매핑됩니다.
openid — OIDC 표준 식별자 토큰profile — 사용자 이름, 아바타 등 프로필 정보email — 이메일 주소 접근offline_access — refresh_token 발급 (오프라인 갱신)claude:read — Claude API 읽기 권한 (대화, 모델 정보)claude:write — Claude API 쓰기 권한 (메시지 전송, 도구 실행)// auth/oauth.ts — 요청 범위
const OAUTH_SCOPES = [
'openid',
'profile',
'email',
'offline_access',
'claude:read',
'claude:write',
]
토큰 교환 직후 프로필 API를 호출하여 사용자 정보와 구독 상태를 가져옵니다. 구독 플랜에 따라 사용 가능한 기능이 결정됩니다.
// auth/profile.ts — 프로필 및 구독 가져오기
async function fetchProfile(accessToken: string): Promise<UserProfile> {
const response = await fetch('https://api.anthropic.com/v1/me', {
headers: { Authorization: `Bearer ${accessToken}` },
})
const data = await response.json()
return {
userId: data.id,
email: data.email,
name: data.name,
plan: mapSubscription(data.subscription), // 플랜 → 기능 매핑
}
}
// 구독 플랜별 기능 매핑
function mapSubscription(sub: Subscription): PlanFeatures {
switch (sub.tier) {
case 'free': return { maxTokens: 8192, remoteControl: false }
case 'pro': return { maxTokens: 32768, remoteControl: false }
case 'max': return { maxTokens: 65536, remoteControl: true }
case 'enterprise': return { maxTokens: 65536, remoteControl: true }
}
}
토큰은 플랫폼에 따라 두 가지 저장 방식 중 하나를 사용합니다.
macOS에서는 시스템 키체인에 토큰을 저장합니다. security CLI 도구를 통해 키체인에 접근하며, 이는 OS 수준의 암호화와 잠금 화면 보호를 제공합니다.
Linux/Windows 또는 키체인 접근 실패 시 ~/.claude/auth.json에 plaintext로 저장됩니다. 파일 권한은 0600(소유자만 읽기/쓰기)으로 설정됩니다.
// auth/tokenStore.ts — 플랫폼별 토큰 저장
async function storeTokens(tokens: TokenSet): Promise<void> {
if (process.platform === 'darwin') {
try {
await keychainStore('claude-code', JSON.stringify(tokens))
return
} catch {
// 키체인 실패 시 plaintext 폴백
}
}
// plaintext 저장 (0600 권한)
await writeFile(AUTH_PATH, JSON.stringify(tokens), { mode: 0o600 })
}
plaintext 저장 시 auth.json은 ~/.claude/ 디렉토리에 위치합니다. 이 디렉토리가 Git 추적 대상이 되거나 클라우드 동기화 폴더에 포함되면 토큰이 유출될 수 있습니다. .gitignore에 ~/.claude/가 포함되어 있는지 확인하세요.
access_token 만료 5분 전에 선제적으로 갱신을 시도합니다. 이 버퍼는 네트워크 지연이나 일시적 서버 오류를 흡수하여 사용자 세션이 중단되지 않도록 합니다.
// auth/refresh.ts — 선제적 토큰 갱신
const REFRESH_BUFFER_MS = 5 * 60 * 1000 // 5분
async function ensureValidToken(): Promise<string> {
const tokens = await loadTokens()
const expiresAt = tokens.issued_at + tokens.expires_in * 1000
const now = Date.now()
if (now + REFRESH_BUFFER_MS >= expiresAt) {
// 만료 5분 전: 갱신 실행
const refreshed = await refreshTokens(tokens.refresh_token)
await storeTokens(refreshed)
return refreshed.access_token
}
return tokens.access_token
}
refresh_token이 만료되었거나 서버가 응답하지 않으면 사용자에게 재인증을 요구합니다. 이때 현재 세션의 대화 컨텍스트는 유지됩니다 - 인증만 갱신되고 세션 상태는 보존됩니다. 갱신 실패는 최대 3회 재시도하며, 각 재시도 사이에 지수 백오프(1s, 2s, 4s)를 적용합니다.
claude logout 명령은 세 단계로 처리됩니다: (1) 서버에 토큰 폐기 요청, (2) 로컬 토큰 저장소 삭제, (3) 키체인에서 항목 제거. 서버 측 폐기가 실패하더라도 로컬 토큰은 반드시 삭제됩니다.
엔터프라이즈 환경에서는 MDM 정책으로 OAuth 서버 URL을 커스터마이징할 수 있습니다. com.anthropic.claude-code 도메인의 AuthServerURL 키가 인증 서버 엔드포인트를 결정합니다.
// auth/oauth.ts — 엔터프라이즈 인증 서버 해석
function getAuthServerURL(): string {
// 1. 환경 변수 우선
if (process.env.CLAUDE_AUTH_SERVER_URL) {
return process.env.CLAUDE_AUTH_SERVER_URL
}
// 2. MDM 정책 확인
const mdm = getMdmSetting('AuthServerURL')
if (mdm) return mdm
// 3. 기본값
return 'https://auth.anthropic.com'
}
Promise.race로 동시에 시작하여 환경에 관계없이 최적의 UX를 제공합니다offline_access는 refresh_token 발급에 필수입니다~/.claude/auth.json plaintext로 폴백됩니다(0600 권한)Q1. PKCE 흐름에서 code_challenge는 어떻게 생성되나요?
code_verifier를 생성한 후 SHA256(verifier)를 base64url 인코딩하여 code_challenge를 만듭니다. 인가 요청에는 challenge만 전송하고, 토큰 교환 시 verifier를 보내면 서버가 동일하게 해시하여 검증합니다.Q2. 자동 인증과 수동 인증이 동시에 시작되는 이유는?
Promise.race로 먼저 완료된 쪽을 사용합니다. 로컬 환경에서는 자동 인증이 빠르게 완료되고, SSH/컨테이너 환경에서는 수동 인증이 유일한 경로가 됩니다. 순차 실행 대신 경쟁 방식으로 환경 감지 없이 최적의 UX를 제공합니다.Q3. 토큰 갱신의 5분 버퍼가 의미하는 것은?
Q4. macOS에서 keychain 접근이 실패하면 토큰은 어떻게 저장되나요?
~/.claude/auth.json 파일에 plaintext로 폴백합니다. 파일 권한은 0600(소유자만 읽기/쓰기)으로 설정되어 다른 사용자의 접근을 방지하지만, OS 수준의 암호화는 제공되지 않습니다.Q5. 엔터프라이즈 환경에서 인증 서버 URL의 해석 우선순위는?
CLAUDE_AUTH_SERVER_URL 환경 변수, (2) MDM 정책의 AuthServerURL, (3) 기본값 https://auth.anthropic.com입니다. 환경 변수가 최우선으로, 개발/테스트 시 임시 오버라이드에 유용합니다.