보안정보

전문화된 보안 관련 자료, 보안 트렌드를 엿볼 수 있는
차세대 통합보안관리 기업 이글루코퍼레이션 보안정보입니다.

Spring Security: Part2. OAuth 2.0 아키텍처 이해와 보안 취약점 사례

2024.10.08

6,650

Part1. 보러가기

01. OAuth 2.0 개요

1) OAuth(Open Authorization)

Spring Security에 대한 시리즈 중 첫 번째인 ‘Spring Security : Part1. 인증과 인가 아키텍처의 이해와 실전 활용’에 이어 2번째 시리즈로 ‘OAuth 2.0 아키텍처 이해와 보안 취약점 사례’에 대해서 살펴보고자 한다.

인증(Authentication)은 시스템이나 서비스가 사용자의 신원을 확인하는 과정이다. 이를 통해 사용자의 신원을 식별하고 무분별한 접근을 차단함으로써 데이터 유출을 방지할 수 있다. 때문에, 인증은 보안의 핵심 요소로 웹 서비스 개발 및 설계 시 중요한 고려사항이다.

현대 IT 환경에서는 신원 확인을 위한 다양한 인증 수단이 제공된다. 대표적인 방법으로는 비밀번호 기반 인증, 2단계 인증(Two-Factor Authentication, 2FA), 생체 인식, 토큰 기반 인증, 그리고 인증서 기반 인증이 있다. 그러나 이러한 인증 수단은 사용자가 단일 애플리케이션에 직접적으로 인증 정보를 제공하는 방식이다. 이로 인해 애플리케이션이 사용자 인증 정보를 관리하게 되며, 정보 유출의 보안 위험이 상존하게 된다. 특히 하나의 서비스에서 유출된 정보는 다른 서비스에도 영향을 미칠 수 있다. 이러한 배경 속에서 더욱 안전하고 효율적인 인증 및 권한 부여 메커니즘을 제공하고자 OAuth(Open Authorization, 이하 OAuth)가 등장하게 되었다.

OAuth는 웹, 모바일 및 데스크톱 애플리케이션에서 널리 사용되는 개방형 표준 인증 프로토콜(RFC 6749)이다. 사용자의 인증 정보를 직접 공유하지 않고도 서비스 간에 안전한 인증과 권한 위임을 가능하게 한다. 이를 통해 사용자는 자신의 계정 정보를 제 3자 애플리케이션에 직접 제공하지 않고도, 해당 애플리케이션이 특정 서비스의 기능을 사용할 수 있도록 권한을 부여할 수 있다.

이는 기존의 중앙집중식 인증 모델(LDAP, Active Directory 등)의 한계를 극복하고, 각 서비스가 사용자의 민감한 인증 정보를 직접 저장하고 관리해야 하는 부담을 줄여준다. 결과적으로 OAuth는 보안성을 강화하면서도 사용자 경험을 개선하고, 서비스 간의 연동을 용이하게 만들어 현대 웹 생태계에서 새로운 인증과 인가 프레임워크의 기반이 되었다.

2) OAuth 버전

[그림 1] 소셜 로그인 페이지 화면

온라인 활동에서 회원가입이나 로그인 시에 [그림 6-1]과 같은 소셜 로그인 기능을 접해본 경험이 있을 것이다. 소셜 로그인은 Facebook, Google, Naver, GitHub 등의 소셜 네트워킹 서비스 계정 정보를 활용하여 다른 애플리케이션이나 플랫폼에 간편하게 로그인하거나 회원가입 할 수 있는 인증 방식이다. 그리고 이러한 소셜 로그인 기능의 핵심 기반 기술이 바로 OAuth이다.

OAuth에는 1.0, 2.0, 2.1의 세 가지 주요 버전이 있으며, 현재 널리 사용되고 있는 버전은 OAuth 2.0이다. 각 버전 간의 주요 차이점은 인증 방식과 범용성에 있으며, 버전별 인증 프로세스에 대해 자세히 살펴보겠다.

[그림 2] OAuth 1.0 워크플로우

OAuth 1.0에서는 참여자를 역할에 따라 사용자(User), 소비자(Consumer), 그리고 서비스 제공자(Service Provider)로 구분 지으며, 각 역할에 대한 구성 설명은 [표 1]와 같다.

[표 1]  OAuth 1.0 참여자 구분.

[그림 2]는 OAuth 1.0의 워크플로우를 보여준다. 이 프로세스를 간략히 설명하면 다음과 같다.


① 사용자는 애플리케이션(소비자)을 사용하려고 접근한다.
② 애플리케이션은 서비스 제공자에게 접근 권한을 요청하기 위해 사용자를 소셜 네트워킹 페이지로
이동시킨다.
③ 사용자는 본인 인증을 수행하고, 애플리케이션이 서비스 제공자로부터 정보를 제공 받는 것을
승인한다.
④ 서비스 제공자는 애플리케이션에게 접근 권한을 부여하고, 애플리케이션은 이를 통해 사용자 정보에
접근할 수 있게 된다.

OAuth 1.0은 사용자와 사용자가 이용하려는 애플리케이션 그리고 사용자 정보를 보유하고 있는 서비스 제공자가 상호 작용을 통해 소비자에게 위임 접근을 제공한다. 여기까지 보면, OAuth 1.0은 단순하고 간편한 서비스처럼 보일 수 있지만, 실제로는 여러 복잡한 메커니즘과 한계점을 가지고 있었다.

첫째, OAuth 1.0은 모든 요청에 대해 암호화된 서명을 생성하고 검증하는 방식을 사용했다. 이는 보안성을 강화하는 수단이기도 했지만, 각 요청마다 복잡한 서명 과정을 필요로 하여 서버 측에서 검증 로직을 구현하는데 어려움이 따랐다.
둘째, OAuth 1.0의 제한된 인증 방식으로 인해 모바일 애플리케이션과 같은 다양한 환경에서 사용하기 어려웠고, 이로 인해 다른 플랫폼에서의 적용이 쉽지 않아 확장성이 부족했다.
마지막으로, OAuth 1.0은 인증 토큰(Access Token) 유효기간에 대한 명확한 스펙이 없어 재사용 가능성이라는 보안 위험이 잔존했고, 토큰을 무효화하려면 서비스 제공자의 애플리케이션에서 비밀번호를 직접 변경해야 했다.

이와 같은 기존 문제점을 개선하고 더 다양한 인증 방식과 간편한 토큰 관리 그리고 세밀한 권한 제어를 제공하기 위해 OAuth 2.0 표준이 개발되었다.

2.2) OAuth 2.0

[그림 3] OAuth 2.0 워크플로우

OAuth 2.0 표준에서는 참여자를 역할에 따라 Client, Resource Owner, Resource Server 그리고 Authorization Server로 구분 지으며, 각 역할에 대한 구성 설명은 [표 2]와 같다.

[표 2] OAuth 2.0 참여자 구분

[그림 3]은 OAuth 2.0의 워크플로우를 보여준다. 이 프로세스를 간략히 설명하면 다음과 같다.


