Skip to content

필사 모드: OAuth 2.0 완전 정복 — 인증과 인가의 모든 것

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

"Google로 로그인" 버튼을 누르면 무슨 일이 벌어질까요? 단 한 번의 클릭 뒤에는 **OAuth 2.0**이라는 정교한 프로토콜이 돌아갑니다.

OAuth를 이해하면 소셜 로그인, API 인증, 마이크로서비스 간 통신, 심지어 Anthropic의 Claude API 인증 정책까지 명확해집니다.

핵심 개념: 인증 vs 인가

인증 (Authentication): "너 누구야?"

→ 사용자가 본인임을 증명

→ ID/비밀번호, 생체인식, OTP

인가 (Authorization): "뭘 할 수 있어?"

→ 인증된 사용자에게 권한 부여

→ 이 앱이 네 Google Calendar를 읽어도 돼?

OAuth 2.0 = 인가(Authorization) 프레임워크

OpenID Connect = OAuth 2.0 위에 인증을 추가한 것

OAuth 2.0의 4가지 역할

┌──────────────┐

│ Resource Owner│ ← 사용자 (영주)

└──────┬───────┘

│ "이 앱에 내 캘린더 접근 허용"

┌──────────────┐ ┌──────────────────┐

│ Client │────▶│ Authorization │

│ (우리 앱) │◀────│ Server │

└──────┬───────┘ │ (Google OAuth) │

│ └──────────────────┘

│ Access Token

┌──────────────┐

│ Resource │

│ Server │ ← Google Calendar API

└──────────────┘

Grant Types (인가 방식)

1. Authorization Code (가장 중요!)

웹 앱에서 가장 많이 쓰이는 방식입니다.

[1] 사용자 → 우리앱: "Google로 로그인" 클릭

[2] 우리앱 → Google: 리다이렉트 (client_id, redirect_uri, scope)

[3] 사용자 → Google: 로그인 + "허용" 클릭

[4] Google → 우리앱: redirect_uri?code=AUTH_CODE

[5] 우리앱 → Google: AUTH_CODE + client_secret → Access Token

[6] 우리앱 → Google API: Access Token으로 데이터 요청

Step 2: Authorization 요청 URL 생성

auth_url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode({

"client_id": "YOUR_CLIENT_ID.apps.googleusercontent.com",

"redirect_uri": "https://yourapp.com/callback",

"response_type": "code",

"scope": "openid email profile https://www.googleapis.com/auth/calendar.readonly",

"state": "random_csrf_token_abc123", # CSRF 방지!

"access_type": "offline", # Refresh Token도 받기

})

→ 사용자를 이 URL로 리다이렉트

Step 5: Authorization Code → Access Token 교환

token_response = requests.post("https://oauth2.googleapis.com/token", data={

"code": "AUTH_CODE_FROM_CALLBACK",

"client_id": "YOUR_CLIENT_ID",

"client_secret": "YOUR_CLIENT_SECRET", # 서버에서만!

"redirect_uri": "https://yourapp.com/callback",

"grant_type": "authorization_code",

})

tokens = token_response.json()

{

"access_token": "ya29.xxx...", ← API 호출용 (1시간)

"refresh_token": "1//xxx...", ← 갱신용 (영구)

"id_token": "eyJhbGci...", ← 사용자 정보 (OIDC)

"expires_in": 3600,

"token_type": "Bearer"

}

Step 6: API 호출

calendar = requests.get(

"https://www.googleapis.com/calendar/v3/calendars/primary/events",

headers={"Authorization": f"Bearer {tokens['access_token']}"}

)

**왜 이렇게 복잡한가?**

- Authorization Code는 브라우저 URL에 노출되지만, **1회용**이고 단독으로는 쓸 수 없음

- Access Token 교환은 **서버 간 통신** (client_secret 보호)

- 프론트엔드에 Secret이 절대 노출되지 않음

2. Authorization Code + PKCE (모바일/SPA 필수!)

모바일 앱이나 SPA(React 등)에는 client_secret을 안전하게 저장할 곳이 없습니다.

Client가 code_verifier 생성 (비밀 값)

code_verifier = secrets.token_urlsafe(64)

"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk..."

code_challenge = SHA256(code_verifier) → Base64URL

code_challenge = base64.urlsafe_b64encode(

hashlib.sha256(code_verifier.encode()).digest()

).rstrip(b'=').decode()

Authorization 요청에 challenge 포함

auth_url = f"...&code_challenge={code_challenge}&code_challenge_method=S256"

Token 교환 시 verifier 제출 (Secret 대신!)

token_response = requests.post("https://oauth2.googleapis.com/token", data={

"code": auth_code,

"client_id": "YOUR_CLIENT_ID",

"code_verifier": code_verifier, # Secret 대신 이걸 검증!

"redirect_uri": "...",

"grant_type": "authorization_code",

})

**PKCE의 핵심**: Auth Code를 가로채도 code_verifier가 없으면 Token을 못 받음.

3. Client Credentials (서버 간 통신)

사용자 개입 없이 서버 → 서버 직접 인증

예: 우리 백엔드 → Google Cloud API

token = requests.post("https://oauth2.googleapis.com/token", data={

"client_id": "SERVICE_CLIENT_ID",

"client_secret": "SERVICE_CLIENT_SECRET",

"grant_type": "client_credentials",

"scope": "https://www.googleapis.com/auth/cloud-platform",

})

