OAuth2.0 PKCE(Proof Key for Code Exchange)를 통한 보안 강화

7 분 소요


RECOMMEND POSTS BEFORE THIS

0. 들어가면서

최근 동료들끼리 OAuth2.0 클라이언트를 프론트엔드 애플리케이션에 두는 것이 좋은지, 백엔드 애플리케이션에 두는 것이 좋은지에 대한 이야기를 나눴다. 나는 보통 OAuth2 클라이언트를 백엔드에 두는 방식을 많이 사용해왔기 때문에 프론트엔드 애플리케이션에 두는 것은 고민해본적이 없었다. 보안적으로 위험할 것이라 생각했지만, PKCE(Proof Key for Code Exchange) 방식으로 보완한다면 프론트엔드 애플리케이션에서도 액세스 토큰(access token) 발급 과정이 안전해진다는 사실을 알게 되었다. 공부한 내용들을 글로 정리해봤다.

1. OAuth2.0 client type and obataining authorization

보안적 리스크가 발생할 수 있는 시나리오를 살펴보기 전에 우선 OAuth2 클라이언트 종류에 대해 알아보자. 클라이언트 종류에 따라 클라이언트 인증 방법이 다르기 때문이다.

  • 비공개 클라이언트(credential client)
    • 비공개 클라이언트는 클라이언트 시크릿과 같은 자격 증명을 기밀로 안전하게 유지할 수 있는 클라이언트다. 이러한 클라이언트는 일반적으로 자체적으로 통제되고 보호되는 서버 환경에서 실행된다.
    • 외부에서 접근할 수 없는 환경에서 실행되며 자신의 클라이언트 시크릿을 안전하게 보관할 수 있는 클라이언트(e.g. 서버)를 의미한다.
    • 클라이언트 비밀번호(client password) 방식을 이용해 클라이언트 애플리케이션임을 인증한다. 예를 들어 인증 서버로부터 미리 발급받은 클라이언트 아이디와 클라이언트 시크릿을 통해 클라이언트 애플리케이션임을 인증하고 액세스 토큰을 발급받는다.
  • 공개 클라이언트(public client)
    • 공개 클라이언트는 클라이언트 시크릿과 같은 자격 증명을 기밀로 안전하게 유지할 수 없는 클라이언트다. 이러한 클라이언트는 일반적으로 사용자의 기기나 브라우저와 같이 신뢰할 수 없는 환경에서 실행된다.
    • 보통 사용자의 디바이스에서 실행되므로 코드 디컴파일(decompile)이나 브라우저 개발자 도구를 통해 클라이언트 시크릿이 노출될 가능성이 있기 때문에 자신의 클라이언트 시크릿을 안전하게 보관할 수 없는 클라이언트(e.g. 모바일 애플리케이션, 웹 애플리케이션)를 의미한다.
    • 클라이언트 시크릿을 안전하게 보관할 수 없기 때문에 클라이언트 아이디만으로 클라이언트 애플리케이션임을 인증한다. 예를 들어 인증 서버로부터 발급받은 클라이언트 아이디를 통해 클라이언트 애플리케이션임을 인증하고 액세스 토큰을 발급받는다.

액세스 토큰을 발급 받는 OAuth2.0 권한 부여 방식에도 여러가지 방법이 존재한다. 아래서 말하는 사용자는 리소스 오너(resource owner)다.

  • 인가 코드 승인(Authorization Code Grant)
    • 가장 안전한 인증 방식으로 비공개 클라이언트(서버)에 적합하다.
    • 사용자가 인증 서버(e.g. google)에서 리소스 소유자 인증을 완료한다. 인증 완료된 후 발급되는 인가 코드(authorization code)를 사용해 OAuth2 클라이언트가 인증 서버로부터 액세스 토큰을 교환하는 구조다.
  • 묵시적 승인(Implicit Grant)
    • 리액트(react) 같은 SPA(Single Page Application)을 위한 간편 플로우다. 인가 코드 없이 바로 액세스 토큰을 발급받는다.
    • 사용자가 인증 서버에서 리소스 소유자 인증을 완료한다. OAuth2 클라이언트가 인증 서버로부터 리다이렉트 되면 인가 코드 교환 없이 액세스 토큰을 발급 구조다.
  • 리소스 소유자 자격 증명 승인(Resource Owner Password Credentials Grant)
    • 매우 신뢰할 수 있는 클라이언트(1st-party)만 사용할 수 있다. 1st-party 애플리케이션은 서비스 제공자가 직접 만든 애플리케이션을 의미한다.
    • 사용자의 ID/PW를 직접 클라이언트 애플리케이션이 받아서 토큰을 발급 받는다.
  • 클라이언트 자격 증명 승인(Client Credentials Grant)
    • 사용자 없이 클라이언트가 직접 인증 서버와 통신할 때 사용한다.(e.g. API 서버간 통신)
    • 클라이언 애플리케이션이 자신의 클라이언트 아이디와 시크릿을 사용하여 액세스 토큰을 발급 받는다.

