콘텐츠로 바로가기

OAuth2 and OIDC

PKCE(Proof Key for Code Exchange): 코드 탈취 방지. SPA/모바일 앱에서 필수.

Article
M

Me

hyunyoun's Blog

software-engineering-devopssoftware-engineeringdev-opsarchitecturedesignoauth2oidcoauth2-and-oidc11 min read

1. Overview

OAuth2와 OIDC(OpenID Connect)는 내 서비스의 아이디와 비밀번호를 다른 서비스에 넘겨주는 미친 짓을 멈추고, 호텔 키 카드처럼 '제한된 출입증(Token)'만을 발급해 주는 현대 웹의 탈중앙화된 신원 위임(Identity Delegation) 역학을 해부합니다.

학습자는 제3자 앱(Client)이 내 구글 캘린더에 접근할 때 구글 비밀번호를 직접 요구하던 과거의 보안 파국을 뜯어봅니다. 나아가 권한 부여(Authorization)를 위한 **OAuth2의 4가지 역할(Resource Owner, Client, Auth Server, Resource Server)**과 토큰(Access Token)의 교환 메커니즘을 장악합니다. 마지막으로, OAuth2는 '출입증'일 뿐 '신분증'이 아님을 인지하고, 신원 인증(Authentication)을 완벽히 수행하기 위해 JWT 기반의 신분증(ID Token)을 덧붙인 OIDC(OpenID Connect) 역량을 확보합니다.

2. Scope & Boundaries

In-Scope

  • OAuth2 Physics: 인가(Authorization), Access Token, Refresh Token, 4 Roles (Resource Owner, Client, Authorization Server, Resource Server).
  • OAuth2 Flows: Authorization Code Grant (가장 안전한 표준), Implicit Grant (사양됨), Client Credentials Grant (기계 간 통신).
  • OIDC (OpenID Connect): 인증(Authentication), ID Token, JWT(JSON Web Token).
  • Security Boundaries: CSRF 방어를 위한 state 파라미터, 인가 코드(Code) 탈취 방지를 위한 PKCE.

Out-of-Scope

  • JWT 암호학적 서명(Signature): RSA, HMAC 등의 수학적 암호화/복호화 알고리즘 \rightarrow 10. Security & Cryptography로 위임.
  • Spring Security / Passport.js 구현체: 특정 언어 프레임워크의 OAuth2 클라이언트 세팅 코드 \rightarrow 프레임워크 매뉴얼로 분리.

Boundaries

  • Authentication(인증) vs Authorization(인가): OAuth2의 가장 거대한 오해를 부수어 버립니다. OAuth2는 "이 문을 통과할 권한(인가)"을 주는 호텔 키 카드일 뿐, "너의 이름이 무엇인지(인증)"를 증명하는 신분증이 아닙니다. OAuth2로 로그인을 구현하는 것은 편법이었으며, 이 한계를 극복하기 위해 OAuth2 위에 신분증(ID Token) 계층을 얹은 것이 OIDC임을 명확히 긋습니다.

3. Counterexample

  • 비밀번호의 양도 (The Anti-Pattern): 옛날엔 '페이스북 친구 찾기' 앱이 페이스북 아이디와 비밀번호를 직접 입력하라고 요구했습니다. 앱은 내 비밀번호를 DB에 저장해 두고 밤마다 내 계정으로 로그인해 데이터를 빼갔습니다. 만약 이 앱이 해킹당하면 내 페이스북은 통째로 털립니다. '제한된 토큰'이 아니라 '마스터키(비밀번호)'를 넘겨버린 통제권 상실의 비극입니다.
  • 토큰 탈취 (Implicit Grant): 주니어가 모바일 앱에 OAuth2를 달면서, 브라우저 주소창(URL)으로 Access Token을 바로 쏴주는 'Implicit Grant' 방식을 썼습니다. 지하철 공용 와이파이 해커가 패킷을 스니핑해서 주소창의 토큰을 훔쳐 내 계정을 탈취했습니다. 토큰을 주소창에 노출하는 방식의 치명적 설계 결함을 뜯어봅니다.

4. Prerequisites

  • HTTP와 통신 (Basic): HTTP Request, Redirect(302), HTTPS (SSL/TLS). (08. Network)
  • 인가와 인증 (Basic): Session/Cookie 기반의 전통적인 로그인 방식. (08. Network)

