-
OAuth 알아보기 1: 권한 부여 코드 승인 방식테크 2023. 4. 21. 21:00
OAuth 알아보기 시리즈
1. OAuth 알아보기 1: 권한 부여 코드 승인 방식
2. OAuth 알아보기 2: PKCE 방식
OAuth 등장 배경
다음과 같은 상황을 가정해 보자. 사용자 A는 Google ID를 가지고 있으며 Google에 여러가지 정보를 저장하고 있다. 가령 YouTube나 Google 검색 기록에 의해 좋아하는 음식 정보가 Google에 저장되어 있을 수 있다. 서비스 C는 자신의 서비스를 이용하는 사용자들의 Google에 저장된 여러가지 정보를 활용해서 특정 서비스를 제공한다. 가령 맛집 추천을 해줄 수 있다.
사용자 A가 서비스 C를 이용하려고 한다. 서비스 C는 Google에 저장되어 있는 사용자 A의 정보가 필요하다. 그렇다면 사용자 A의 Google ID 및 패스워드를 알아야 하는데 사용자 A의 Google ID 및 패스워드를 서비스 C에 넘겨주는 것이 바람직한가? 만약 서비스 C에 저장된 모든 정보가 악의적인 공격자에 의해 털린다면? 사용자 A의 Google ID 및 패스워드도 같이 함께 털리는 것이다.
OAuth
이런 상황을 막기 위해서 OAuth가 등장한다. OAuth는 Google, Meta와 같은 대규모 플랫폼에 저장된 사용자 데이터에 접근하기 위해 서비스가 접근 권한을 받을 수 있는 표준 프로토콜이다. 즉, 위의 예에서 사용자 A는 Google ID 및 패스워드를 서비스 C에 직접적으로 넘겨주지 않아도 서비스 C가 OAuth에 의해 Google에 저장되어 있는 사용자 A의 정보에 접근할 수 있는 것이다.
OAuth의 최초 버전은 OAuth 1.0이다. 하지만 여러가지 문제로 인해서 OAuth 2.0이 등장했다. 참고로 OAuth 1.0과 OAuth 2.0은 완전히 다른 프로토콜이다. 만약 두 프로토콜의 차이점을 알고 싶다면 공식 문서, rfc 문서를 참고하자.
이 포스트에서 설명할 주요 대상은 OAuth 2.0이다.
Terminology
OAuth 2.0에 대해서 자세히 알기 위해서는 몇 가지 terminology를 알고 있어야 한다.
리소스 소유자 (Resource Owner)
Google, Meta 등의 리소스를 소유하고 있는 사용자이다. 위의 예시를 가져와서 이야기를 해보자면, 리소스 소유자는 사용자 A이며 리소스는 좋아하는 음식 정보를 포함한 Google에 저장되어 있는 정보이다.
클라이언트 (Client)
리소스를 사용하려는 서비스이다. 위의 예시에서 서비스 C가 클라이언트이다.
인가 서버 (Authorization Server)
리소스 소유자를 인증하고 액세스 토큰(Access Token, 밑에 설명 있음)을 발급해 주는 서버이다.
리소스 서버 (Resource Server)
리소스를 가지고 있는 서버이다. 위의 예로 Google은 사용자 A와 관련된 정보를 가지고 있는 리소스 서버를 가지고 있다.
액세스 토큰 (Access Token)
사용자(=리소스 소유자) 데이터의 특정 부분에 접근할 수 있는 특정 클라이언트의 권한을 나타내는 토큰이다. 위의 예로 액세스 토큰 B는 서비스 C가 Google에 누적되어 있는 사용자 A의 정보 중 맛집 관심사 정보에 접근할 수 있는 권한 토큰이다.
액세스 토큰은 전송 및 보관될 때 기밀로 유지되어야 한다. 액세스 토큰을 직접 볼 수 있는 유일한 주체는 액세스 토큰을 가지고 있는 클라이언트, 인가 서버 및 리소스 서버이다.
OAuth 2.0을 사용하기 전에 선행되어야 할 작업
당연히 리소스 서버에 클라이언트를 등록해야 한다. 만약 Google의 리소스에 접근을 하고 싶다면 https://console.cloud.google.com/에서 새로운 프로젝트를 등록해야 한다. 이 때 클라이언트는 프로젝트를 등록할 때 redirect URI를 반드시 등록해야 한다. 등록이 완료되면 클라이언트는 리소스 서버로 부터 client ID 및 client secret을 얻는다.
Redirect URI
클라이언트가 성공적으로 액세스 토큰을 발급 받으면 리소스 소유자는 클라이언트가 사전에 등록한 특정 URI로 리다이렉션 된다. 이 URI을 redirect URI라고 한다.
Client ID
클라이언트가 리소스 서버로부터 받는 ID이다. Client ID는 공개 정보이기 때문에 유출되어도 상관 없다.상관없다. (Javascript 소스코드에 사용해도 상관없다.) Client ID는 이후에 authorization URL을 만드는 데 사용된다.
Client Secret
클라이언트가 리소스 서버로부터 받는 비밀 정보이다. Client ID와는 다르게 비밀 정보이기 때문에 절대 유출되어서는 안된다. GitHub repository에 커밋해서도 안되고 Javascript 소스코드에 사용해서도 안된다.
OAuth 2.0의 네 가지 인증 방식
OAuth 2.0은 네 가지 인증 방식이 있다.
- 권한 부여 코드 승인 (Authorization Code Grant)
- 암묵적 승인 (Implicit Grant)
- 리소스 오너 패스워드 크레덴셜 승인 (Resource Owner Password Credentials Grant)
- 클라이언트 크레덴셜 승인 (Client Credentials Grant)
여담으로 지금은 활발하게 OAuth 2.1이 만들어지고 있다. 여기에서 Implicit Grant나 Resource Owner Password Credentials Grant의 경우에는 legacy이기 때문에 OAuth 2.1에서는 더 이상 볼 수 없다. 어쨌든 이번 포스트에서는 네 가지 중에서 제일 보안성이 높은 Authorization Code Grant를 위주로 살펴볼 것이다.
실제 동작 과정을 자세히 알아보기 위해서 OAuth2.0 홈페이지에서 제공하는 playground를 이용했다. 클라이언트가 OAuth 2.0을 사용하려면 먼저 리소스 서버에 클라이언트를 등록해야 한다. 이 작업을 playground에서 대신해준다. 결과적으로 client_id와 client_secret을 리턴 받는다. Playground에서 제공되는 값들은 전부 임의의 값들이기 때문에 유효한 값이 아니다. 편의상 client_secret을 공개하긴 했지만 실제로는 절대로 공개해서는 안된다...! 추가로 리소스 소유자의 ID 및 패스워드도 제공받는다. 이제 이 정보들을 이용해서 각각의 인증 방식이 어떻게 작동하는지 살펴보자.
권한 부여 코드 승인 방식 (Authorization Code Grant Type )
OAuth 2.0의 인증 방식 중 가장 많이 사용되며 권한 부여 코드 승인을 위해 authorization code를 전달하는 방식이다. 앞서 이야기한 것처럼 권한 부여 코드 승인 방식 플로우를 이해하기 위해 홈페이지에서 제공하는 playground의 내용 또한 같이 살펴보았는데 이 때문에 여기에서 가져온 그림과는 조금 차이가 날 수 있다. 하지만 그림이 굉장히 깔끔하게 정리되어 있기 때문에 가져왔다.
- User -> 리소스 소유자
- Regular Web App -> 클라이언트
- Auth0 Tenant -> 인가 서버
- Your API -> 리소스 서버
1. 로그인 요청 (리소스 소유자 -> 클라이언트)
리소스 소유자(=사용자)가 클라이언트에게 서비스를 사용할 수 있도록 로그인 요청을 한다.
2. 로그인 요청 (클라이언트 -> 인가 서버)
클라이언트가 리소스 소유자에게 받은 로그인 요청을 인가 서버로 넘겨준다. 이 때 아래처럼 여러 파라미터를 함께 넘겨주는데 파라미터가 각각 무엇을 뜻하는지 살펴보자.
- response_type: 어떤 방식으로 OAuth2.0을 진행할지 선택하는 파라미터다. code로 넣으면 권한 부여 코드 승인 방식으로 진행된다.
- client_id: OAuth2.0을 진행하기 전에 리소스 서버로부터 받은 client ID이다.
- redirect_uri: OAuth2.0을 진행하기 전에 리소스 서버에 미리 알려줬던 리다이렉트 URI이다. 리소스 소유자는 성공적으로 로그인을 하게 되면 해당 URI로 이동한다.
- scope: 어떤 리소스에 접근할 수 있는지 나타내는 파라미터다. scope에 들어가는 값은 인가 서버에 의해서 결정된다. 위의 예시에서 사용자가 좋아하는 음식 정보를 가지고 오고 싶다면 interest_food_info_access라는 값을 넣을 수 있다.
- state: (옵션) CSRF 공격을 막기 위해서 사용하는 랜덤 문자열이다. OAuth2.0에서 어떻게 CSRF 공격이 일어날 수 있는지 state 값은 어떻게 CSRF 공격을 막을 수 있는지에 대해서 알고 싶다면 이 문서를 끝까지 살펴보자.
state 값은 옵셔널이기 때문에 state 값이 없는 원래의 권한 승인 코드 부여 방식은 CSRF 공격에 취약하다. 이 문제 뿐만 아니라 client secret 탈취 문제 등도 있기 때문에 OAuth 2.1에서 PKCE 방법을 밀고 있다. 이 부분에 대해서는 다음 포스팅에서 자세히 알아보도록 하겠다.
3. 로그인 페이지 제공 (인가 서버 -> 리소스 소유자)
인가 서버가 리소스 소유자에게 로그인 페이지를 제공한다.
4. ID 및 패스워드 제공 (리소스 소유자 -> 인가 서버)
리소스 소유자는 인가 서버에게 ID 및 패스워드를 제공한다.
5. authorization code 발급 (인가 서버 -> 리소스 소유자)
리소스 소유자가 제공한 ID 및 패스워드가 유효하다면 인가 서버는 리소스 소유자에게 redirect uri와 함께 authorization code를 넘긴다. authorization code는 후에 클라이언트가 인가 서버에게 액세스 토큰을 요청할 때 사용된다. authorization code의 유효 기간은 오직 10분이다. 인가 서버는 쿼리 파라미터로 authorization code 뿐만 아니라 스텝 2에서 클라이언트에게 받았던 state도 함께 넘겨준다.
6. 리다이렉트 (리소스 소유자 -> 클라이언트)
리소스 소유자는 앞서 인가 서버에게 받았던 URI를 클라이언트에게 넘긴다. 이 단계에서 클라이언트는 리소스 소유자의 브라우저에 저장된 state와 URI에 같이 넘겨진 state 값을 비교한다. 만약 이 두 값이 일치하지 않는다면 CSRF 공격이 일어났다고 생각하고 리다이렉트를 취소한다. 두 값이 같다면 리소스 소유자를 리다이렉션 시킨다. 클라이언트는 인가 서버에 액세스 토큰을 요청할 때 authorization code를 사용하므로 잘 가지고 있어야 한다.
7. 액세스 토큰 요청 (클라이언트 -> 인가 서버)
클라이언트는 앞 스텝에서 받았던 authorization code와 함께 인가 서버에게 액세스 토큰을 요청한다.
- grant_type: authorization_code로 설정해서 권한 부여 코드 인증 방식으로 OAuth2.0을 진행한다는 사실을 알린다.
- client_id: client ID
- client_secret: client ID와 마찬가지로 리소스 서버에 받았던 값이다. 이 값은 비밀로 유지해야 하기 때문에 POST 요청의 body에 넣는다.
- redirect_uri: redirect URL
- code: authorization code
8. 액세스 토큰 제공 (인가 서버 -> 클라이언트)
클라이언트의 요청이 유효하면 인가 서버는 클라이언트에 액세스 토큰을 제공한다. 클라이언트는 훗날 액세스 토큰을 사용해야 하기 때문에 DB에 저장한다. 액세스 토큰은 유출되면 안되는 정보이기 때문에 안전하게 저장한다. 만약 아래의 그림처럼 리프레쉬 토큰까지도 제공 받았다면 이것도 안전하게 저장한다. 리프레쉬 토큰에 대한 이야기는 다른 포스팅에서 하도록 하겠다.
9. 로그인 성공 (클라이언트 -> 리소스 소유자)
클라이언트는 리소스 소유자에게 로그인이 성공했다고 알린다.
10. 서비스 요청 (리소스 소유자 -> 클라이언트)
리소스 소유자는 리소스 소유자에게 특정 서비스를 요청한다. 예를 들어 주변 맛집 정보를 요청한다.
11. API 호출 with 액세스 토큰 (클라이언트 -> 리소스 서버)
클라이언트는 액세스 토큰을 사용하여 리소스 서버에 API 호출을 한다. 예를 들어 리소스 서버에 사용자가 좋아하는 음식 정보가 저장되어 있다고 생각하자. 클라이언트는 리소스 서버에 사용자가 좋아하는 음식 정보를 요청할 수 있다.
12. 리소스 제공 (리소스 서버 -> 클라이언트)
액세스 토큰이 유효하다면 클라이언트에게 리소스를 제공한다. 클라이언트는 리소스를 이용하여 적절한 서비스를 만든다. 예를 들어 사용자가 좋아하는 음식 정보를 활용해서 주변의 맛집을 추천해줄 수 있다.
13. 서비스 제공 (클라이언트 -> 리소스 소유자)
클라이언트는 리소스 소유자에게 서비스를 제공한다. 예를 들어 주변의 맛집을 추천해준다.
CSRF 공격은 어떻게 일어날까?
- 멜로리: CSRF 공격을 하려는 공격자
- 앨리스: 희생자
- 클라이언트: uploadphototofacebook.kr (가상의 홈페이지)
- 리소스 서버: Facebook
사용자가 uploadphototofacebook.kr 홈페이지에 사진을 업로드하면 자동으로 Facebook에 사진이 올라간다고 가정하자. 공격 시나리오는 아래와 같다.
1. 멜로리는 클라이언트인 uploadphototofacebook.kr에 로그인 요청을 한다.
2. 클라이언트는 Facebook에 로그인 요청을 한다.
3. Facebook은 멜로리에게 ID, 패스워드 입력을 요청한다.
4. 멜로리는 자신의 ID, 패스워드를 입력한다.
5. Facebook은 멜로리의 정보가 유효한지 확인한 후 authorization code 및 redirect URI를 멜로리에게 넘긴다. (CSRF 공격이 일어나려면 state 값은 없어야 하므로, 이 경우에서는 제외했다.)
6. 멜로리는 Facebook으로부터 받은 redirect URI를 authorization code와 함께 잘 저장한다.
7. 불쌍한 앨리스가 uploadphototofacebook.kr에 로그인을 시도한다.
8. 앨리스가 액세스 토큰을 발급하기 전, 멜로리는 앨리스의 승인 코드가 아닌 멜로리의 승인 코드로 바꿔치기한다.
9. 앨리스의 액세스 토큰이 정상적으로 발급된다. 이때 액세스 토큰은 앨리스의 Facebook에 접근하는 토큰이 아니라 멜로리의 Facebook에 접근하는 토큰이다. 하지만 앨리스와 클라이언트는 그 사실을 모른 채 잘못된 액세스 토큰을 저장한다.
10. 앨리스는 uploadphototofacebook.kr에 사진을 올린다. 앨리스는 자신의 Facebook에 사진이 올라가는 것을 기대하겠지만 실제로는 멜로리의 Facebook에 사진이 업로드된다.
state 값이 어떻게 CSRF 공격을 막을 수 있을까?
문제는 공격 시나리오 8번에서 멜로리가 앨리스의 승인 코드가 아닌 멜로리의 승인 코드로 바꿔치기를 했다는 점이다. 만약 state가 있다면 앨리스의 승인 코드를 멜로리의 승인 코드로 바꿔치기할 수 없다. 예를 들어서 멜로리가 로그인을 할 때 state 값으로 ABCDE를 받았다고 하자. 그렇다면 멜로리의 브라우저에는 ABCDE가 저장되었을 것이다. 마찬가지로 앨리스가 로그인을 할 때 state 값으로 GHIJK를 받았다고 하자. 그렇다면 앨리스의 브라우저에는 state 값으로 GHIJK가 저장되었을 것이다. 멜로리 입장에서는 앨리스가 어떤 state를 가지고 있는지 앨리스의 브라우저를 해킹하지 않은 이상 알 수가 없다. 클라이언트는 리소스 서버에 액세스 토큰을 요청하기 전에 state 값을 체크하는 단계가 있는데, 멜로리가 앨리스의 승인 코드를 자신의 승인 코드로 바꿔치기한다고 하더라도 앨리스의 state 값을 알 수 없기 때문에 이 단계에서 실패한다. 멜로리의 state를 넣게 될 경우 클라이언트는 1) 앨리스의 브라우저에 저장되어 있는 state 값과 2) 멜로리가 넣은 state 값을 비교하게 될 것이고 이 값은 다른 값이기 때문이다.
마치며
OAuth 2.0의 권한 부여 코드 승인 코드 방식은 지금은 굉장히 널리 사용되고 있으나 state 값을 사용하지 않는 이상 CSRF 취약점에 노출되어 있을 것이고 client secret 문제가 많이 있을 것으로 예상된다. 하루 빨리 최신 버전의 PKCE를 공부해서 보안성을 높일 필요가 있다.
레퍼런스
'테크' 카테고리의 다른 글
[OS101] 운영 체제를 이해하기 위한 컴퓨터 구조 기초 (4) 2023.09.05 [OS101] 운영체제 역사 및 기능 (0) 2023.09.05 [WWDC21] Mitigate fraud with App Attest and DeviceCheck (0) 2023.08.17 [Android] Keystore와 StrongBox의 정의 및 차이점 (1) 2023.08.17 [OS101] CPU 스케줄링 (0) 2018.08.11