2. Security risky scenarios

지금부터 보안적 리스크가 발생할 수 있는 시나리오를 살펴보자. 시나리오는 웹 애플리케이션을 기준으로 설명한다. 1st-party 애플리케이션에서 사용하는 리소스 소유자 자격 증명 승인 방법과 서버간 통신을 위해 사용하는 클라이언트 자격 증명 승인에 대한 시나리오는 제외한다.

2.1. Implicit Grant

묵시적 승인 방식에서 어떤 식으로 인가 코드를 탈취할까? 묵시적 승인은 다음과 과정을 통해 이뤄진다.

  1. 사용자가 myapp.com 서비스에 접속한다.
  2. 사용자가 로그인 버튼을 누른다.
  3. 사용자는 인증 서버로 리다이렉트된다.
  4. 사용자가 리소스 소유자임을 인증한다.
  5. 사용자는 myapp.com 서버로 리다이렉트된다. 리다이렉트 URL 프래그먼트(fragment)에 액세스 토큰이 포함된다.
  6. 사용자는 myapp.com 서비스에 접속한다.


myapp 서비스는 리다이렉트 URL 프레그먼트에 포함된 액세스 토큰을 추출하여 사용한다.

const token = location.hash.split('access_token=')[1].split('&')[0];

위 시나리오에서 발생할 수 있는 보안 위험은 다음과 같다.

  • XSS 공격에 대한 취약점을 가진 애플리케이션인 경우 악성 스크립트에 의해 액세스 토큰이 탈취되어 외부로 노출될 수 있다. 악성 스크립트는 사용자가 작성하는 게시글 등을 통해 삽입될 수 있다.
  • URL 주소가 브라우저 히스토리에 기록된다. 공용으로 사용하는 PC의 브라우저라면 외부 다른 사용자에 의해 액세스 토큰이 노출될 수 있다. 브라우저 동기화나 히스토리에 접근할 수 있는 악성 브라우저 확장 프로그램에 의해서도 액세스 토큰이 노출될 가능성이 있다.

2.2. Public client with Authorization Code Grant

인가 코드 승인 방식엔 어떤 보안 리스크가 있을까? 공개 클라이언트의 인가 코드 승인 방식은 리다이렉션을 통해 인가 코드를 받고, 이를 통해 SPA가 액세스 토큰을 발급 받는 방식이기 때문에 액세스 토큰이 URL 주소에 직접 노출되지 않는다. 액세스 토큰이 브라우저 히스토리에 남지 않는다.

  1. 사용자가 myapp.com 서비스에 접속한다.
  2. 사용자가 로그인 버튼을 누른다.
  3. 사용자는 인증 서버로 리다이렉트된다.
  4. 사용자가 리소스 소유자임을 인증한다.
  5. 사용자는 myapp.com 서버로 리다이렉트된다. 인가 코드가 리다이렉트 URL 경로에 포함된다.
  6. 악성 브라우저 플러그인(혹은 확장 프로그램) 혹은 XSS 공격 스크립트가 인가 코드를 SPA 애플리케이션보다 먼저 획득한다.
  7. 리다이렉트에 포함된 인가 코드를 사용해 액세스 토큰을 발급받는다.
  8. 발급 받은 액세스 토큰을 악성 서버로 전송한다.


보안 수준이 높아지긴 했지만, XSS 공격이나 사용자 PC에 설치된 악성 프로그램들에 의해 여전히 보안 리스크는 존재한다. 공개된 클라이언트 아이디만 있으면 액세스 토큰을 발급 받을 수 있기 때문이다. RFC 문서에선 스마트폰에 악성 애플리케이션이 설치된 경우 인가 코드를 탈취되는 시나리오에 대해 설명하고 있다.

2.2. Credential client with Authorization Code Grant