① 사용자인 Resource Owner는 소셜 로그인을 통해 클라이언트가 제공하는 서비스에 접근하고자
하며, 이를 위한 권한 위임을 요청한다.
② 클라이언트(Client)는 Authorization Server에게 Resource Server가 보호하는 자원에 접근하기
위한 Access Token 발급을 요청한다. 이때, 사용자는 인증(로그인)을 위해 Authorization Server의
페이지로 이동한다.
③ 사용자는 클라이언트에 대한 권한 위임을 승인한다.
④ Authorization Server는 클라이언트에게 Access Token을 발급한다. 이 Access Token에는 만료
기간이 설정되어 있다.
⑤ 클라이언트는 Resource Server에게 Access Token을 전달하여, 사용자의 정보에 접근을 요청한다.
⑥ Resource Server는 Access Token의 유효성을 확인하기 위해 이를 발급한 Authorization
Server에 검증을 요청한다.
⑦ Authorization Server가 Access Token의 유효성을 확인하면, Resource Server에게 인증 성공
메시지를 전송한다.
⑧ Resource Server는 클라이언트가 요청한 사용자 정보를 반환한다.

[표 3] OAuth 1.0과 2.0 차이점

[표 3]에서 확인할 수 있듯이, OAuth 2.0은 1.0 버전과 비교하여 여러 측면에서 크게 개선되었다. 가장 주목할 만한 변화는 서버 구조의 분리다. OAuth 1.0에서는 인증, 권한 부여, 자원 제공이 단일 서버에서 통합 처리되었으나, 2.0에서는 이를 인증 및 권한 부여 서버와 자원 서버로 분리하여 시스템의 유연성과 확장성을 높였다.

OAuth 2.0은 Access Token의 유효 기간에 대한 명확한 기술 명세를 제시하여 토큰 재사용과 관련된 보안 취약점을 개선했다. 또한, HTTPS 프로토콜을 통한 암호화를 도입함으로써 OAuth 1.0에서 요구되던 복잡한 서명 과정을 제거했다. 이로 인해 기능 구현이 간소화되었고, 다양한 인증 수단을 지원하게 되어 범용성이 향상되었다.

결과적으로 OAuth 2.0은 1.0에 비해 다양한 인증 방식, 간편한 구현, 강화된 보안을 제공한다. 이러한 개선 사항들을 바탕으로 OAuth 2.0은 현대 애플리케이션 환경에서 널리 사용되는 개방형 인증 표준 프로토콜로 자리매김하게 되었다.

02. OAuth 2.0 Token & Client Types

1) Token Types

1.1) Access Token

OAuth는 사용자의 비밀번호를 제3자 애플리케이션에 직접 제공하지 않고도 자원에 대한 접근 권한을 위임할 수 있는 인증 및 인가 방식이다. 이 과정에서 자원을 요청한 사용자가 적절한 권한을 가졌는지 검증하기 위해 자격증명 토큰이 사용된다. OAuth 2.0에서는 Access Token, Refresh Token, ID Token, Authorization Code 네 가지 주요 토큰을 사용하며, 이들은 각각 고유한 역할을 수행하여 OAuth 2.0의 인증 및 인가 프로세스를 안전하고 효율적으로 관리하는데 기여한다.

[그림 4] OAuth 2.0 Access Token 흐름

Access Token은 클라이언트에서 사용자의 보호된 리소스에 접근하는데 사용되는 자격 증명이다. 즉, 인증을 위한 사용자의 아이디와 비밀번호를 일련의 문자열로 표현한 형태라고 할 수 있다. 일반적으로 JWT(JSON Web Token) 형식을 취하며, 유형에 따라 식별자 타입(Identifier Type)과 독립형 타입(Self-contained Type)으로 분류 된다.

[그림 5] 식별자 타입(Identifier Type)

식별자 타입(Identifier Type) 자격증명 방식은 클라이언트(Client)가 사용자의 보호된 자원에 접근할 때, 인가 서버(Authorization Server)에 제출한 Access Token과 인가 서버가 보유한 Access Token의 일치 여부를 검증하는 방식이다.

사용자가 최초 권한 부여를 승인하면, 인가 서버는 사용자에 관한 정보를 데이터 저장소에 저장하고, 이 정보에 대한 고유 식별자를 포함한 Access Token을 발행한다. 이후 클라이언트가 해당 토큰을 사용해 자원 서버(Resource Server)에 요청을 보내면, 자원 서버는 클라이언트가 요청한 자원을 반환하기 전에 인가 서버와 통신하여 토큰의 유효성을 검증한다.

식별자 타입은 독립형 타입과 달리 발행된 토큰 자체에 민감한 정보가 포함되지 않기 때문에, 토큰이 외부에 유출되더라도 사용자 정보나 권한 정보가 직접적으로 노출되지 않는다. 또한, 식별자 타입 토큰은 짧은 문자열로 구성된 경량화된 구조를 가지고 있어 URL 매개변수로 쉽게 전달될 수 있으며, 이로 인해 토큰 발급과 검증 과정에서의 처리 부담이 줄어든다.

다만, 자원 서버가 토큰의 유효성을 검증하기 위해서는 반드시 인가 서버와 통신하는 절차가 필요하다. 이는 해당 자격증명 방식이 인가 서버에 대한 높은 의존도를 가지고 있음을 의미한다. 예를 들어, 인가 서버의 네트워크 지연으로 인해 응답 시간이 증가하면 유효성 검증에 실패할 수 있으며, 인가 서버의 가용성이나 성능 문제가 발생할 경우, 전체 시스템의 영향을 미치게 된다. 또한, 각 토큰 요청마다 인가 서버와 통신해야 하므로 트래픽이 증가하게 되고 이는 서버의 부하를 증가시키는 요인이 된다. 그럼에도 불구하고, 토큰에 사용자의 중요한 정보가 포함되지 않기 때문에, 정보 유출에 민감한 시스템에서는 많이 사용되는 방식이다.

[그림 6] 독립형 타입(Self-contained Type)

독립형 타입(Self-contained Type) 자격증명 방식은 토큰 자체에 필요한 모든 정보를 포함하고 있어, 리소스 서버가 Access Token의 유효성을 자체적으로 검증할 수 있는 방식이다.

사용자가 권한 부여를 승인하면, 인가 서버는 사용자의 정보, 권한 정보, 토큰의 만료 시간 등 인증과 인가에 필요한 필수 정보를 JWT(JSON Web Token) 형태로 인코딩하여 발행한다. 클라이언트가 해당 토큰을 사용해 자원 서버에 요청을 보내면, 자원 서버는 토큰을 디코딩하여 자체적으로 유효성을 검증한다. 이 검증 과정은 일반적으로 공개키 방식을 사용하며, 사용되는 알고리즘은 서버의 사양에 따라 달라질 수 있다.

독립형 타입의 가장 큰 장점은 독립적인 검증이다. 식별자 타입과 달리, 자원 서버가 토큰의 유효성을 검증할 때 인가 서버와의 추가적인 통신을 할 필요가 없다. 이는 검증 과정을 빠르게 만들고, 추가적인 리소스 소모를 줄여준다. 특히, 요청 빈도가 높은 시스템에서 이러한 이점이 두드러진다.

독립형 타입에서 자원 서버는 인가 서버에 대한 의존도가 낮다. 이로 인해 인가 서버에 가용성 문제가 발생하더라도 그 영향이 최소화된다. 또한, Access Token은 상태 비저장(stateless) 방식으로 운영되므로 인가 서버가 토큰의 상태를 관리할 필요가 없다. 따라서 장애 복구나 서버 간 세션 공유가 불필요하다.