5. Learning Map

Sequence Core Cluster Objective & Description Evidence (BoK)
1 The Anatomy of OAuth2 마스터키(비밀번호) 대신 1시간짜리 호텔 키 카드(Token)를 쥐여주는 권한 위임의 4가지 등장인물과 춤(Dance)을 쥡니다. P1
2 Authorization Code Grant 왜 토큰을 바로 안 주고 '인가 코드(Code)'라는 교환권을 먼저 주는가? 토큰 탈취를 백엔드에서 방어하는 완벽한 표준 흐름을 뜯어봅니다. P5
3 Authentication vs Authorization 출입증(OAuth2)과 신분증(OIDC)은 다르다! OAuth2의 설계 결함을 부수고 JWT 신분증을 발급하는 OpenID Connect를 장악합니다. Industry
4 PKCE & Security Physics 모바일 앱에서 인가 코드를 가로채려는 해커를 엿먹이기 위해, '내가 아까 그놈이다'를 증명하는 암호학적 자물쇠(PKCE)를 확보합니다. Industry

6. Learning Topics

Basic

Core Topic 01: 호텔 키 카드의 발급 (The Anatomy of OAuth2)

  • Why to Learn: 사용자의 구글 비밀번호를 내 서비스 DB에 저장하는 멍청한 짓을 막고, 언제든 구글 쪽에서 권한을 취소할 수 있는 '안전한 대리인(Delegation)' 아키텍처를 설계하기 위함입니다.
  • What to Learn:
    • Concepts: OAuth 2.0, Resource Owner (사용자), Client (내 서비스), Authorization Server (구글 인증 서버), Resource Server (구글 캘린더 API).
    • Skills: "내 서비스가 구글 캘린더에 접근한다"는 시나리오에서 4가지 역할(Role)을 정확히 매핑하고, Access Token의 라이프사이클을 설계하기.
  • How to Learn:
    • 1단계: 등장인물 4인방: 당신(Resource Owner)은 내 서비스(Client)에게 구글 캘린더(Resource Server)를 볼 권한을 주려 합니다. 허락을 내리는 곳은 구글 인증 서버(Authorization Server)입니다.
    • 2단계: 호텔의 비유: 당신은 호텔 방(자원)의 주인입니다. 친구(Client)에게 방에 있는 짐을 가져오라고 시킵니다. 마스터키(비밀번호)를 주면 친구가 금고까지 다 열어봅니다. 그래서 프론트데스크(Auth Server)에 가서 "내 친구에게 1시간 동안 문만 열 수 있는 '키 카드(Token)'를 줘"라고 허락(Grant)합니다.
    • 3단계: 권한의 범위 (Scope): 키 카드를 발급할 때, 구글은 묻습니다. "이 앱이 당신의 캘린더를 '읽기'만 할 건가요, '쓰기'도 할 건가요?" 이것이 Scope입니다. 최소 권한의 원칙에 따라 통제권을 완벽히 분리하는 위임의 물리법칙을 해부합니다.
  • Implement: OAuth2 등장인물 매핑 렌더링. 요구사항: '스파르타코딩' 앱이 '카카오톡' 친구 목록을 불러온다. Resource Owner: 스마트폰을 들고 있는 '나'. Client: '스파르타코딩' 앱. Auth Server: 카카오 로그인 서버 (토큰 발급). Resource Server: 카카오 친구 API 서버 (토큰 검증 후 데이터 반환). 권력의 4분할 시각화.