PKCE 방식은 공개 클라이언트의 보안 리스크를 보완하는 방식이므로 비공개 클라이언트의 인가 코드 승인 방식의 보안 리스크는 글의 주제에서 조금은 벗어나지만, 이 부분도 간단하게 살펴보자. 비공개 클라이언트가 인가 코드 승인 방식을 사용할 땐 어떤 보안 리스크가 있을까? 비공개 클라이언트의 인가 코드 승인 방식은 보안성이 매우 높다. 비공개 클라이언트의 클라이언트 시크릿이 공개되지 않는다면 액세스 토큰 발급은 불가능하기 때문이다. 이 시나리오에서는 다음과 같은 클라이언트 시크릿이 개발자의 실수로 깃허브(github) 공개 저장소에 노출되었다는 상황을 전제로 설명한다.

  1. 사용자가 myapp.com 서비스에 접속한다.
  2. 사용자가 로그인 버튼을 누른다.
  3. 사용자는 인증 서버로 리다이렉트된다.
  4. 사용자가 리소스 소유자임을 인증한다.
  5. 사용자는 myapp.com 서버로 리다이렉트된다. 인가 코드가 리다이렉트 URL 경로에 포함된다.
  6. 리다이렉트 요청이 MITM(Man In The Middle) 공격에 의해 조작되어 attacker.com 서버로 전달된다.
  7. 공격자 서버는 사전에 획득한 클라이언트 시크릿과 리다이렉트에 포함된 인가 코드를 사용해 통해 액세스 토큰을 발급받는다.


위 흐름을 보면 알 수 있듯이 공격 가능성은 매우 희박하다. 사전에 클라이언트 시크릿을 미리 획득해야 하고 리다이렉트 요청을 가로채기 위한 작업까지 필요하다. 보안적으로 충분히 안전하기 때문에 인가 코드 중간에 가로챌 가능성은 매우 낮지만, 액세스 토큰 탈취가 불가능하진 않다.

3. PKCE(Proof Key for Code Exchange)

공개 클라이언트가 사용하는 묵시적 승인 방식은 보안이 매우 취약하다. 인가 코드 승인 방식을 활용하면 보안 수준이 높아지긴 하지만, 여전히 보안적으로 취약하다. 이를 보완하기 위해 PKCE(Proof Key for Code Exchange) 방식이 등장했다. RFC7636엔 다음과 같은 설명을 볼 수 있다.

OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack. This specification describes the attack as well as a technique to mitigate against the threat through the use of Proof Key for Code Exchange (PKCE, pronounced “pixy”).

공개 클라이언트의 PKCE를 사용한 인가 코드 승인 방식에서 액세스 토큰을 발급받는 시나리오는 다음과 같다.

  1. 사용자가 myapp.com 서비스에 접속한다.
  2. 사용자가 로그인 버튼을 누른다.
  3. 클라이언트 애플리케이션은 code_verifier을 만들고 이를 기반으로 code_challenge을 만든다.
  4. 사용자는 인증 서버로 리다이렉트된다. 리다이렉트 요청에는 code_challenge와 챌린지를 만들 때 사용하는 해시 알고리즘이 포함된다. 인증 서버는 code_challenge와 해시 알고리즘을 저장한다.
  5. 사용자가 리소스 소유자임을 인증한다.
  6. 사용자는 myapp.com 서버로 리다이렉트된다. 인가 코드가 리다이렉트 URL 경로에 포함된다.
  7. 악성 브라우저 플러그인(혹은 확장 프로그램) 혹은 XSS 공격 스크립트가 인가 코드를 SPA 애플리케이션보다 먼저 획득한다.
  8. code_verifier 없이 리다이렉트에 포함된 인가 코드를 사용해 액세스 토큰 발급 요청을 수행한다.
  9. 인증 서버는 code_challenge의 유효성을 검증한다.
  10. 액세스 토큰 발급이 실패한다.


클라이언트에 설치된 악성 프로그램에 의해 인가 코드가 탈취되더라도 code_verifier 검증에 의해 액세스 토큰 발급이 불가능하다. code_verifier는 매번 인가 요청을 보낼 때 생성하는 랜덤 문자열이다. RFC7636을 보면 다음과 같은 규격을 따른다.

  • 고난도 암호학적 무작위 문자열이다.
  • A-Z, a-z, 0-9, -, ., _, ~을 사용한다.
  • 최소 길이는 43자, 최대 길이는 128자이다.
  • 모바일 애플리케이션의 경우 메모리 변수에 저장한다.
  • SPA의 경우 리다이렉트가 될 때 메모리 변수 값이 초기화되므로 세션 스토리지를 사용한다.