반면에, 토큰 검증을 위해 인가 서버와의 추가적인 통신 절차가 필요 없다는 것은, 한 번 발급된 토큰의 내용을 변경할 수 없다는 의미이기도 하다. 따라서, 사용자의 권한이나 정보가 변경될 경우 새로운 토큰을 발급해야 한다. 이로 인해 권한 변경이나 정보 갱신에 어려움이 발생한다.

더구나, 식별자 타입과 달리 토큰 자체에 중요한 정보가 포함되어 있기 때문에, 토큰이 탈취될 경우 사용자의 민감한 정보도 함께 노출될 위험이 있다. 하지만, 이와 같은 단점에도 불구하고 독립형 타입은 확장성과 효율성 측면에서 많은 이점을 제공하기에 가장 널리 사용되는 방식이다.

1.2) ID Token

소셜 로그인은 OAuth를 기반으로 구현된 기능 중 하나이며, 이 기능을 구현하는 핵심 기반 기술이 바로 OpenID Connect이다.

OpenID Connect(이하 OIDC)는 OAuth 2.0을 확장하여 사용자 인증을 제공하는 프로토콜이다. OIDC의 핵심 구성 요소 중 하나인 ID Token은 사용자 인증 정보를 담고 있는 토큰으로, 클라이언트는 이 ID Token을 통해 사용자의 인증 상태를 확인하게 된다.

[그림 7] ID Token과 Access Token 구성도 비교

ID Token과 Access Token은 겉으로 보기에는 사용자 정보를 담고 있으며, 검증 역할을 수행한다는 점에서 유사하지만, 그 역할과 목적이 명확히 구분된다.

[표 4] Access Token과 ID Token 차이점

ID Token은 "누가" 인증 되었는지를 알려주는 반면, Access Token은 "무엇을" 할 수 있는지를 나타낸다. [그림 7]에서 볼 수 있듯이, ID Token은 OpenID Provider가 발행하며, OpenID Provider는 OAuth 2.0 서버의 일종으로, 최종 사용자를 인증하고 그 결과와 사용자 정보를 신뢰 당사자에게 제공하는 역할을 한다. 그리고 이 과정에서 사용자의 신분을 증명하는 것이 ID Token이다.

1.3) Refresh Token

[그림 8] Refresh Token 교환 흐름도

Refresh Token은 Access Token이 만료되었을 때 클라이언트가 새로운 Access Token을 얻기 위해 사용하는 자격 증명이다. OAuth 1.0에서는 토큰의 유효 기간에 대한 명확한 규정이 없어 Access Token을 무한히 사용할 수 있었다. 그러나 OAuth 2.0에서는 토큰 발행 시 유효 기간을 설정하여, 일정 시간이 지나면 Access Token을 사용할 수 없게 되었다. 이는 토큰이 탈취되었을 경우, 공격자가 이를 오랫동안 사용할 수 없도록 제한하는데 도움이 되었지만, 장기간 실행되는 애플리케이션에는 적합하지 않다는 단점이 있었다. 이러한 문제를 해결하기 위해 도입된 것이 Refresh Token이다.

Refresh Token은 인가 서버에서 Access Token과 함께 발행되며, 클라이언트가 이를 관리한다. 클라이언트가 자원 서버로 유효 기간이 만료된 Access Token을 보내면, 자원 서버는 클라이언트에게 유효 기간이 만료되었다는 메시지를 반환한다. 클라이언트는 이 메시지를 수신한 후 [그림 8]과 같은 과정을 통해 새로운 Access Token을 발급받게 된다.

2) Client Types

2.1) 공개 클라이언트(Public Client)

지금까지 OAuth 2.0의 참여자 역할, 권한 위임을 위한 Access Token의 발급 및 유형, 그리고 토큰 관리 과정에 대해 자세히 살펴보았다. 이러한 전체 흐름에서 주목할 점은 사용자(Resource Owner)의 역할이다. 사용자는 권한 부여를 승인하는 핵심 주체이지만, 자원 서버나 인가 서버와 직접적으로 상호작용하지 않는다.

[그림 3]의 워크플로우에서 명확히 볼 수 있듯이, 사용자를 대신하여 실제로 요청을 처리하고 서버들과 상호작용하는 주체는 클라이언트이다. 그리고 OAuth 2.0에서 클라이언트는 인증 및 권한 부여의 보안 요구 사항에 따라 공개 클라이언트(Public Client)와 기밀 클라이언트(Confidential Client)으로 나뉘게 된다.

[그림 9] 공개 클라이언트(Public Client) 구성도

클라이언트 유형을 공개 클라이언트와 기밀 클라이언트로 구분하는 주된 이유는 자격 증명의 기밀성 유지 여부에 있다. 공개 클라이언트는 인가 서버가 Access Token을 발행할 때, 별도의 비밀 통신 채널을 사용하지 않고 프론트엔드에서 URL을 통해 사용자에게 직접 전달한다. 이로 인해 Access Token이 사용자 화면에 그대로 노출되며, 외부 사용자에 의한 토큰 탈취 위험이 존재하게 된다. 즉, 공개 클라이언트는 클라이언트의 비밀 정보를 안전하게 보호할 수 없는 환경에서 운용되는 애플리케이션을 의미한다.

대표적으로 모바일 앱, 웹 기반 애플리케이션, 그리고 공용 데스크톱 애플리케이션 등 사용자 장치에서 실행되는 클라이언트에서 주로 사용된다. 보안 측면에서 몇 가지 위험이 존재하지만, 배포와 사용이 간편하여 개발과 유지보수가 비교적 쉬운 장점이 있다. 또한, 클라이언트 자격증명을 안전하게 저장할 필요가 없기 때문에, 다양한 플랫폼에서 환경에 구애 받지 않고 유연하게 사용할 수 있다. 그리고 OAuth 2.0이 지원하는 Grant Type 중 Implicit Grant Type의 인증 방식이 여기에 포함된다.

2.2) 기밀 클라이언트(Confidential Client)

[그림 10] 기밀 클라이언트(Confidential Client) 구성도

기밀 클라이언트는 클라이언트 비밀을 안전하게 저장하고 관리할 수 있는 환경에서 운용되는 애플리케이션으로, 주로 서버 측에서 실행되는 백엔드 서비스나 데스크톱 애플리케이션에서 사용된다.

공개 클라이언트와 기밀 클라이언트의 주요 차이점은 Access Token 획득 방식에 있다. 공개 클라이언트의 경우 인가 서버로부터 Access Token을 직접 전달받는 반면, 기밀 클라이언트는 먼저 권한 승인 코드(Authorization Code)를 받은 후 이를 이용해 Access Token으로 교환하는 과정을 거친다. 이 과정에서 승인 코드는 철저히 백엔드에서 처리되므로, 공개 클라이언트와 달리 사용자에게 노출될 위험이 없다. 또한, 권한 승인 코드가 탈취되더라도, 이를 검증하기 위한 보안 토큰이 없으면 Access Token이 발급되지 않기 때문에 보안성이 우수하다.

OAuth 2.0이 지원하는 Grant Type 중 Authorization Code Grant Type과 Password Credentials Grant Type의 인증 방식이 여기에 포함된다.

03.OAuth 2.0 Grant Types