Core Topic 02: 인가 코드의 교환술 (Authorization Code Grant)

  • Why to Learn: 구글이 토큰을 발급할 때 브라우저 주소창(URL)으로 던져주면 지나가는 해커가 다 훔쳐보므로, 오직 백엔드 서버끼리만 은밀하게 토큰을 교환하는 가장 안전한 표준 흐름을 짜기 위함입니다.
  • What to Learn:
    • Concepts: Authorization Code (인가 코드), Redirect URI, Client ID & Client Secret, Back-channel (백채널 통신).
    • Skills: 프론트엔드가 주소창으로 '토큰' 대신 일회용 '인가 코드(Code)'를 받아 백엔드에 넘기면, 백엔드가 Client Secret을 들고 구글과 몰래 통신(Back-channel)하여 진짜 '토큰'으로 교환하는 3-Way 댄스 설계하기.
  • How to Learn:
    • 1단계: 주소창의 공포 (Implicit): 구글 로그인을 성공했습니다. 구글이 주소창에 myapp.com/callback?token=abcdef라고 쏴줍니다. 이 URL은 브라우저 히스토리, 통신사 프록시, 카페 와이파이에 전부 기록(노출)됩니다. 최악입니다.
    • 2단계: 교환권의 발행 (Authorization Code): 구글은 토큰 대신 1분짜리 임시 '교환권(Code)'을 주소창으로 쏴줍니다. myapp.com/callback?code=12345. 해커가 이걸 훔쳐도, 이 교환권은 껍데기일 뿐입니다.
    • 3단계: 백채널 통신 (Back-channel): 내 백엔드 서버가 프론트에서 넘어온 이 code를 받습니다. 그리고 아무도 훔쳐볼 수 없는 서버-투-서버(Server-to-server) 통신으로 구글에 접속해 codeClient Secret(서버만 아는 비밀번호)을 내밀고, 비로소 진짜 Access Token을 받아옵니다. 프론트엔드(브라우저)에는 절대 토큰이 노출되지 않는 극강의 보안 댄스를 뜯어봅니다.
  • Implement: Authorization Code Grant 시퀀스 다이어그램 모사. Step 1: 유저가 구글 로그인. 구글 \rightarrow 프론트엔드 (302 Redirect with code). Step 2: 프론트엔드 \rightarrow 내 백엔드 (code 전달). Step 3: 내 백엔드 \rightarrow 구글 Auth 서버 (code + client_secret 전달). Step 4: 구글 Auth 서버 \rightarrow 내 백엔드 (Access Token 발급). 해커가 code를 탈취해도 client_secret이 없어 토큰 교환에 실패하는 완벽한 방어 시각화.

Practical

Core Topic 03: 출입증과 신분증의 융합 (OIDC - OpenID Connect)

  • Why to Learn: "OAuth2로 로그인을 구현했습니다!"라는 주니어에게 "OAuth2는 인가(Authorization) 프레임워크지, 인증(Authentication) 프로토콜이 아니다"라는 충격적인 진실을 알려주고 OIDC라는 신분증을 발급받기 위함입니다.
  • What to Learn:
    • Concepts: OpenID Connect (OIDC), ID Token, Authentication vs Authorization, JWT (JSON Web Token), UserInfo Endpoint.
    • Skills: OAuth2로 받아온 Access Token을 들고 다시 구글 API를 찔러 유저 정보를 가져오는 헛수고를 버리고, 한 번에 JWT 형식의 id_token을 받아와 디코딩(Decoding)하여 즉시 로그인 처리하기.
  • How to Learn:
    • 1단계: 가짜 로그인 (Pseudo-Authentication): OAuth2 토큰은 "캘린더를 볼 권한"입니다. 옛날엔 이 토큰을 받으면 "아, 구글이 토큰을 줬으니 정상 유저구나" 하고 그냥 로그인 처리를 했습니다. 만약 해커가 다른 앱에서 탈취한 토큰을 내 앱에 들이밀면? 내 앱은 누군지도 모르고 문을 열어줍니다(Confused Deputy Problem).
    • 2단계: 신분증의 탄생 (OIDC): 권한(출입증) 말고 신원(신분증)을 증명할 표준이 필요했습니다. 구글, 마이크로소프트 등이 모여 OAuth2 위에 신원 인증 레이어인 OIDC를 올렸습니다.
    • 3단계: ID Token (JWT): OIDC를 쓰면(scope에 openid 추가), 구글은 Access Token과 함께 id_token을 던져줍니다. 이 안에는 "이름: 홍길동, 이메일: hong@, 발급자: 구글, 발급대상: 내 앱"이라는 정보가 위변조 불가능한 서명(JWT)과 함께 들어있습니다. 서버를 찌를 필요 없이 즉시 유저를 인증하는 프로토콜의 진화를 해부합니다.
  • Implement: ID Token 디코딩 및 검증(Validation) 렌더링. Action: 구글에서 받은 id_token의 Payload를 Base64 디코딩. Payload: { "iss": "https://accounts.google.com", "aud": "내앱의_클라이언트ID", "sub": "1234567890", "email": "test@gmail.com" }. 검증: "발급자(iss)가 구글인가? 수신자(aud)가 우리 앱이 맞는가?"를 체크하여 탈취 토큰의 릴레이 공격을 완벽히 차단하는 시각화.