client_challangecode_verifier를 변환한 값이다. 다음과 같은 규격을 따른다.

  • plain
    • code_challenge = code_verifier
  • S256
    • code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

클라이언트 애플리케이션은 “S256” 알고리즘이 가능하다면 이를 사용해야 한다. “plain”은 보안적으로 취약하지만, “S256”이 기술적 문제로 지원하지 못하는 클라이언트들에게 허용된다.

code_challenge_method는 옵셔널(optional) 값이다.

  • 기본적으로 요청에 포함되지 않는 경우 “plain”으로 인식된다.
  • “S256”과 “plain” 두가지 메소드가 있다.

4. Is PKCE safe enough?

PKCE는 정말 안전할까? 액세스 토큰을 발급 받을 때마다 매번 code_verifier을 새로 만들기 때문에 인가 코드를 탈취하여 액세스 토큰을 발급받기 정말 어렵지만, 웹 SPA는 여전히 XSS 공격에 취약하다. 리다이렉트 흐름으로 인해 변수가 초기화 되므로 code_verifier 반드시 브라우저 저장소에 저장하기 때문이다.

몇 가지 후보들이 있지만, 탭 단위로 값을 저장하고 탭 세션이 끊어지면 삭제되는 세션 스토리지(session storage)가 가장 적합할 것이다. 세션 스토리지는 자바스크립트(javascript)를 통해 접근이 가능하기 때문에 XSS 공격에 대해 방어가 반드시 필요하다. 사용 후 즉시 세션 스토리지에서 삭제하는 등의 로직을 통해 노출 시간을 최소한으로 감소해야 한다.

보안 리스크는 어디에 초점을 맞추느냐에 따라 달라질 수 있다. 액세스 토큰을 발급받는 과정에 초점을 맞춘다면 공개 클라이언트의 PKCE를 사용한 인가 코드 승인 방식은 가장 강력한 보안 방식이라고 이야기할 수 있다. 매번 달라지는 랜덤 code_verifier에 의해 인가 코드가 중간에 탈취되더라도, 공격자는 액세스 토큰을 발급 받지 못 한다.

액세스 토큰을 발급 받은 이후 이를 관리하는 것에 초첨을 맞춘다면 어떨까? 공개 클라이언트는 액세스 토큰을 브라우저나 모바일 애플리케이션에 저장해야 한다. 반면 비공개 클라이언트(서버)의 경우 발급 받은 액세스 토큰을 세션 서버나 데이터베이스에 저장한다. 액세스 토큰을 신뢰할 수 없는 일반 사용자들의 기기에 저장하는 것이 안전할까? 아니면 네트워크 제어, 방화벽, 운영체제 접근 제어, 침입 탐지 시스템 등 다양한 보안 메커니즘에 의해 보회되고 물리적으로 접근도 통제되는 서버 측에 저장하는 것이 안전할까?

OAuth2.0 인증은 인가 프로세스다. 리소스 소유자의 권한을 액세스 토큰 형태로 위임 받는 것이다. 액세스 토큰의 탈취는 누구나 리소스 소유자처럼 행동할 수 있다는 의미다. 액세스 토큰에 대한 관리 관점에서도 보안 리스크를 바라보면 좋을 것 같다. OAuth 2.0 보안 모범 사례(Security Best Current Practice)가 정리된 RFC9700에서는 모든 유형의 클라이언트(기밀 클라이언트 포함)에 PKCE 사용을 권장한다. PKCE가 기존 클라이언트 인증 방식에 더하여 추가적인 보안 계층(defense in depth)을 제공하기 때문이다. 비공개 클라이언트는 클라이언트 시크릿이라는 강력한 인증 수단이 있지만, 인가 코드 자체를 노리는 특정 공격 시나리오에 대해서는 PKCE를 사용하는 것보다 취약할 수 있다.

CLOSING

공개 클라이언트나 비공개 클라이언트에 대한 예제를 작성해보는 것도 좋을 것 같다. 시간이 된다면 리액트 애플리케이션, 스프링 애플리케이션에서 관련된 예제 코드를 작성해봐야겠다.

REFERENCE

댓글남기기