OAuth 2.0은 다양한 클라이언트 환경과 사용 시나리오에 적합한 인증 및 인가 방식을 제공하기 위해 6가지의 인증 수단을 제공한다.

1) Authorization Code Grant Type

[그림 11] Authorization Code Grant Type 흐름도

Authorization Code Grant Type은 보안이 중요한 웹 애플리케이션과 서버 간 통신에 사용되는 대표적인 기밀 클라이언트 유형이다. 이 방식에서는 사용자 인증과 인가가 클라이언트에서 직접 이루어지지 않고 인가 서버를 통해 처리되며, 클라이언트는 권한 승인 코드(Authorization Code)를 받아 이를 Access Token으로 교환한다. 이로 인해 민감한 정보가 사용자 에이전트(브라우저)나 프론트엔드에 노출되지 않아 토큰 탈취의 위험성이 적다.

2) Implicit Grant Type

[그림 12] Implicit Grant Type 흐름도

Implicit Grant Type은 대표적인 공개 클라이언트 유형으로, 주로 SPA(Single Page Application)와 같은 클라이언트 측 애플리케이션에서 사용된다. 이 방식에서는 Access Token이 권한 승인 코드 없이 직접 발행되므로, 클라이언트와 인가 서버 간의 통신이 단순해지는 장점이 있다. 그러나 Access Token이 URL 매개변수를 통해 브라우저에 직접 전달되기 때문에, 토큰 탈취의 위험성이 존재한다. 따라서, 가용성이 우선 시 되는 환경에서 사용하기에 적합하다.

3) Resource Owner Password Credentials Grant Type

[그림 13] Resource Owner Password Credentials Grant Type 흐름도

Resource Owner Password Credentials Grant Type은 사용자 계정 정보(아이디와 비밀번호)를 직접 사용하여 Access Token을 얻는 방식이다. 이 방식은 사용자의 계정 정보를 신뢰할 수 있는 애플리케이션에서 직접 수집(저장)할 수 있는 경우에만 사용된다. 따라서, 제3자 애플리케이션보다는 자사 서비스에서 제공하는 애플리케이션 환경에 운용하기 적합하다.

4) Client Credentials Grant Type

[그림 14] Client Credentials Grant Type 흐름도

Client Credentials Grant Type은 애플리케이션이 리소스 소유자이자 클라이언트의 역할을 동시에 수행하는 방식이다. 주로 자사에서 관리하는 애플리케이션 환경에서 서버 간 통신이 필요한 IoT 환경에서 사용된다. 이 방식은 클라이언트의 정보를 기반으로 하기 때문에 OAuth 2.0 인증 및 권한 부여 방식 중 가장 간단한 구성을 가진다.

5) PKCE-enhanced Authorization Code Grant Type

[그림 15] PKCE-enhanced Authorization Code Grant Type 흐름도

PKCE-enhanced Authorization Code Grant Type(이하 PKCE)은 CSRF(Cross-Site Request Forgery) 및 코드 교환 공격을 방지하기 위해 Authorization Code Grant Type의 보안성을 향상시킨 확장 버전이다. PKCE는 본래 취약한 모바일 애플리케이션 환경에서 Authorization Code Grant Type의 보안을 강화하기 위해 설계되었으나, 이후에는 모든 유형의 OAuth 2.0 클라이언트에서도 적용할 수 있도록 개선되었다.

이 방식은 특징은 클라이언트가 권한 승인 코드를 Access Token으로 교환할 때, 초기 요청 시 전송한 해시 값과 Client Secret(code_verifier) 값이 일치하는 경우에만 Access Token을 발행한다는 것이다. 이를 통해 교환 요청이 올바른 클라이언트로부터 온 것인지를 보장하며, 권한 승인 코드가 탈취되더라도, 공격자가 Client Secret 값을 알지 못하기 때문에, Access Token을 획득할 수 없다.

04. OAuth 2.0 취약점

1) JWT(JSON Web Token) 정보 추출

[그림 16] JWT(JSON Web Token) 구성

JWT(JSON Web Token, 이하 JWT)는 당사자 간의 정보를 안전하게 전송하기 위한 개방형 표준(RFC 7519)이다. JWT는 self-contained 형태의 토큰으로, [그림 16]과 같이 서버가 작업 처리에 필요한 정보를 Base64Url로 인코딩하여 포함한다. 주로 서버 간 인증 작업에 사용되며, Access Token 또한, JWT를 채택하고 있다.

[그림 17] Access Token을 통한 정보 탈취 시나리오

JWT는 기본적으로 [그림 17]과 같이 3가지 영역으로 구성되어 Base64Url로 인코딩되어 전달된다.

● Header : 토큰의 유형과 서명 알고리즘을 지정한다.

● Payload : 서버 작업에 필요한 주요 정보가 포함된다.

● Signature : 토큰의 무결성을 보장하기 위해 Header와 Payload를 서명한 값이 포함되며, 이 서명은 토큰이 변조되지 않았음을 보장한다.

여기서 주목해야 할 점은 Base64Url은 대칭키나 비대칭키를 이용하여 데이터를 암호화하는 방식이 아니라, ASCII 문자(A-Z, a-z, 0-9, +, /)를 사용하여 이진 데이터를 문자열로 표현한 인코딩 방식이라는 것이다. 이러한 특성으로 인해, JWT가 노출될 경우, 간단한 디코딩 과정만으로도 중요한 정보가 쉽게 유출될 수 있는 위험이 존재한다.

다만, JWT는 데이터가 암호화되어 있지 않을 뿐 “Signature” 영역의 서명 값을 통해 정보의 무결성은 보장된다.

[그림 18] Access Token 탈취 후 디코드를 통한 정보 탈취 시나리오

Access Token은 앞서 살펴본 것처럼, 서버로부터 인증된 사용자를 증명하는 자격 증명이다. 이러한 Access Token은 인가 서버와 리소스 서버의 설정에 따라 URL 매개변수, 헤더, 또는 쿠키로 발급될 수 있다. 그러나 이렇게 발급된 토큰에 대한 관리가 미흡할 경우, 탈취되거나 공격자에 의해 악용될 위험이 존재한다.

이번 섹션에서는 다양한 토큰 전달 방식 중 쿠키를 이용한 방법을 이용할 때, 보안 설정이 미흡할 경우 발생할 수 있는 XSS(Cross-Site Scripting, 이하 XSS)와 연계된 정보 탈취 위험에 대해 중점적으로 다뤄보겠다.

[그림 19] 로그인 페이지

대부분의 사이트는 OAuth를 기반으로 한 소셜 로그인 기능을 제공하며, [그림 19]에서 볼 수 있듯이 Facebook, GitHub 등의 서드파티 서비스가 대표적이다.

여기서는 Keycloak을 이용한 로그인을 시도한다.

[그림 20] Authorization 서버 로그인

서드파티 로그인 방식에서는 클라이언트가 직접 인증을 처리하지 않고, [그림 6-20] 처럼 소셜 네트워킹 서비스의 Authorization 서버로 이동하여 사용자 인증을 진행한다.

[그림 21] 리소스 서버로 받아 온 사용자 정보를 가공하여 사용자에게 보여주고 있다.

인증이 성공하면 인가 서버로부터 Access Token이 발급된다. 클라이언트는 이 토큰을 리소스 서버에 전달하고, 리소스 서버는 해당 토큰을 검증한 후 사용자 정보를 클라이언트에게 반환한다. 그리고 [그림 21]과 같이 클라이언트는 이 정보를 가공 처리하게 된다.