Advanced

Core Topic 04: 모바일 앱의 딜레마와 PKCE (Proof Key for Code Exchange)

  • Why to Learn: 모바일 앱(iOS/Android) 환경에서는 Client Secret을 소스코드에 안전하게 숨길 방법이 전혀 없으므로, Secret 없이도 인가 코드가 탈취당하는 것을 막는 암호학적 자물쇠(PKCE)를 채우기 위함입니다.
  • What to Learn:
    • Concepts: Public Client vs Confidential Client, PKCE (Proof Key for Code Exchange), Code Verifier, Code Challenge, SHA-256.
    • Skills: 프론트엔드(SPA)나 모바일 앱처럼 코드가 100% 털리는 환경(Public Client)에서 OAuth2를 구현할 때, 동적으로 생성한 난수(Verifier)와 해시값(Challenge)을 이용해 탈취범을 엿먹이기.
  • How to Learn:
    • 1단계: 비밀의 부재 (Public Client): 백엔드 서버(Confidential)는 client_secret.env에 숨길 수 있습니다. 하지만 모바일 앱(Public)에 하드코딩하면 앱을 디컴파일해서 다 털립니다. Secret을 쓸 수 없습니다.
    • 2단계: 인터셉트 공격: Secret이 없으니 모바일 앱은 그냥 인가 코드(code)만 구글에 던지면 토큰을 받습니다. 악성 앱이 내 스마트폰에 깔려서, myapp://으로 돌아오는 code를 가로채서 구글에 던집니다. 구글은 Secret을 검사 안 하니 그냥 토큰을 내줍니다. 계정이 털렸습니다.
    • 3단계: PKCE의 맹세: 로그인을 시작할 때, 앱이 난수(Verifier)를 하나 만들고 이를 SHA-256으로 해시(Challenge)해서 구글에 던집니다. "나중에 code 교환하러 올 놈은 이 해시의 원본(Verifier)을 알고 있는 놈이어야 해!" 나중에 악성 앱이 가로챈 code를 들고 구글에 가봤자 원본 난수를 모르므로 거절당합니다. 상태 유지를 기반으로 한 동적 방어술을 뜯어봅니다.
  • Implement: PKCE 암호학적 증명 프로세스 모사. : 난수 apple(Verifier) 생성 \rightarrow 해시 XYZ123(Challenge) 생성. Step 1: 앱 \rightarrow 구글. 로그인 요청 시 Challenge=XYZ123 동봉. Step 2: 구글 \rightarrow 앱. code=777 발급. (악성 앱이 777 탈취). Step 3 (악성 앱): 구글에 code=777 제출. 구글: "Verifier 내놔." 악성 앱: "모름." \rightarrow 차단. Step 4 (정상 앱): 구글에 code=777 + Verifier=apple 제출. 토큰 정상 발급. Secret 없이도 완벽한 인증을 구현하는 마법 렌더링.

7. Terminology