4. Refresh Token (갱신)

Access Token 만료 (1시간) → Refresh Token으로 갱신

new_tokens = requests.post("https://oauth2.googleapis.com/token", data={

"refresh_token": "1//xxx...",

"client_id": "YOUR_CLIENT_ID",

"client_secret": "YOUR_CLIENT_SECRET",

"grant_type": "refresh_token",

})

새 Access Token 발급 (Refresh Token은 유지)

Token 종류 비교

| Token | 수명 | 용도 | 저장 위치 |

| ------------------ | ---------------- | ------------------ | ----------------------------- |

| Authorization Code | 수 초 | Token 교환용 1회성 | URL 파라미터 (즉시 소멸) |

| Access Token | 1시간 | API 호출 | 메모리 (브라우저) / DB (서버) |

| Refresh Token | 영구 (취소 가능) | Access Token 갱신 | 서버 DB (안전하게!) |

| ID Token (OIDC) | 1시간 | 사용자 정보 확인 | 메모리 |

JWT (JSON Web Token) 구조

Access Token과 ID Token은 보통 JWT 형식입니다:

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik...

├── Header ──────────┤├── Payload ─────────────────────────────...

jwt = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature"

Header

header = json.loads(base64.urlsafe_b64decode(jwt.split('.')[0] + '=='))

{"alg": "RS256", "typ": "JWT"}

Payload

payload = json.loads(base64.urlsafe_b64decode(jwt.split('.')[1] + '=='))

{"sub": "1234567890", "name": "Youngju Kim", "iat": 1709420400}

Signature = RS256(header + "." + payload, private_key)

→ 서버의 public key로 검증 (위변조 불가)

OpenID Connect (OIDC) — OAuth + 인증

OAuth 2.0만으로는 "이 사용자가 누구인지" 알 수 없음

→ OIDC가 id_token을 추가해서 사용자 정보 제공

scope에 "openid" 추가 → id_token 발급

scope에 "profile email" 추가 → 이름, 이메일 포함

보안 체크리스트

✅ state 파라미터로 CSRF 방지

✅ PKCE 사용 (SPA/모바일 필수)

✅ redirect_uri 정확히 매칭 (와일드카드 X)

✅ Refresh Token은 서버에만 저장

✅ Access Token은 Authorization 헤더로만 전송

✅ HTTPS 필수 (Token 탈취 방지)

❌ Access Token을 URL 파라미터에 넣지 않기

❌ client_secret을 프론트엔드에 노출하지 않기

❌ Token을 localStorage에 저장하지 않기 (XSS 취약)

실무 예시: Anthropic OAuth 이슈

최근 Anthropic이 OAuth 정책을 변경한 것도 이 맥락입니다:

기존: Claude Pro 구독 → OAuth Token → 서드파티 앱 (OpenClaw 등)

변경: OAuth Token은 Claude.ai / Claude Code에서만 사용 가능

이유: Token 차익거래 방지 (구독료 < API 토큰 가치)

대안: Anthropic API Key (종량제) 사용

**Q1.** OAuth 2.0에서 인증(Authentication)과 인가(Authorization)의 차이는?

||인증: 사용자가 누구인지 확인. 인가: 인증된 사용자에게 특정 리소스 접근 권한 부여. OAuth 2.0은 인가 프레임워크이고, OIDC가 인증을 추가||

**Q2.** Authorization Code Grant에서 Code가 1회용인 이유는?

||Code는 브라우저 URL에 노출되므로 탈취 위험. 1회용이고 client_secret 없이는 Token 교환 불가하므로 단독으로는 무용지물||

**Q3.** PKCE에서 code_verifier와 code_challenge의 관계는?

||code_challenge = SHA256(code_verifier)의 Base64URL 인코딩. 인가 요청 시 challenge를 보내고, 토큰 교환 시 verifier를 보내 서버가 검증||

**Q4.** Refresh Token을 localStorage에 저장하면 안 되는 이유는?

||XSS 공격으로 JavaScript가 localStorage를 읽을 수 있음. Refresh Token은 장기 유효하므로 탈취 시 지속적인 접근 가능. httpOnly 쿠키 또는 서버 DB에 저장해야 함||

**Q5.** Client Credentials Grant는 어떤 상황에 쓰이나?

||사용자 개입 없이 서버 간 직접 인증. 예: 백엔드 서비스가 다른 API 서버에 접근할 때. client_id + client_secret만으로 Token 발급||

**Q6.** state 파라미터가 없으면 어떤 공격이 가능한가?

||CSRF 공격 — 공격자가 자신의 계정으로 발급받은 Authorization Code를 피해자의 콜백 URL로 보내, 피해자를 공격자 계정에 로그인시킬 수 있음||

**Q7.** JWT의 Signature는 무엇을 보장하나?

||토큰 내용(Header + Payload)이 위변조되지 않았음을 보장. 서버의 Private Key로 서명하고 Public Key로 검증. 단, 암호화는 아님 — Payload는 Base64 디코딩으로 누구나 읽을 수 있음||

현재 단락 (1/127)

"Google로 로그인" 버튼을 누르면 무슨 일이 벌어질까요? 단 한 번의 클릭 뒤에는 **OAuth 2.0**이라는 정교한 프로토콜이 돌아갑니다.

작성 글자: 0원문 글자: 5,816작성 단락: 0/127