[그림 22] Access Token 전달

인가 서버 인증 후 클라이언트에게 전달되는 토큰의 전달 방식을 Proxy 도구(Burp Suite)를 통해 확인해 보면, Access Token이 HTTP 헤더의 쿠키에 설정되어 전달되는 것을 확인할 수 있다.

[그림 23] 사용자 쿠키에 저장되어 있는 Access Token 정보

Access Token을 쿠키를 통해 전달하는 경우, 클라이언트는 사용 후 쿠키에서 이를 제거해야 할 필요가 있지만, 보편적으로는 그대로 유지하는 경우가 많다. 해당 사이트에서도 [그림 23]과 같이 쿠키에서 Access Token이 제거되지 않고 그대로 남아 있는 것을 확인할 수 있다.

[그림 24] 보안 설정이 적용되어 있지 않은 쿠키(Authorization_Token)

더불어, 쿠키를 통해 중요한 정보를 전달할 때는 자바스크립트와 같은 클라이언트 측 스크립트에서 접근할 수 없도록 설정하여, 잠재적인 XSS 공격으로부터 쿠키를 보호해야 한다.

HttpOnly 플래그는 이를 대응하는 서버 측 보안 설정으로, 이 플래그가 설정된 쿠키는 HTTP 요청 및 응답과 같은 서버 측 통신에서만 사용되며, 클라이언트 측 스크립트에서는 접근할 수 없다.

[그림 24]를 보면, Access Token 값을 저장하고 있는 Authorization_Token에 HttpOnly 설정이 적용되지 않은 것을 확인할 수 있다. 따라서, 해당 사이트에 XSS 취약점이 존재할 경우, Access Token 탈취가 가능해진다.

[그림 25] 페이지 소스 보기

[그림 25]와 같이 마이페이지에는 사용자로부터 입력 받는 value 매개변수가 있으며, 입력된 값은 해당 페이지의 태그 안에 삽입되는 것을 볼 수 있다. 이는 사용자가 입력한 값에 따라 클라이언트 측 로직을 제어할 수 있다는 의미이다.

[그림 26] XSS 공격을 통한 토큰 탈취

XSS 공격 구문을 구성하여 value 매개변수에 삽입한다. 이 구문은 사용자의 쿠키 정보를 공격자의 서버로 전송하도록 설계되어 있다. [그림 26]에서 볼 수 있듯이, 이 공격이 성공하면 사용자의 Authorization_Token을 포함한 쿠키 정보가 공격자의 서버로 전송된다.

결과적으로, 공격자는 XSS 취약점이 삽입된 링크를 만들어 사용자들이 클릭하도록 유도함으로써 토큰을 탈취할 수 있게 된다.

[그림 27] 탈취한 Access Token 디코드

탈취한 Access Token을 디코드하면 [그림 27]에서 볼 수 있듯이 사용자의 상세 정보가 노출된다. 이러한 과정을 통해 공격자는 사용자의 민감한 개인정보를 손쉽게 획득할 수 있으며, 이는 단순한 토큰 탈취를 넘어 개인정보 유출로 이어질 수 있다.

[그림 28] HttpOnly 보안 설정 적용 전/후

공격자로부터의 쿠키 탈취 공격을 대응하기 위해서는 서버 측에서 적절한 보안 설정을 적용해야 한다. 그 중 가장 효과적인 방법 중 하나는 쿠키에 HttpOnly 플래그를 설정하는 것이다. [그림 28]은 이 HttpOnly 보안 설정의 적용 전과 후를 비교하여 보여주고 있으며, Cookie API를 통해 설정을 간단하게 적용할 수 있다.

[그림 29] HttpOnly 보안 설정 적용 후 쿠키 확인

[그림 29]는 서버 측에서 HttpOnly 보안 설정을 적용한 후의 결과를 보여준다. 쿠키 정보에 HttpOnly 플래그가 설정된 것을 확인할 수 있으며, 콘솔에서 쿠키 정보를 출력하려고 시도하면, 이전과 달리 아무 정보도 표시되지 않고 빈 값만 반환된다. 이는 HttpOnly 설정이 클라이언트 측 스크립트의 쿠키 접근을 차단하고 있음을 의미한다.

2) Access Token 탈취(with Implicit Grant Type)

[그림 30] Implicit Grant Type 토큰 발급 흐름도

‘3. OAuth 2.0 Grant Types’단락에서 살펴본 Implicit Grant Type 방식은 공개 클라이언트 유형으로 권한 승인 코드 없이 Access Token이 발행되며, 토큰이 URL 매개변수를 통해 클라이언트로 전달되는 특징을 가지고 있다.

이러한 특성으로 인해 Implicit Grant Type 방식은 다른 방식(Authorization Code Grant Type, Password Credentials Grant Type 등)과 달리, 프론트엔드에서 모든 요청 흐름이 이루어지며, 그 과정이 사용자 브라우저에 노출된다. 따라서, Implicit Grant Type 방식은 다른 방식보다 보안에 더욱 신경 써야 하며, Access Token의 재사용과 유효기간을 엄격히 관리해야 한다.

이번 섹션에서는 클라이언트와 인가 서버에서 Implicit Grant Type 방식을 사용해 Access Token을 발급할 때, 서버 측의 토큰 관리 미흡으로 인해 발생할 수 있는 토큰 재사용 공격에 대해 다뤄보겠다.

[그림 31] Authorization 서버 로그인 수행

먼저, Implicit Grant Type 방식을 사용할 때 실제 토큰이 발급되고 전달되는 과정을 살펴보겠다. [그림 31]과 같이, 사용자가 서드파티를 통해 로그인을 수행하여 인증에 성공하면, 인가 서버로부터 Access Token을 발급 받게 된다.

[그림 32] URL 매개변수를 통해 전달된 Access Token

인가 서버가 발급한 토큰은 URL 매개변수를 통해 클라이언트에게 전달된다. [그림 32]에서 볼 수 있듯이, 발급된 Access Token이 access_token이라는 매개변수에 저장되어 있는 것을 확인할 수 있다.

[그림 33] URL 매개변수로 전달된 Access Token을 /api/user 자원으로 전달하는 자바스크립트 함수

Access Token을 실제로 사용하기 전에, 먼저 토큰의 유효성을 검증하는 과정이 필요하다. 이를 위해 토큰은 검증 기능을 수행하는 서버 측 자원으로 전달된다.

[그림 34] Access Token 검증 및 가공 처리 과정

이후, 토큰 검증이 완료되면 서버 측에서 추가 가공 처리를 거쳐 사용자가 로그인하게 된다. 지금까지 Implicit Grant Type 방식에서 토큰이 어떻게 발급되고 전달되는지 그 과정을 살펴보았다. 이러한 일련의 흐름에서 관심 있게 봐야 할 부분은 Access Token의 이동이다.

[그림 32]와 [그림 34]에서 볼 수 있듯이, 토큰을 전달받는 과정과 토큰을 검증하기 위해 전달하는 과정에서 Access Token이 사용자(브라우저)에게 노출된다. 이렇게 노출된 토큰은 CSRF(Cross-Site Request Forgery, 이하 CSRF)나 XSS 공격, 또는 예기치 않은 상황으로 인한 정보 노출에 의해 공격자에게 유출될 수 있다.

