Proof Key for Code Exchange
PKCE란?
PKCE(Proof Key for Code Exchange, RFC 7636)는 OAuth 2.0의 Authorization Code Grant Flow를 보다 안전하게 만들기 위해 도입된 방법이다. 원래 모바일 앱과 SPA(Single Page Application) 같은 Public Client를 위해 고안되었지만, 현재 모든 OAuth 2.0 클라이언트에서 사용을 권장한다.
PKCE에서 클라이언트는 OAuth2 제공자에 Authorization Code를 요청할 때 code_challenge
를 함께 전송하고, 이후 토큰을 교환할 때 원본인 code_verifier
를 제시해 동일한 요청자임을 검증한다.
PKCE가 필요한 이유
기존 OAuth 2.0의 한계
전통적인 OAuth 2.0 Authorization Code Grant는 client_secret
을 사용해 클라이언트를 인증한다. 하지만 이 방식은 몇 가지 문제가 있었다:
1. Public Client의 보안 문제
- SPA나 모바일 앱에서는 클라이언트 시크릿을 안전하게 저장할 수 없다
- 소스코드에 하드코딩하거나 앱 번들에 포함시키면 누구나 추출할 수 있다
2. Authorization Code Interception
- 공격자가 리다이렉트 과정에서 Authorization Code를 가로챌 수 있다
- 특히 모바일 환경에서 Custom URL Scheme 사용 시 더욱 취약하다
3. CSRF 공격
- 공격자가 피해자의 계정에 자신의 소셜 계정을 연결시킬 수 있다
PKCE의 해결책
PKCE는 이러한 문제들을 다음과 같이 해결한다:
- 동적 시크릿: 매 요청마다 새로운
code_verifier
를 생성 - 단방향 암호화:
code_challenge
는code_verifier
를 SHA256으로 해시한 값 - 소유 증명: 토큰 교환 시에만 원본
code_verifier
를 사용
PKCE 작동 원리
위 시퀀스 다이어그램에서 보듯이 PKCE의 작동 과정은 다음과 같다:
1단계: Code Verifier 생성
1
2
// 43-128자의 랜덤 문자열 생성
const codeVerifier = generateRandomString(128);
2단계: Code Challenge 생성
1
2
// SHA256 해시 후 Base64URL 인코딩
const codeChallenge = base64url(sha256(codeVerifier));
3단계: Authorization 요청
OAuth Provider에게 code_challenge
와 code_challenge_method=S256
을 포함해 요청
4단계: 토큰 교환
Authorization Code를 받은 후, 토큰 교환 시 원본 code_verifier
를 함께 전송
5단계: 검증
OAuth Provider는 받은 code_verifier
를 해시하여 저장된 code_challenge
와 비교
PKCE 방식의 장점
보안성 강화
- Code Interception 방지: 공격자가 Authorization Code를 가로채더라도
code_verifier
없이는 토큰을 얻을 수 없다 - Client Secret 불필요: Public Client에서 시크릿 저장 문제를 완전히 해결
- CSRF 방어:
state
매개변수와 함께 사용하면 CSRF 공격을 효과적으로 방어
구현 편의성
- 표준화: RFC 7636으로 표준화되어 대부분의 OAuth Provider가 지원
- 하위 호환성: 기존 Authorization Code Grant와 완벽 호환
- 간단한 구현: 복잡한 서명이나 암호화 과정이 불필요
확장성
- 모든 클라이언트 타입에 적용 가능: Confidential Client에서도 추가 보안층으로 활용
- 미래 지향적: OAuth 2.1에서는 PKCE 사용이 필수가 될 예정
PKCE 방식의 단점
구현 복잡도
- 추가 로직 필요: 기존 OAuth 2.0 구현에 PKCE 로직을 추가해야 함
- 상태 관리:
code_verifier
와state
를 적절히 저장하고 관리해야 함 - 에러 처리: PKCE 검증 실패 시의 에러 처리 로직 필요
성능 오버헤드
- 해시 연산: SHA256 해싱 연산이 추가됨 (하지만 매우 미미함)
- 저장 공간: 서버 측에서
code_challenge
저장을 위한 추가 공간 필요
호환성 이슈
- 구형 Provider 미지원: 일부 오래된 OAuth Provider는 PKCE를 지원하지 않을 수 있음
- 클라이언트 업데이트: 기존 클라이언트들을 PKCE 지원하도록 업데이트해야 함
실제 구현에서의 고려사항
Code Verifier 생성
- 충분한 엔트로피: 최소 43자, 권장 128자의 랜덤 문자열
- 허용 문자:
A-Z
,a-z
,0-9
,-
,.
,_
,~
만 사용 - 암호학적 보안:
crypto.getRandomValues()
와 같은 안전한 난수 생성기 사용
상태 관리
- 적절한 만료시간: 보통 10분 내외로 설정
- 안전한 저장소: Redis나 메모리 기반 저장소 사용 권장
- 정리 작업: 만료된 상태 정보의 적절한 정리
에러 처리
1
2
3
4
// PKCE 검증 실패 시
if (receivedChallenge !== expectedChallenge) {
throw new Error('PKCE verification failed');
}
FlowDiagram
sequenceDiagram
participant FE as Client
participant BE as API Server
participant Redis as Redis
participant OAuth as OAuth Provider (Google)
Note over FE: 1. 소셜 로그인 클릭
FE->>FE: 2. code_verifier 생성
FE->>BE: 3. OAuth URL 요청<br/>POST /auth/oauth/url<br/>provider=google, code_verifier, redirectPath
Note over BE: 4. code_challenge 생성 & state 생성<br/>SHA256(code_verifier) → Base64URL
BE->>Redis: 5. state를 key로 OAuth 정보 저장<br/>SET state {provider, code_verifier, redirectPath, expiresAt} EX 600
BE->>FE: 6. OAuth URL 반환<br/>code_challenge 포함된 authUrl 응답
FE->>OAuth: 7. OAuth 인증 페이지로 리다이렉트<br/>GET authUrl (code_challenge 포함)
Note over OAuth: 8. 사용자 인증 및 동의
OAuth-->>FE: 9. 사용자 로그인 화면 표시
OAuth->>FE: 10. Authorization Code 반환<br/>Google: http://localhost:3000/auth/callback/google?code=xxx&state=yyy<br/>Kakao: http://localhost:3000/auth/callback/kakao?code=xxx&state=yyy
FE->>BE: 11. OAuth 콜백 처리<br/>POST /auth/oauth2/login<br/>state, provider, code 전달
Note over BE: 12. state 검증
BE->>Redis: 13. OAuth 정보 조회<br/>GET state
Redis->>BE: 14. OAuth 정보 반환<br/>{provider, code_verifier, redirectPath, expiresAt}
BE->>OAuth: 15. Access Token 요청<br/>POST /token<br/>code_verifier로 토큰 교환
OAuth->>BE: 16. Access Token 발급<br/>토큰 응답
BE->>OAuth: 17. 사용자 프로필 요청<br/>GET /userinfo<br/>Bearer 토큰으로 요청
OAuth->>BE: 18. 사용자 이메일 정보<br/>이메일 정보 응답
Note over BE: 19. 이메일로 회원가입/로그인 처리<br/>JWT 토큰 생성
BE->>FE: 20. 쿠키 설정 후 응답<br/>HttpOnly 쿠키로 토큰 설정<br/>로그인 성공 응답 + redirectPath 반환
Note over FE: 21. 로그인 완료<br/>Client에서 redirectPath로 리다이렉트 처리