Term (EN / ko, abbr) 1문장 정의 단계(기본/권장/실무/심화) 역할/맥락 관련 개념 유사/대비/함께 사용 오해 포인트 Evidence(Primary/Secondary/Industry) Flags(core)
OAuth 2.0 사용자의 아이디와 비밀번호를 넘기지 않고, '접근 권한이 한정된 토큰'만을 발급해 서드파티 앱이 내 데이터를 사용할 수 있게 허락하는 인가(Authorization) 프레임워크입니다. 기본 자원 접근 권한의 위임(Delegation) Access Token / Scope OIDC (OpenID Connect) OAuth 2.0은 '인가(권한 부여)'를 위한 것이지, '인증(로그인)'을 위한 표준이 아님. 로그인을 흉내 낼 수는 있지만 취약점이 존재함 P1:CS2023 core
Authorization Code Grant (인가 코드 승인) 클라이언트(프론트엔드)가 토큰을 직접 받지 않고 일회용 '인가 코드(code)'만 받은 뒤, 백엔드 서버가 이를 건네받아 비밀키(Secret)와 함께 토큰으로 교환하는 가장 안전한 표준 흐름입니다. 권장 토큰 탈취 방지 및 서버-투-서버 보안 통신 Client Secret / Back-channel Implicit Grant (암묵적 승인) Implicit Grant는 토큰을 브라우저 주소창(Fragment)으로 바로 던져주는 낡고 위험한 방식으로, 현재 보안 표준에서는 절대 사용 금지(Deprecated)됨 P5:SFIA core
OpenID Connect (OIDC) OAuth 2.0 프로토콜 위에 얹혀진 '신원 인증(Authentication)' 레이어로, 유저의 이름과 이메일 정보가 암호학적으로 서명된 신분증(ID Token)을 추가로 발급해 줍니다. 실무 안전하고 표준화된 소셜 로그인(SSO) 구현 ID Token / JWT OAuth 2.0 Access Token이 문을 여는 '호텔 키 카드'라면, ID Token은 프론트데스크에서 신원을 확인하는 '여권(Passport)'의 역할을 수행함 Industry core
PKCE (Proof Key for Code Exchange) Client Secret을 숨길 수 없는 모바일 앱이나 SPA(리액트 등) 환경에서, 인가 코드를 탈취하려는 악성 앱을 막기 위해 매번 동적으로 해시값(Challenge)을 주고받는 보안 기법입니다. 심화 Public Client 환경에서의 인가 코드 탈취 방어 Code Verifier / Public Client Client Secret (비밀키) 백엔드 서버(Confidential Client)가 있다고 해서 PKCE를 안 써도 되는 건 아님. 최근 OAuth 2.1 초안에서는 모든 클라이언트가 무조건 PKCE를 쓰도록 강제하고 있음 Industry core

8. References

Primary

  • [P1] CS2023 - Security (SEC) - Authentication and Authorization (OAuth2, OIDC)
  • [P5] SFIA - Information Security (SCTY) - Access control protocols

Secondary

  • [RFC 6749] The OAuth 2.0 Authorization Framework
  • [OpenID Connect Core 1.0] - OIDC specification and ID Token (JWT)

Industry

  • [Auth0 Docs] - Which OAuth 2.0 Flow Should I Use? (Authorization Code with PKCE)
  • [Okta Developer] - OAuth 2.0 and OpenID Connect (in plain English)

9. Final Checklist

Primary

  • 내 서비스(Client)가 구글 캘린더 API(Resource Server)에 접근하기 위해 사용자의 아이디/비밀번호를 통째로 넘겨받는 옛날 방식의 보안적 파국을 설명하고, OAuth2가 호텔 키 카드(Token) 발급을 통해 이를 어떻게 해결했는지 논증할 수 있는가?
  • OAuth2 흐름 중 토큰을 주소창으로 직접 쏴주는 암묵적 승인(Implicit Grant) 방식이 폐기(Deprecated)된 이유를 설명하고, 인가 코드(Code)를 받은 뒤 백엔드 서버끼리 통신(Back-channel)하여 토큰을 교환하는 Authorization Code Grant의 3-way 구조를 설계할 수 있는가?

Secondary

  • "OAuth 2.0은 인가(Authorization)를 위한 것이지 인증(Authentication)을 위한 것이 아니다"라는 명제를 기반으로, 악의적인 앱이 탈취한 토큰을 내 서비스에 들이밀어 로그인을 뚫어내는 취약점을 증명할 수 있는가?
  • OAuth 2.0의 한계를 극복하기 위해 OIDC(OpenID Connect)가 발급하는 id_token(JWT)의 내부 구조(Payload의 iss, aud, sub)를 디코딩하여, 서버를 한 번 더 찌르지 않고도 즉시 사용자의 신원(신분증)을 안전하게 검증(Validation)할 수 있는가?

Industry

  • 클라이언트 시크릿(Client Secret)을 안전하게 보관할 수 없는 모바일 앱이나 React 기반의 SPA(Public Client) 환경에서, 악성 앱이 딥링크(URI Scheme)를 가로채 인가 코드를 탈취하는 공격을 어떻게 방어할 것인가?
  • 위 탈취 공격을 무력화하기 위해, 로그인 시작 시 동적으로 난수(Verifier)를 생성하고 해시(Challenge)하여 구글에 던져두는 PKCE(Proof Key for Code Exchange)의 암호학적 상태 유지 매커니즘을 렌더링할 수 있는가?

Architecture & Documentation

5 / 5