OAuth 2.0, JWT 및 API 키 관리를 포함한 API 인증 및 권한 부여에 대한 실용적인 가이드입니다.
OAuth 2.0은 API 액세스를 위해 가장 널리 사용되는 인증 프레임워크입니다. 적절한 권한 부여 유형을 선택하는 것이 중요합니다.
| 보조금 유형 | 사용 사례 | 보안 수준 |
|---|---|---|
| Authorization Code + PKCE | SPA 및 모바일 앱(권장) | 최고 |
| Authorization Code | 서버 측 웹 앱 | 높음 |
| Client Credentials | 서버 간 통신(M2M) | Medium |
| 암시적(사용 중단됨) | 레거시 SPA | 낮음 |
| 리소스 소유자 비밀번호(사용 중단됨) | 신뢰도가 높은 퍼스트 파티 전용 | 낮음 |
토큰이 URL 조각에 노출되기 때문에 암시적 부여는 이제 더 이상 사용되지 않습니다. SPA에는 인증 코드 + PKCE를 사용합니다.
// 1. 코드 검증기를 무작위로 생성합니다. function generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 2. SHA-256으로 code_challenge를 생성합니다. async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 3. 권한 요청 const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); const authUrl = `https://auth.example.com/authorize?` + `response_type=code&` + `client_id=${clientId}&` + `redirect_uri=${redirectUri}&` + `code_challenge=${challenge}&` + `code_challenge_method=S256&` + `scope=read write`;
JWT는 상태 비저장 인증 토큰으로 널리 사용되지만 구현 실수는 보안 취약점으로 직결됩니다.
const jwt = require('jsonwebtoken'); // --- 토큰 발행 --- function issueTokens(user) { const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '15m', issuer: 'api.example.com', audience: 'app.example.com', } ); const refreshToken = jwt.sign( { sub: user.id, type: 'refresh' }, process.env.REFRESH_SECRET, { algorithm: 'HS256', expiresIn: '7d' } ); return { accessToken, refreshToken }; } // --- 토큰 확인 --- function verifyAccessToken(token) { return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'], // ← 항상 지정하세요! issuer: 'api.example.com', audience: 'app.example.com', }); }
| 저장 위치 | XSS 저항 | CSRF 저항 | 권장 사항 |
|---|---|---|---|
| HttpOnly Cookie | ◎ | △ (SameSite 설정 필요) | 추천 |
| 메모리(가변) | ◎ | ◎ | 페이지 탐색 중 분실 |
| localStorage | ✕ (XSS를 통해 도용될 수 있음) | ◎ | 권장하지 않음 |
| sessionStorage | ✕ (XSS를 통해 도용될 수 있음) | ◎ | 제한된 사용 |
액세스 토큰을 쿠키에 HttpOnly, Secure 및 SameSite=Strict 속성을 사용하여 저장하는 것이 가장 안전한 방법입니다. 이렇게 하면 XSS 공격을 통한 토큰 도용을 방지할 수 있습니다.
API 키는 간단한 인증 메커니즘이지만 부적절하게 관리하면 심각한 위험을 초래할 수 있습니다.
충분한 엔트로피(256비트 이상)로 생성합니다. 데이터베이스에 일반 텍스트를 보관하지 말고 해시된 상태로 저장합니다.
키당 최소 범위(읽기 전용, 특정 리소스 전용 등)를 할당합니다. 와일드카드 권한은 피하세요.
키를 정기적으로 교체합니다(90일 이내). 오래된 키에 대한 유예 기간을 설정합니다. 유출 시 즉시 해지하세요.
헤더(X-API-Key 또는 권한 부여)를 통해 전송합니다. URL 쿼리 매개변수에는 포함하지 마세요(로그에 표시됨).
소스 코드에 키를 하드코딩하지 마세요. 환경 변수 또는 비밀 관리자를 사용하여 관리하세요.
API 키 사용 모니터링. 비정상적인 패턴(대량 요청, 알 수 없는 IP로부터의 액세스)을 감지합니다.
const crypto = require('crypto'); // SHA-256으로 API 키를 해시하고 비교합니다. async function validateApiKey(req, res, next) { const apiKey = req.headers['x-api-key']; if (!apiKey) { return res.status(401).json({ error: 'API key required' }); } // 해시하고 DB에서 조회합니다(해시를 비교하므로 timingSafeEqual은 필요하지 않습니다). const hashedKey = crypto .createHash('sha256') .update(apiKey) .digest('hex'); const keyRecord = await db.findApiKey(hashedKey); if (!keyRecord || keyRecord.revokedAt) { return res.status(403).json({ error: 'Invalid API key' }); } // 만료 확인 if (keyRecord.expiresAt && new Date() > keyRecord.expiresAt) { return res.status(403).json({ error: 'API key expired' }); } req.apiClient = keyRecord; next(); }
AI agents and LLM-powered applications require additional authentication and authorization patterns beyond traditional API security.
Assign different access levels per model tier (e.g., GPT-4 requires elevated scope). Prevent unauthorized use of expensive or sensitive models.
Embed token budgets in access tokens or API key metadata. Enforce per-request and per-day limits to prevent denial-of-wallet attacks.
Check user permissions before executing certain prompt types (e.g., code generation, data analysis). Map prompt categories to role-based permissions.
Ensure conversation history, fine-tuned models, and RAG data are isolated per tenant. Use tenant-scoped API keys with namespace enforcement.
Assign cryptographic identities to AI agents. Use signed JWTs for inter-agent communication. Verify agent identity before granting tool access.
Automate API key rotation for LLM providers. Use short-lived tokens for agent sessions. Revoke credentials immediately when agents are decommissioned.
| Scenario | Auth Method | Required Scope | Approval |
|---|---|---|---|
| Agent reads public data | API Key | data:read |
Automatic |
| Agent modifies user data | OAuth 2.0 (delegated) | data:write |
User consent required |
| Agent calls external API | OAuth 2.0 + mTLS | external:invoke |
Human-in-the-loop |
| Agent executes code / shell | Scoped JWT + Sandbox | exec:sandbox |
Admin approval + audit log |
import jwt import time from datetime import datetime, timedelta class AgentTokenIssuer: """Issues scoped, short-lived tokens for AI agents.""" ALLOWED_SCOPES = { "reader": ["data:read"], "writer": ["data:read", "data:write"], "executor": ["data:read", "data:write", "exec:sandbox"], } def __init__(self, secret_key: str): self.secret_key = secret_key def issue_agent_token( self, agent_id: str, role: str, tenant_id: str, token_budget: int = 10000, ttl_minutes: int = 15, ) -> str: # Validate role and resolve scopes if role not in self.ALLOWED_SCOPES: raise ValueError(f"Invalid role: {role}") payload = { "sub": agent_id, "tenant": tenant_id, "scopes": self.ALLOWED_SCOPES[role], "token_budget": token_budget, "iat": datetime.utcnow(), "exp": datetime.utcnow() + timedelta(minutes=ttl_minutes), "type": "agent", } return jwt.encode(payload, self.secret_key, algorithm="HS256") def verify_tool_access(self, token: str, required_scope: str) -> dict: # Decode and verify agent token payload = jwt.decode( token, self.secret_key, algorithms=["HS256"] ) if required_scope not in payload["scopes"]: raise PermissionError( f"Agent {payload['sub']} lacks scope: {required_scope}" ) return payload
Related: LLM06: Excessive Agency, ASI01: Excessive Agency, ASI03: Insecure Tool/Function Calling
| 방법 | 무국적자 | 사용 사례 | 보안 고려 사항 |
|---|---|---|---|
| OAuth 2.0 + PKCE | ○ | 사용자 승인이 필요한 API | 상태/PKCE 필수, 토큰 만료 관리 |
| JWT Bearer | ○ | 마이크로서비스 간 커뮤니케이션 | 알고리즘 사양 필요, 페이로드에 민감한 데이터 없음 |
| API 키 | ○ | 서버 간, 외부 통합 | 회전 및 범위 제한 필요 |
| mTLS | ○ | 높은 보안성의 M2M 통신 | 인증서 관리를 위한 높은 운영 비용 |
| 세션 쿠키 | ✕ | 기존 웹 애플리케이션 | CSRF 보호 필요, 확장성 문제 |