따라서, Implicit Grant Type을 사용할 때는 토큰의 유효 기간을 짧게 설정하고, 사용자가 로그아웃 할 때 해당 세션을 즉시 폐기하여 재사용을 방지해야 한다.

[그림 35] Access Token 탈취 후 재사용을 통한 계정 탈취 시나리오

이제 Access Token 재사용과 관련된 보안 취약점을 살펴보겠다. 일반적으로 사용자가 로그아웃 할 때 서버는 사용자 세션 정보(JSESSIONID)를 폐기하며, OAuth를 통한 로그인 시에는 사용자 세션 정보와 함께 Access Token 세션 정보도 폐기된다.

그러나 일부 사이트에서는 Access Token 세션 정보를 제대로 폐기하지 않아 남아 있는 경우가 있다. 이로 인해 공격자가 Access Token을 탈취하면 해당 사용자의 계정으로 로그인할 수 있는 위험이 발생하게 된다.

[그림 36] 로그아웃 후 세션 폐기 여부 확인

사용자가 로그아웃을 수행하면 로그인 화면으로 이동하게 된다. 이후, 로그인을 하지 않은 상태에서 마이페이지에 접근할 경우, [그림 36]과 같이 HTTP 403 Error가 반환되며 접근이 차단된다. 이는 사용자가 로그아웃 할 때 서버 측에서 기존 세션을 폐기한다는 것을 알 수 있다.

[그림 37] 탈취한 Access Token으로 인증 시도

공격자는 XSS, CSRF, 또는 기타 정보 노출 방법을 통해 사전에 탈취한 Access Token을 이용해 인증을 시도한다. 이후, [그림 37]에서 볼 수 있듯이, 공격자는 토큰의 원소유자의 계정으로 로그인에 성공하게 된다.

이 과정에서 알 수 있는 것은 사용자가 로그아웃을 하고 서버 측에서 기존 세션(JSESSIONID)을 폐기하더라도, Access Token에 연결된 세션이 제대로 무효화 되지 않으면 해당 토큰은 여전히 유효하게 남아 재사용될 수 있다는 것이다.

그러므로, 이러한 보안 위험에 대응하기 위해서는 로그아웃 프로세스를 더욱 철저히 관리해야 한다. 사용자가 로그아웃할 때, 서버는 기존 세션을 폐기하는 것에 그치지 않고, 인가 서버와 연동하여 Access Token 관련 세션도 함께 폐기해야 한다. 그런 점에서, Spring Security 프레임워크는 이와 같이 복잡한 과정을 간소화하고 자동화할 수 있는 API를 제공하며, 이를 활용 시 보다 안전하고 효율적인 로그아웃 매커니즘을 쉽게 구현할 수 있다.

[그림 38] Spring Security 로그아웃 API 설정 예시 소스코드

[그림 38]은 Spring Security에서 제공하는 로그아웃 API의 설정 예시를 보여주는 소스코드이다. 지정된 URL로 로그아웃 요청이 수신되면 Spring Security가 자동으로 로그아웃 프로세스를 수행하며, 이 과정에서 OidcClientInitiatedLogoutSuccessHandler가 인가 서버와 연동하여 Access Token을 무효화하는 작업을 수행한다.

[그림 39] Spring Security 로그아웃 API 설정 적용 전/후

[그림 39]는 Spring Security의 로그아웃 API 설정 적용 전후의 차이를 보여준다. 설정 적용 전에는 사용자가 로그아웃 하더라도 인가 서버에 Access Token 관련 세션 정보가 그대로 남아 있었으나, API 설정을 적용한 후에는 로그아웃 시 해당 세션 정보가 무효화되는 것을 확인할 수 있다.

결과적으로, 개발자는 세션 폐기와 같은 세부적인 작업을 일일이 구현할 필요가 없으며, 예기치 않게 토큰이 유출되더라도 사용자는 로그아웃을 통해 토큰 재사용을 방지할 수 있다.

3) Open Redirect 정보 탈취(Conversion Redirect)

[그림 40] 권한 부여 요청 시 redirect_uri 검증 과정

클라이언트는 인가 서버에 토큰을 요청할 때, 사용자 인증에 필요한 다양한 정보를 매개변수로 포함해 전송한다. 이 과정에서 사용되는 redirect_uri 매개변수는 인증 완료 후 인가 서버가 인증 결과를 전달할 주소를 지정하는 역할을 수행한다. 즉, 인증 절차가 끝난 후 사용자를 리다이렉트 할 페이지를 명시함으로써, 인가 서버에 인증 결과를 전달할 클라이언트 경로를 알려주는 핵심 요소다.

[그림 41] redirect_uri 검증 실패 시 화면

그래서 대부분의 인가 서버는 요청을 수신할 때 redirect_uri를 우선적으로 검증하며, 이 값이 사전에 등록된 유효한 주소가 아닐 경우 [그림 41]과 같이 요청을 거부한다.

만약 인가 서버가 이러한 검증을 제대로 수행하지 않거나 미흡할 경우, 공격자는 redirect_uri를 조작하여 사용자를 악의적인 피싱 사이트로 유도할 수 있으며, 인가 서버가 발급한 Access Token 탈취도 가능해진다.

[그림 42] 인가 서버의 redirect_uri 설정

인가 서버에서 리다이렉트 URI를 설정할 때, 일반적으로 두 가지 방식을 사용한다. 첫 번째 방식은 클라이언트가 인증 결과를 수신할 URI를 명시적으로 지정하는 방법으로, 예를 들어 “http://localhost:8081/api/token”과 같이 정확한 URI를 설정하는 것이다. 두 번째 방식은 [그림 42]에서 볼 수 있듯이 특정 경로에 와일드카드(Wildcard)를 사용하여 설정하는 방법으로, 예를 들어 http://localhost:8081/*와 같이 루트 이하의 모든 경로를 포괄할 수 있도록 지정한다.

각 방식에는 장단점이 있지만, 보안 측면에서 두 번째 방식은 다른 취약점과 결합되어 악용될 가능성이 크다. 따라서, 정확한 URI를 명시적으로 지정하는 첫 번째 방식 사용을 권장하고 있다.

[그림 43] 신뢰 기반 리다이렉트 URI를 이용한 Access Token 탈취 시나리오

이번 섹션에서는 인가 서버에서 리다이렉트 URI를 와일드카드로 설정할 때 발생할 수 있는 보안 위험에 대해 살펴보겠다. 이 공격은 인가 서버와 호스트 간의 신뢰 관계를 악용한 사례이다.

먼저, 공격자는 인가 서버가 신뢰하는 호스트에 악성 HTML 파일을 업로드한다. 이후, 해당 파일의 접근 URL을 redirect_uri로 설정한 인증 링크를 사용자에게 전달한다. 그리고 사용자가 이 링크를 통해 인증을 수행하게 되면, 인증 정보가 악성 파일로 리다이렉트 되면서 공격자는 Access Token을 탈취하게 된다.

[그림 44] redirect_uri 조작 시도

공격자는 서드파티 서비스를 통해 로그인 시도 시, redirect_uri 값을 임의로 설정하여 인가 서버의 리다이렉트 URI 설정 방식을 탐색한다. 이때, [그림 44]와 같이 접근이 허용된다면, 이는 인가 서버가 리다이렉트 URI를 와일드카드 방식으로 관리하고 있음을 나타낸다.

