상위: Web Security
요약
OAuth 2.0은 제3자 애플리케이션이 사용자의 리소스에 접근할 수 있도록 권한을 위임하는 프로토콜입니다. "Google로 로그인", "카카오로 로그인" 등의 소셜 로그인에 사용됩니다.
핵심 개념
OAuth의 목적
❌ 비밀번호 공유 없이
✅ 제한된 접근 권한 부여
예시:
- "이 앱이 내 Google 캘린더를 읽을 수 있게 허용"
- "이 앱이 내 카카오톡 프로필을 볼 수 있게 허용"
4가지 역할
| 역할 | 설명 | 예시 |
|---|---|---|
| Resource Owner | 리소스 소유자 (사용자) | 당신 |
| Client | 접근을 요청하는 앱 | 새로운 앱 |
| Authorization Server | 인증 서버 | Google 로그인 서버 |
| Resource Server | 리소스 보유 서버 | Google API 서버 |
Authorization Code Grant (가장 일반적)
┌─────────┐ ┌─────────────────┐
│ User │ │ Authorization │
│(Browser)│ │ Server │
└────┬────┘ │ (Google) │
│ └────────┬────────┘
│ 1. 로그인 버튼 클릭 │
│─────────────────────────────────────▶│ │
│ │ │
│ 2. Google 로그인 페이지로 리다이렉트 │ │
│◀─────────────────────────────────────│ │
│ │ │
│ 3. 사용자 로그인 및 권한 동의 │ │
│─────────────────────────────────────▶│ │
│ │ │
│ 4. Authorization Code와 함께 리다이렉트 │ │
│◀─────────────────────────────────────│ │
│ │ │
│ ┌──────────┐ │ │
│ │ Client │ │ │
│ │ (Server) │ │ │
│ └────┬─────┘ │ │
│ │ │ │
│ 5. Code를 서버로 전달 │ │ │
│────────────────────────▶│ │ │
│ │ │ │
│ │ 6. Code + Client Secret으로 Token 교환
│ │────────────────────────────────▶│
│ │ │
│ │ 7. Access Token 반환 │
│ │◀────────────────────────────────│
│ │ │
│ 8. 로그인 완료 │ │
│◀────────────────────────│ │
단계별 상세
1-2. 인증 요청
https://accounts.google.com/oauth/authorize?
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
response_type=code&
scope=email profile&
state=random_string # CSRF 방지
3-4. 사용자 동의 후 리다이렉트
https://yourapp.com/callback?
code=AUTHORIZATION_CODE&
state=random_string
5-7. 토큰 교환 (서버 간 통신)
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
redirect_uri=https://yourapp.com/callback
응답:
{
"access_token": "ya29.xxx...",
"refresh_token": "1//xxx...",
"expires_in": 3600,
"token_type": "Bearer"
}
OAuth vs OpenID Connect
| 항목 | OAuth 2.0 | OpenID Connect (OIDC) |
|---|---|---|
| 목적 | 권한 부여 (Authorization) | 인증 (Authentication) |
| 반환 값 | Access Token | Access Token + ID Token |
| 용도 | "앱이 내 데이터 접근" | "이 사람이 누구인가" |
| 표준 | 권한만 | 신원 정보 포함 |
OIDC ID Token 예시
{
"iss": "https://accounts.google.com",
"sub": "1234567890",
"email": "[email protected]",
"name": "홍길동",
"picture": "https://...",
"iat": 1234567890,
"exp": 1234571490
}
토큰 종류
| 토큰 | 용도 | 수명 |
|---|---|---|
| Access Token | API 접근 | 짧음 (1시간) |
| Refresh Token | Access Token 갱신 | 김 (수개월) |
| ID Token | 사용자 정보 | 짧음 |
토큰 갱신
POST https://oauth2.googleapis.com/token
grant_type=refresh_token&
refresh_token=REFRESH_TOKEN&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET
OAuth Grant Types
| Grant Type | 용도 | 보안 |
|---|---|---|
| Authorization Code | 웹 앱, 서버 사이드 | 높음 |
| Authorization Code + PKCE | 모바일, SPA | 높음 |
| Implicit (deprecated) | SPA (구식) | 낮음 |
| Client Credentials | 서버 간 통신 | 중간 |
| Resource Owner Password | 자사 앱만 | 낮음 |
PKCE (Proof Key for Code Exchange)
모바일/SPA에서 Client Secret 없이 안전하게
// 1. Code Verifier 생성 (랜덤 문자열)
const codeVerifier = generateRandomString(64);
// 2. Code Challenge 생성 (SHA-256 해시)
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
// 3. 인증 요청에 포함
// code_challenge=xxxxx&code_challenge_method=S256
// 4. 토큰 교환 시 code_verifier 전송
// code_verifier=original_verifier
보안 취약점
1. CSRF 공격
공격자가 자신의 Authorization Code를 피해자에게 사용하게 유도
→ 피해자 계정이 공격자 계정에 연결됨
방어: state 파라미터 사용
# 요청 시
state = generate_random_string()
session['oauth_state'] = state
# 콜백에서 검증
if request.args['state'] != session['oauth_state']:
abort(403, "CSRF attack detected")
2. Open Redirect
공격자가 redirect_uri를 조작
→ Authorization Code가 공격자에게 전송
방어: redirect_uri 화이트리스트
ALLOWED_REDIRECTS = ['https://myapp.com/callback']
if redirect_uri not in ALLOWED_REDIRECTS:
abort(400, "Invalid redirect_uri")
3. Authorization Code 탈취
Code가 URL에 노출되어 Referer 헤더나 로그로 유출
방어:
- Code는 1회용 + 짧은 유효시간 (10분)
- PKCE 사용
- HTTPS 필수
4. Access Token 노출
Token이 프론트엔드 코드나 로그에 노출
방어:
- Token은 메모리에만 저장 (localStorage 금지)
- 백엔드에서 토큰 관리
- 짧은 만료 시간
구현 예시
Python (Flask + Google)
from flask import Flask, redirect, request, session
import requests
app = Flask(__name__)
app.secret_key = 'your-secret-key'
CLIENT_ID = 'your-client-id'
CLIENT_SECRET = 'your-client-secret'
REDIRECT_URI = 'http://localhost:5000/callback'
@app.route('/login')
def login():
state = secrets.token_urlsafe(16)
session['oauth_state'] = state
return redirect(
f"https://accounts.google.com/o/oauth2/auth?"
f"client_id={CLIENT_ID}&"
f"redirect_uri={REDIRECT_URI}&"
f"response_type=code&"
f"scope=email profile&"
f"state={state}"
)
@app.route('/callback')
def callback():
# CSRF 검증
if request.args['state'] != session.get('oauth_state'):
return "CSRF detected", 403
code = request.args['code']
# 토큰 교환
token_response = requests.post(
'https://oauth2.googleapis.com/token',
data={
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
}
)
tokens = token_response.json()
access_token = tokens['access_token']
# 사용자 정보 조회
user_info = requests.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
headers={'Authorization': f'Bearer {access_token}'}
).json()
return f"Hello, {user_info['name']}!"
주요 OAuth 제공자
| 제공자 | 문서 |
|---|---|
| developers.google.com/identity | |
| Kakao | developers.kakao.com |
| Naver | developers.naver.com |
| GitHub | docs.github.com/oauth |