OAuth 2.0 Authorization Code Flow 동작 원리: 비밀번호 없는 인증이 필요한 이유
OAuth 2.0 Authorization Code Flow가 비밀번호 없이 인증하는 5단계 구조를 분석한다. redirect_uri 조작부터 PKCE까지 보안 위협과 방어를 다룬다.
2010년 이전 웹 애플리케이션 시대를 상상해보세요. 트위터에 자동으로 글을 올려주는 서드파티 앱을 사용하려면, 그 앱에 트위터 아이디와 비밀번호를 직접 입력해야 했다. 이는 심각한 보안 문제를 야기했다.
비밀번호 직접 공유 방식은 다음과 같은 문제를 가졌습니다:
사용자가 여러 서드파티 앱을 사용한다면, 그만큼 많은 곳에 비밀번호를 맡겨야 했고, 그 중 한 곳이라도 해킹당하면 모든 계정이 위험해졌다.
한 줄 요약: OAuth 2.0 Authorization Code Flow는 비밀번호 공유 없이 제한된 권한을 안전하게 위임하기 위해 만들어졌다.
OAuth 2.0 Authorization Code Flow
OAuth 2.0 Authorization Code Flow는 사용자가 비밀번호를 공유하지 않고도 제3자 애플리케이션이 특정 권한으로 사용자 데이터에 접근할 수 있게 하는 인증 방식이다.
The authorization code grant type is used to obtain both access tokens and refresh tokens and is optimized for confidential clients. Since this is a redirection-based flow, the client must be capable of interacting with the resource owner's user-agent (typically a web browser) and capable of receiving incoming requests (via redirection) from the authorization server. — RFC 6749, Section 4.1
RFC 정의를 풀어보면, Authorization Code Grant는 액세스 토큰과 리프레시 토큰을 모두 얻을 수 있으며, 클라이언트 비밀을 안전하게 보관할 수 있는 기밀 클라이언트에 최적화된 방식이다.
| 용어 | 설명 |
|---|---|
| Authorization Code | 액세스 토큰 교환용 임시 코드. 일회성이며 수 분 내 만료 |
| Access Token | API 호출 시 사용하는 토큰. 제한된 권한(scope), 1-24시간 유효 |
| Refresh Token | 액세스 토큰 갱신용 장기 토큰. 30일-1년 유효 |
| Client Secret | 클라이언트 앱 인증용 비밀키. 서버에만 저장, 수동 갱신 |
OAuth 2.0 Authorization Code Flow는 이중 인증 구조 를 가집니다. 먼저 사용자가 권한을 부여하면 임시 코드를 받고, 이 코드를 실제 토큰으로 교환하는 2단계 과정을 거칩니다.
이 흐름에서 핵심은 5단계와 6단계의 분리 이다. Authorization Code(5단계)만으로는 리소스에 접근할 수 없고, 반드시 Client Secret과 함께 토큰 교환(6단계)을 거쳐야 한다. 이 이중 구조가 OAuth 2.0 Authorization Code Flow의 보안 핵심이다.
사용자가 제3자 앱에서 "Google로 로그인" 버튼을 클릭하면, 앱은 사용자를 Google의 권한 부여 서버로 리다이렉트한다.
GET /oauth/authorize?
response_type=code&
client_id=s6BhdRkqt3&
state=xyz&
redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
scope=read_profile
Host: authorization-server.com
각 파라미터의 역할:
response_type=code: Authorization Code를 요청한다는 의미client_id: 미리 등록된 클라이언트 앱의 식별자state: CSRF 공격 방지용 랜덤 값redirect_uri: 인증 완료 후 돌아올 주소scope: 요청하는 권한 범위권한 부여 서버는 사용자에게 로그인 화면과 권한 승인 화면을 보여줍니다. 사용자가 승인하면 Authorization Code가 생성된다.
HTTP/1.1 302 Found
Location: https://client.example.com/cb?
code=SplxlOBeZQQYbYS6WxSbIA&
state=xyz
핵심 보안 특징:
code는 일회성 이며 짧은 시간 내에 사용해야 함 (RFC 6749 Section 4.1.2에서는 최대 10분을 권고)state 값이 1단계와 동일한지 검증하여 CSRF 공격 방지클라이언트 앱은 받은 Authorization Code와 자신의 Client Secret 을 함께 권한 부여 서버에 전송한다.
POST /oauth/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
client_id=s6BhdRkqt3&
client_secret=gX1fBat3bV
왜 Client Secret이 필요한가:
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read_profile"
}
토큰 구조 분석:
access_token: 실제 API 호출에 사용하는 토큰expires_in: 3600초(1시간) 후 만료refresh_token: 액세스 토큰 갱신용 장기 토큰scope: 실제로 부여된 권한 범위GET /api/profile HTTP/1.1
Host: resource-server.com
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
리소스 서버는 액세스 토큰을 검증하고 요청된 리소스를 반환한다.
OAuth 2.0 Authorization Code Flow는 호텔 키카드 발급 과정 과 비슷합니다:
임시 영수증(Authorization Code)만으로는 객실에 들어갈 수 없고, 반드시 예약 확인서와 함께 제시해야 키카드를 받을 수 있는 구조이다.
리소스 서버가 액세스 토큰을 받으면 어떻게 검증할까요?
토큰 검증은 다음 순서로 진행됩니다:
Authorization: Bearer <token> 형식인지 검사exp 클레임이 현재 시간 이후인지 검사액세스 토큰이 만료되면 Refresh Token으로 새로운 액세스 토큰을 발급받습니다:
POST /oauth/token HTTP/1.1
Host: authorization-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&
client_id=s6BhdRkqt3&
client_secret=gX1fBat3bV
Refresh Token의 보안 설계:
PKCE (Proof Key for Code Exchange) 확장
모바일 앱이나 SPA(Single Page Application)처럼 Client Secret을 안전하게 저장할 수 없는 환경에서는 PKCE 를 사용합니다:
# 1단계: code_challenge 생성
GET /oauth/authorize?
response_type=code&
client_id=s6BhdRkqt3&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
# 2단계: code_verifier로 검증
POST /oauth/token
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
PKCE는 Client Secret 대신 동적으로 생성된 code_challenge/code_verifier 쌍 을 사용하여 보안을 강화한다.
Authorization Code Flow의 핵심 질문: "왜 Authorization Server가 바로 Access Token을 주지 않고 굳이 Code를 거치는가?"
이유 1: 프론트채널 vs 백채널 분리
이유 2: Client 인증
이유 3: 일회성 보장
브라우저 (프론트채널) 서버 (백채널)
│ │
│ ←── Authorization Code ───│ (노출 가능하지만 단독 사용 불가)
│ │
│ │──→ Code + Client Secret ──→ Auth Server
│ │←── Access Token ←─────────────│
│ │ (안전한 서버 간 통신)
PKCE 챌린지 응답 흐름
OAuth 2.0 Authorization Code Flow에서 공격자는 주로 Authorization Code 탈취 와 토큰 교환 과정 조작 을 노립니다. 이 흐름의 핵심 약점은 브라우저 리다이렉트 에 의존한다는 점이다.
CVE-2023-6927: Keycloak 오픈 리다이렉트 취약점
공격 시나리오: Keycloak < 23.0.4 버전에서 와일드카드로 끝나는 redirect URI를 가진 OAuth 2.0 클라이언트는 redirect URI 검증을 우회할 수 있었다.
# 정상적인 redirect URI
https://app.example.com/callback
# 공격자가 조작한 URI
https://app.example.com/callback/../../../evil.com/steal
공격 과정:
방어 방법:
CVE-2023-28131: Expo Auth Session 라이브러리 취약점
공격 메커니즘: expo-auth-session 라이브러리에서 authorization code가 안전하지 않은 채널을 통해 전송되어 공격자가 탈취할 수 있었다.
// 취약한 코드 예시
const result = await AuthSession.startAsync({
authUrl: `https://provider.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}`,
// returnUrl이 안전하지 않게 처리됨
});
// 공격자가 authorization code 탈취 후 토큰 교환
const tokenResponse = await fetch('https://provider.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=authorization_code&code=${stolenCode}&client_id=${clientId}`
});
피해 범위:
공격 원리: ConsentFix는 OAuth 2.0 authorization code flow를 악용하여 Microsoft Entra ID의 장치 규정 준수 확인과 조건부 액세스 정책을 우회한다.
# 공격자가 생성한 악의적 로그인 URI
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=legitimate_client_id&
response_type=code&
redirect_uri=http://localhost:8080/callback&
scope=https://graph.microsoft.com/.default&
state=malicious_state
공격 단계:
공격 코드:
<a href="https://oauth-provider.com/authorize?
response_type=code&
client_id=victim_app_id&
redirect_uri=https://victim-app.com/callback&
scope=read_write
">무료 쿠폰 받기</a>
피해자가 이미 OAuth 제공자에 로그인된 상태라면, 공격자의 계정이 피해자의 OAuth 앱 계정과 연결된다.
Authorization Server 측
Client 측
OAuth 2.0 Authorization Code Flow는 비밀번호 공유 없이 안전한 권한 위임을 구현하는 표준 방식으로, 이중 인증 구조를 통해 보안을 강화했지만 구현 과정에서 다양한 취약점이 발생할 수 있어 신중한 설계가 필요한다.
현대 웹 보안에서 OAuth는 핵심 인증 인프라가 되었지만, 네트워크 ACL 우회: 방화벽 정책을 무력화하는 원리에서 다룬 것처럼 보안 정책 우회 공격의 대상이 되기도 한다. 특히 다운그레이드 공격: 보안 기능이 무력화되는 원리에서 설명한 보안 기능 무력화 패턴은 OAuth 구현에서도 자주 발견된다.
OAuth 2.0에서 사용하는 토큰의 구조를 이해하려면 JWT를 알아야 한다. JWT 동작 원리와 보안 취약점에서 토큰의 서명 검증 원리와 보안 취약점을 확인해보세요.
다음에 알아볼 심화 주제:
Public Client(SPA, 모바일 앱)는 Client Secret을 안전하게 저장할 수 없다. 코드 탈취 공격(Authorization Code Interception)을 방어하기 위해 PKCE(Proof Key for Code Exchange)가 RFC 7636으로 표준화됐다.
동작은 세 단계다. 클라이언트가 랜덤 code_verifier를 생성하고, 이의 SHA256 해시인 code_challenge를 인가 요청에 포함한다. 토큰 교환 시 원본 code_verifier를 전송하면 인가 서버가 해시를 비교 검증한다. 중간에서 Authorization Code를 탈취하더라도 code_verifier 없이는 토큰을 발급받을 수 없다.
Access Token의 짧은 수명은 보안의 핵심이다. 탈취되더라도 15분이면 만료된다. 하지만 사용자에게 매번 재로그인을 요구할 수는 없다. Refresh Token이 이 문제를 해결한다.
Refresh Token Rotation은 최신 보안 권장사항이다. Refresh Token을 사용할 때마다 새 Refresh Token을 발급하고, 이전 토큰을 무효화한다. 만약 이미 사용된 Refresh Token이 재사용되면 토큰 탈취로 판단하고 해당 사용자의 모든 토큰을 즉시 무효화한다. Auth0은 이를 "Automatic Reuse Detection"이라 부른다.
OAuth 2.1(RFC 작업 중)은 기존 2.0의 보안 취약점을 정리한다. Implicit Grant 타입이 공식 제거된다. 이 방식은 Access Token이 URL fragment에 노출되어 탈취 위험이 높았다. PKCE가 모든 클라이언트 유형에서 필수가 된다. Client Credentials를 사용하는 Confidential Client도 예외가 아니다.
Resource Owner Password Credentials(ROPC) Grant도 제거된다. 사용자가 클라이언트 앱에 직접 비밀번호를 입력하는 이 방식은 피싱 공격과 구별이 불가능하기 때문이다.
Redirect URI 검증 부족은 가장 위험한 구현 실수다. 인가 서버가 와일드카드 패턴(*.example.com)을 허용하면 공격자는 서브도메인을 탈취하여 Authorization Code를 가로챌 수 있다. 정확한 문자열 일치(exact match)만 허용해야 한다.
state 파라미터 누락은 CSRF 공격을 가능하게 한다. 공격자가 자신의 Authorization Code를 피해자의 세션에 연결하여 계정을 탈취할 수 있다. state에 CSRF 토큰을 포함하고 콜백 시 검증해야 한다.
기존 Authorization Code Flow는 클라이언트 시크릿으로 인가 코드의 유효성을 보장한다. 하지만 모바일 앱이나 SPA(Single Page Application)는 클라이언트 시크릿을 안전하게 저장할 수 없다. 앱을 디컴파일하거나 브라우저 개발자 도구로 소스코드를 확인하면 시크릿이 노출된다.
PKCE(Proof Key for Code Exchange, RFC 7636)는 이 문제를 해결한다. 클라이언트가 매 인증 요청마다 랜덤한 code_verifier를 생성하고, 이를 SHA-256으로 해싱한 code_challenge를 인가 서버에 전송한다. 인가 코드를 토큰으로 교환할 때 원본 code_verifier를 함께 보내면, 서버가 해시값을 비교하여 요청의 정당성을 검증한다.
공격자가 인가 코드를 가로채더라도 원본 code_verifier를 모르면 토큰 교환이 불가능하다. code_verifier는 최소 43자, 최대 128자의 랜덤 문자열이어야 하며, 클라이언트 메모리에만 존재하고 네트워크로 전송되지 않는다.
2024년 Microsoft Azure의 OAuth 구현에서 발견된 취약점은 redirect_uri 검증의 중요성을 보여준다. 공격자가 하위 도메인 탈취(Subdomain Takeover)를 통해 정상 redirect_uri와 일치하는 도메인을 장악하고 인가 코드를 탈취한 사건이다. redirect_uri를 정확한 전체 URL로 검증하지 않고 도메인 패턴 매칭만 적용한 것이 원인이었다.
OAuth 토큰 저장 위치도 보안의 핵심이다. 브라우저 환경에서 localStorage에 토큰을 저장하면 XSS를 통해 탈취될 수 있고, 쿠키에 저장하면 CSRF 위험이 있다. 현재 권장되는 방식은 BFF(Backend for Frontend) 패턴이다. 토큰을 서버 측에 저장하고, 브라우저에는 httpOnly 세션 쿠키만 발급한다.
OAuth 2.1은 기존 OAuth 2.0의 보안 취약점을 체계적으로 개선한 업데이트다. 가장 중요한 변경은 Implicit Flow의 공식 폐지다. Implicit Flow는 access_token이 URL fragment에 노출되어 브라우저 히스토리나 리퍼러 헤더를 통한 토큰 유출 위험이 있었다. OAuth 2.1에서는 모든 클라이언트가 Authorization Code Flow를 사용하도록 강제한다.
두 번째 변경은 PKCE(Proof Key for Code Exchange)의 필수화다. 기존에는 퍼블릭 클라이언트에만 권장되던 PKCE가 모든 클라이언트 유형에 필수로 적용된다. 이는 Authorization Code 가로채기 공격을 원천 차단한다. 세 번째로 refresh_token 교체(rotation)가 기본 정책으로 채택됐다. refresh_token 사용 시 새로운 refresh_token을 발급하고 기존 토큰을 즉시 폐기하여, 탈취된 토큰의 재사용을 방지한다.
인증 보안 관련
관련 보안 사건
AI 활용 안내 이 글은 AI(Claude)의 도움을 받아 작성되었습니다. 인용된 통계와 사례는 참고 자료에 명시된 출처에 근거하며, 설명을 위한 일부 표현은 각색되었습니다.
면책 조항 본 글은 보안 인식 제고를 위한 교육 목적으로 작성되었습니다. 언급된 공격 기법을 실제로 시도하는 행위는 「정보통신망법」, 「형법」 등에 따라 처벌받을 수 있으며, 본 블로그는 이에 대한 법적 책임을 지지 않습니다.