[그림 45] 임의 파일 업로드 시도

다음으로, 공격자는 인가 서버와 클라이언트 간의 신뢰 관계를 악용하기 위해, 대상 시스템 내에서 파일 업로드 기능이 구현된 페이지를 찾아 임의의 파일을 업로드한다.

[그림 46] 업로드가 허용되는 파일 확장자 확인

[그림 46]에서 볼 수 있듯이, 대상 시스템은 블랙리스트 방식을 통해 서버 사이드 스크립트 파일(JSP, PHP, ASP 등)의 업로드를 차단하고 있지만, HTML 파일은 업로드가 허용된 상태이다. 또한, 업로드 된 파일의 위치가 노출되고 있어 파일 접근 경로도 확인할 수 있다.

[그림 47] 악성 HTML 파일 업로드

공격자는 HTML 파일을 업로드 한다. 이 파일에는 자바스크립트 코드가 포함되어 있으며, URL을 통해 인가 서버로부터 전달되는 Access Token을 파싱하고 이를 공격자의 서버로 전송하는 기능을 수행한다.

[그림 48] 조작된 서드파티 로그인 링크 전달

공격자는 redirect_uri 매개변수에 자신이 업로드한 악성 HTML 파일의 경로를 설정한 후, 조작된 서드파티 로그인 링크를 사용자에게 전달하여 로그인을 유도한다.

[그림 49] 공격자 서버로 전송된 사용자의 Access Token

로그인 절차가 완료되면, 인가 서버는 설정된 redirect_uri로 사용자를 리다이렉트하며, 이 과정에서 사용자의 인증 정보가 함께 전달된다. 이후, 악성 HTML 파일에 포함된 자바스크립트 코드가 실행되어 사용자의 Access Token이 공격자의 서버로 전송된다. 결과적으로, 이를 통해 공격자는 사용자의 Access Token을 탈취하게 된다.

이러한 리다이렉트 URI 악용을 방지하기 위해서는 인가 서버의 설정이 매우 중요하다. 인가 서버는 클라이언트 애플리케이션별로 명확하게 허용된 리다이렉트 URI 만을 등록하고, 인가 요청을 철저히 검증해야 한다. 또한, 와일드카드 패턴의 사용은 이와 같은 보안 문제를 야기할 수 있으므로, 사용을 최소화하여 보안성을 강화해야 한다.

4) 부적절한 키 관리로 인한 JWT 변조

[그림 50] JWT 토큰 구조

이전 “[1) JWT(JSON Web Token) 정보 추출]” 섹션에서 설명한 바와 같이, JWT는 Header, Payload, Signature의 세 부분으로 구성되며, 데이터 자체는 기본적으로 암호화되지 않지만, Signature 영역에 포함된 서명을 통해 토큰의 무결성을 보장한다.

일반적으로 서드파티 인가 서버에서 서명에 사용하는 알고리즘으로는 HMAC(Hash-based Message Authentication Code, 이하 HMAC), RSA(Rivest-Shamir-Adelman, 이하 RSA), ECDSA(Elliptic Curve Digital Signature Algorithm, 이하 ECDSA), EdDSA(Edwards-Curve Digital Signature Algorithm, 이하 EdDSA) 등이 있다. 이 중 HMAC과 RSA가 가장 널리 사용되며, 특히 RSA는 OAuth 2.0 및 OpenID Connect의 표준 서명 알고리즘으로 자리잡고 있다.

HMAC은 대칭 키 알고리즘으로, 클라이언트와 인가 서버가 동일한 키를 사용하여 서명과 검증을 수행한다. 반면, RSA는 비대칭 키 알고리즘으로 인가 서버의 공개 키와 개인 키 쌍을 사용한다. RSA의 경우, 인가 서버는 토큰 발행 시 개인 키로 서명하고, 클라이언트는 공개 키로 해당 서명을 검증하게 된다.

JWT의 서명 알고리즘 선택은 클라이언트와 여러 API 서버 간의 관계, 성능 요구 사항, 키 관리의 복잡성 등을 고려하여 결정된다. 분산 시스템의 경우 RSA와 같은 비대칭 암호화 알고리즘이 주로 사용되며, Spring Security에서 제공하는 단일 서버인 리소스 서버와 같이 비교적 단순한 아키텍처를 가진 환경에서는 HMAC과 같은 대칭키 암호화 알고리즘을 사용한다.

[그림 51] Spring Security 리소스 서버 서명 검증 흐름도

Spring Security 프레임워크는 OAuth 2.0 및 OpenID Connect를 활용하여 인증된 클라이언트에 대해 보호된 자원을 제공하는 기능을 갖춘 자체 리소스 서버(Spring Security Resource Server)를 지원한다.

이러한 리소스 서버를 사용하는 주요 이유는 Keycloak, Google, Facebook과 같은 외부 서드파티 서비스에 의존하지 않고, 특정 비즈니스 로직이나 보안 정책을 직접 구현하고 제어하기 위해서이다. 이를 통해 조직은 인증 및 권한 부여 과정을 세밀하게 조정하고, 외부 서비스의 제약 없이 자원 보호 및 보안 정책을 맞춤형으로 적용할 수 있는 통합 환경을 운영할 수 있게 된다.

그러나, 자체 환경을 구축하다 보면 설계상의 취약점이 발생할 수 있으며, 아울러 토큰 서명에 사용되는 키 관리가 미흡할 경우 의도치 않게 외부에 키가 노출되는 문제가 발생하게 된다. 이렇게 노출된 키가 공격자에 의해 악용될 경우 데이터 변조 등의 보안 문제를 초래할 수 있다.

특히, 단일 리소스 서버 환경에서는 클라이언트가 리소스 서버에서 발급한 토큰의 서명 값만 검증하고 이를 신뢰하는 경우가 종종 있다. 이로 인해 토큰의 변조가 가능하다면, 공격자는 다른 사용자로 위장할 수 있는 위험이 존재하게 된다.

[그림 52] Spring Security 리소스 서버 사용자 인증을 위한 로그인 페이지

공격 사례를 다루기 전에, 실습 서버를 통해 Spring Security 리소스 서버와 클라이언트 간의 인증이 어떻게 이루어지는지 먼저 살펴보겠다. Spring Security 리소스 서버는 다른 서드파티 로그인 서비스와 동일하게 사용자가 로그인 과정을 통해 인증을 수행하는 방식으로 설계되어 있다.

[그림 53] 인증 성공/실패 차이

인증에 성공하면 클라이언트에 로그인 처리가 된 후 "/user" 페이지로 리다이렉트되어 사용자의 정보를 표시하게 되며, 인증에 실패할 경우에는 [그림 53]와 같이 인증 실패와 관련된 오류 메시지를 반환한다.

[그림 54] 리소스 서버 인증 후 클라이언트 로그인 과정

인증 성공 이후, 클라이언트의 로그인 처리 과정을 좀 더 구체적으로 살펴보면, [그림 54] 처럼 신규 사용자와 기존 사용자에 따라 처리 절차가 달라진다. 신규 사용자는 인가 서버에서 제공된 정보를 바탕으로 자동으로 회원가입이 이루어진 후 로그인 처리가 되고, 기존 사용자는 OAuth 인증을 통해 즉시 로그인 된다. 그리고 이 과정에서 신규 사용자와 기존 사용자를 구분하는데 사용되는 것이 토큰에 포함된 식별 정보(예: 이메일, 아이디, 고객 번호 등)이다.

[그림 55] 인증 성공 시 쿠키로 전달되는 Access Token

로그인 과정에서 전송되는 패킷을 살펴보면, 인증에 성공한 경우 [그림 55]와 같이 사용자 쿠키에 Access Token이 전달되는 것을 확인할 수 있다. 클라이언트는 이 토큰의 서명을 검증하여 요청이 올바른 사용자의 것인지 확인한 후, [그림 54]에서 설명된 처리 절차를 수행한다.

[그림 56] Spring Security 리소스 서버 서명 검증 흐름도

리소스 서버에서 발급한 Access Token은 별도의 암호화 모듈을 사용하지 않는다면 디코딩을 통해 내부의 사용자 인증 정보를 확인할 수 있고, 데이터 변경도 가능하다. 그러나, 이 경우 [그림 56]에서 보듯이 Signature 영역의 서명 값이 원본의 값과 달라지게 된다.

[그림 57] 변조된 토큰 전송

변경된 토큰이 서버로 전송되면, [그림 57]에서 보듯이 서명 검증에 실패하게 된다. 이는 클라이언트가 토큰의 서명을 검증할 때, 초기 서명 값과 변경된 서명 값을 비교하여 일치하지 않아 토큰이 변조된 것으로 판단하고 요청을 거부하기 때문이다.

[그림 58] 탈취된 암호 키를 이용한 토큰 변조 시나리오

이번 섹션에서는 Spring Security 리소스 서버와 같은 단일 리소스 서버 환경에서 대칭 키 암호화 알고리즘을 채택할 때, 부적절한 키 관리로 인해 암호 키가 외부에 노출되는 경우 공격자가 노출된 키를 이용해 토큰을 변조함으로써, 다른 사용자로 위장하여 인증을 우회하는 보안 취약 사례를 살펴보겠다.

[그림 59] 주석문 안에 포함된 JWT 서명 키

공격자는 대상 시스템의 취약점을 탐색하는 과정에서, 리소스 서버의 로그인 페이지 소스코드에 포함된 주석에서 개발 당시 사용된 것으로 추정되는 서명 키 관련 정보를 발견한다.

[그림 60] JWT 서명 시 사용된 암호화 알고리즘 확인

공격자는 로그인을 시도하여 인증이 성공한 후, 리소스 서버가 발급한 Access Token을 디코딩하고, Header 값을 확인함으로써 JWT 서명에 사용된 암호화 알고리즘을 파악한다.

[그림 61] JWT 생성 코드 작성

지금까지의 정보를 바탕으로 공격자는 [그림 61]에 나와 있는 JWT 생성 코드를 작성한다. 이때, 서명 키는 로그인 페이지에서 노출된 키 값을 사용하며, sub 클레임을 "admin"으로 설정한다. 여기서 사용되는 sub 클레임은 JWT 주체를 나타내는 정보로 클라이언트가 사용자나 엔티티를 구분하기 위한 고유 식별 값이다.

[그림 62] Access Token 전달 및 로그인 과정

생성한 Access Token을 클라이언트에게 제출하면, 토큰 유효성 검증이 시작되고, 검증이 완료되면 [그림 61]에서 설정한 sub 클레임 정보를 기반으로 데이터베이스에서 사용자 정보를 조회한다. 만약 일치하는 정보가 있다면 공격자는 [그림 62]와 같이 해당 사용자로 로그인에 성공하게 된다.

[그림 63] 클라이언트에 의해 재설정된 토큰 정보

앞서 토큰을 생성할 때 sub 클레임 정보만을 설정했으나, 로그인 성공 후 클라이언트가 재설정한 토큰 정보를 확인해보면 원래 설정하지 않았던 추가 정보가 포함되어 있는 것을 알 수 있다.
이는 클라이언트가 sub 클레임으로 지정된 사용자 정보를 데이터베이스에서 조회하면서, 해당 사용자에 대한 추가 정보를 토큰에 포함시킨 결과이다.

[그림 64] JWT 토큰 서명 시 비대칭 암호화 사용 설정

지금까지 OAuth 환경에서 대칭키 기반 암호화 알고리즘을 사용할 때 키 노출로 인한 보안 위험에 대해 알아보았다. 이러한 경우, 앞서 살펴본 사례와 같이 공격자는 노출된 키를 이용해 토큰을 변조하거나 인증을 우회할 수 있으며, 이로 인해 시스템의 신뢰성이 위협받게 된다.

따라서, 효과적인 대응을 위해서는 키 관리를 철저히 해야 하며, 암호화 키가 노출되었을 경우 즉시 키를 교체하여 악의적인 사용자가 이를 악용하는 것을 방지해야 한다. 추가적으로, Spring Security를 활용하면 [그림 64]와 같이 간단한 설정으로 비대칭키 기반 암호화 알고리즘을 구현할 수 있다. 비대칭 암호화는 개인 키가 공개되지 않으므로, 대칭 암호화에 비해 키 관리와 보안 측면에서 더 뛰어난 장점을 제공한다. 그런 점에서, 특별한 이유가 없다면 JWT 서명 시에는 대칭키 대신 비대칭 암호화 알고리즘을 사용하는 것이 권장된다.

05. 마무리

OAuth 2.0은 현대 웹 애플리케이션과 서비스에서 널리 사용되는 권한 부여 프레임워크로, 사용자 민감 정보의 접근을 안전하게 관리할 수 있는 강력한 도구이다. 그러나 이러한 장점에도 불구하고, OAuth 2.0의 구현 과정에서 발생할 수 있는 보안 취약점을 간과해서는 안된다. 특히, 잘못된 로직 구현이나 미흡한 보안 설정은 예기치 못한 공격 벡터를 만들어 낼 수 있으며, 이는 시스템의 보안을 위협하는 결과로 이어지게 된다.

따라서, OAuth 2.0의 강력한 기능을 온전히 활용하고 보안 위협을 최소화하기 위해서는 각 구성 요소와 프로세스에 대한 깊이 있는 이해가 필요하며, 이를 바탕으로 정확한 권한 부여 프로세스를 구현하고, 지속적인 키 관리 및 정기적인 보안을 수행해야 한다. 결론적으로, OAuth 2.0의 성공적인 도입과 안전한 운영은 기술적 접근뿐만 아니라, 보안 관점에서의 지속적인 관심과 노력을 통해 이루어질 수 있다.

06. 참고자료

[1] OAuith 2.0 아키텍처
https://oauth.net/2/
[2] OAuth 2.0 역할
https://guide.ncloud-docs.com/docs/b2bpls-oauth2
[3] 소셜 로그인
https://www.okta.com/kr/blog/2020/08/social-login/
[4] Keycloak
https://www.keycloak.org/
[5] CVE-2023-6927
https://securityblog.omegapoint.se/en/writeup-keycloak-cve-2023-6927/
[6] OAuth 2.0 Token
https://is.docs.wso2.com/en/5.9.0/learn/self-contained-access-tokens/
[7] OAuth 1.0
https://openid.net/specs/openid-connect-core-1_0.html