OAuth 2.0 / Refresh token, Authorization code

0xf4d3c0d3·2022년 4월 20일
0
post-thumbnail

개요

평소 OAuth 2.0에서 궁금했던것들. Refresh token이 왜 필요한건지, Authorization code Grant flow에서 왜 굳이 Access token 대신 Authorization code를 중간에 거치는건지에 대한 고민 같은것들을 정리해봤다.

고민에 대한 답을 정리 하기전에 OAuth 2.0에 대해 간단히 먼저 알아보고 가겠다.
시간이 없으면 바로 #궁금했던 것들 정리로 넘어가도 좋다.

잘못된 내용이 있으면 피드백 바랍니다. 감사합니다.


OAuth 2.0

OAuth는 Open Authorization의 약자로, 어떤 웹앱의 자원에 다른 웹사이트나 어플리케이션이 유저를 대신해 접근할 수 있도록 해주는 표준이다. 2012년에 OAuth 1.0을 대체해 지금은 온라인 인증을 위한 de facto 산업 표준이 되었다. OAuth 2.0은 유저의 인증정보를 공유하지 않고 클라이언트 앱이 유저를 대신해 보호된 자원에 접근할 수 있게 해준다.

즉, OAuth를 통해 우리는 3rd party client가 우리를 대신해 특정 자원에 인가된 작업만을 허용할 수 있다. 예컨대, AwesomeTODO 라는 누가 만들었는지 모를 서비스에 내 구글 계정정보를 건네지 않고도, 딱 필요한 "구글 캘린더 조회 권한"만 사용할 수 있게 해주는 것이다.

먼저 용어부터 정리해보기 위해 Role에 대해 정리하면..

Roles

  • Resource Owner: 보호된 자원을 소유하는 유저 또는 시스템

  • Client: 보호된 자원에 접근이 필요한 시스템. 해당 자원에 접근하기 위해선, 반드시 적절한 Access token을 갖고 있어야 함

  • Authorization Server: Client가 Access token을 달라는 요청을 받았을때, 인가를 성공적으로 수행하고 Resource Owner의 동의도 받았다면, 이를 발급해주는 서버. Autorization Server는 인가/유저동의 상호작용을 처리하는 Authorization endpoint와 기계 대 기계 상호작용을 처리하는 Token endpoint를 노출한다.

  • Resource Server: 유저의 자원을 보호하고 Client로부터 Access token을 받는 서버. Client로부터 받은 Access token이 유효하다면 적절한 자원을 반환한다.

Authorization Grant

위의 Roles절에서 Client는 어떤 자원에 접근하기 위해 Access token이 필요하다고 했다. Client는 이 Access token을 어떻게? 누구로부터? 얻을 수 있을까?

이를 위한 Authorization Grant는 Resource Owner의 인증 정보를 나타낸다. Client는 이를 이용해 Access token을 얻을 수 있다. Authorization Grant는 당연히 Resource Owner가 주는것이다.

RFC6749 명세에서는 크게 다음과 같은 4가지 Authorization Grant type을 정의하고 있는데, 웹앱인지 네이티브앱인지 웹브라우저를 실행하지 못하는 장치거나 server-to-server 어플리케이션인지 등등에 따라 각각 use case가 다르다고 한다.

우린 여기서 Authorization code와 Implicit만 보겠다.

Authorization code

Client와 Resource Owner 사이에 Authorization Server가 중간에 껴서 Authorization Grant를 발급해주는 방식이다. Resource Owner로부터 바로 인증을 요청하는 대신, Resource Owner로 하여금 Authorization Server에서 인증을 마치고 그 응답을 Client에 Authorization code로 넘기게 하는 식이다. 즉 이 방식은 redirection 기반이기 때문에 Client는 Resource Owner의 user-agent(일반적으로 웹 브라우저)와 상호작용 할 수 있어야 하고 Authorization Server로부터 들어오는 요청(redirection을 통해)을 받을 수 있어야 한다.

Resource Owner를 다시 Client로 Authorization code와 함께 redirect하기 전에, Authorization Server는 이미 Resource Owner에 대한 인증/인가 작업을 마친다. Resource Owner가 오직 Authorization Server랑만 인가 작업을 수행했기에, Client는 Resource Owner의 credential 정보를 알지 못한다.

Authorization code는 보안적으로도 몇가지 좋은점이 있는데, Client 자체를 인가할 수 있다는 점이나 Access token을 Resource Owner의 user-agent를 거치지 않고 Client에 직접 전달할 수 있어 잠재적인 노출을 줄일 수 있다는 점이 그렇다.

Implicit

Authorization code의 간소화 버전으로, JS같은 스크립트 언어를 사용하는 브라우저에서 구현된 Client에 최적화 되어있다. Resource Owner가 인증을 마치면, Authorization Server가 Client에게 Authorization code를 발급하는 대신 Access token을 바로 발급해준다.

Implicit grant flow에선 Access token을 발급할 때, Client에 대한 인가는 하지 않는다. 몇몇 경우엔 redirection URI를 통해 Client를 식별하면서 Client에 Access token을 전달할 수 있다는데, 이러면 Resource Owner의 user-agent에 접근할 수 있는 다른 어플리케이션에 Access token이 노출될 수 있기에 굳이 안하는게 좋아보인다.

이 방법은 secret을 안전하게 보관할 방법이 없는 JS 앱에서 사용한다고 한다.

Flows

... 일단 RFC나 다른 곳에서 정리되어 있는걸 나름 정리해봤지만 여전히 잘 이해가 가지 않는다.
정확히 어떻게 데이터를 주고 받길래 뭐가 안전하고 안전하지 않다는걸까? 그림으로 다시 보자.

Authorization code

     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization code ---------'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------'
     +---------+       (w/ Optional Refresh Token)

Authorization code flow는 Figure 3과 같다. Access token과 Refresh token 둘 다 받을 수 있고 신뢰할 수 있는 Client에 최적화 되어 있다. 앞서 설명했듯이, Authorization code는 redirection 기반 flow라서 Resource Owner의 user-agent와 상호작용할 수 있어야 한다. 단계별로 살펴보면..

(A) Client Identifier & Redirection URI

Client가 Resource Owner의 user-agent를 Authorization endpoint로 redirect 시킨다. Client는 client_id, scope, state와 함께 grant가 끝나면 어디로 user-agent를 redirect 시킬건지 알려줄 redirection_uri도 같이 파라미터에 넣어준다.

예를 들면 다음과 같다.

https://authorization-server.com/authorize?
  response_type=code
  &client_id=dple3JolZrc9R877kmADdK9J
  &redirect_uri=https://www.oauth.com/playground/authorization-code.html
  &scope=photo+offline_access
  &state=YqwDZqD8ax5GgqYZ

(B) User authenticates

유저(Resource Owner)는 링크를 타고 들어가면 아래와 같은 로그인 화면과 해당 앱에게 접근 권한을 줄것인지 묻는 동의 화면을 보게 된다.

(C) Authorization code

Approve를 해주면 위의 요청에서 redirect_uri로 넘겨주었던 주소 뒤에

?state=YqwDZqD8ax5GgqYZ&code=NwI_id9MzzCWRHLrMXIlx0o9S018ivFbHKUGK6xv9iHm1nOA`

를 붙여 Client로 redirected back된다. state는 CSRF 공격을 막기 위해 사용된다.

이전 요청의 state(YqwDZqD8ax5GgqYZ)와 동일하니 다음 단계를 진행해도 좋다. Authorization code를 Access token으로 바꾸는 과정이다.

(D) Authorization code & Redirection URI

이번엔 Client가 Authorization endpoint 대신 Token endpoint로 다음과 같은 POST 요청을 보낸다.

POST https://authorization-server.com/token

grant_type=authorization_code
&client_id=dple3JolZrc9R877kmADdK9J
&client_secret=M7boNj9r3MUmaI84NIi3jvbWWveZV_UMD_oSQLoOrQUPmR6Y
&redirect_uri=https://www.oauth.com/playground/authorization-code.html
&code=NwI_id9MzzCWRHLrMXIlx0o9S018ivFbHKUGK6xv9iHm1nOA

(E) Access Token w/ Optional Refresh Token

Authorization Server는 Client, Authorization code가 유효한지 redirect_uri이 (C)에서의 것과 동일한지 검사하고 모두 유효하다면, Client의 redirect_uri로 다음과 같은 응답을 보내준다.

{
  "token_type": "Bearer",
  "expires_in": 86400,
  "access_token": "8ipZN-GAj4Feyt-xgPqTCsIIvkdTa_RMNET3RTxzbBj-lpvYaq6nqYN1_gC3-VW9WjA_pSDK",
  "scope": "photo offline_access",
  "refresh_token": "qxEMeYIKHDRpEAwqa8E4h3mJ"
}

Implicit

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

이렇게 그림으로 그려보니 Implicit이 더 복잡해 보인다. 각 단계별로 살펴보자.

(A) Client Identifier & Redirection URI

여기서도 Client가 Resource Owner에게 Authorization Grant를 받기 위해 Authorization endpoint로 redirect 시켜줄 redirection url이 필요하다.

https://authorization-server.com/authorize?
  response_type=token
  &client_id=dple3JolZrc9R877kmADdK9J
  &redirect_uri=https://www.oauth.com/playground/implicit.html
  &scope=photo
  &state=NhLCrjy7ih4HUTtU

Grant type을 명시하기 위해 response_type이 token인것을 제외하면, Authorization code와 다를게 없다.

(B) User authenticates

이 부분은 Authorization code와 동일하므로 그림은 생략한다.

(C) Redirection URI with Access Token in Fragment

(B)에서 인증을 성공적으로 마쳤으면 Client의 redirect_uri로 redirected back 된다. 그런데 이번엔 Authorization code와 달리 query string이 아니라 다음과 같은 url fragment이 붙어서 redirect 된다. url fragment에 대해서는 (D)에서 설명한다.

#access_token=QqljQ16Zu000DIPiyv7G50GhY-4SLfupEU3tx3IUCjib4_XyO3BrVbQyBykY_BBIneHHk3Cz&token_type=Bearer&expires_in=86400&scope=photos&state=NhLCrjy7ih4HUTtU

Access token을 받았는데 더 할게 있을까? 라고 생각했는데 지금 잘 보면 Access token을 받은건 user-agent다.

(D) Redirection URI without Fragment

(A)의 redirect_uri와 (C)의 url fragment를 붙여 보면, 다음과 같은 web-hosted client resource의 주소를 알 수 있다.

https://www.oauth.com/playground/implicit.html#access_token=QqljQ16Zu000DIPiyv7G50GhY-4SLfupEU3tx3IUCjib4_XyO3BrVbQyBykY_BBIneHHk3Cz&token_type=Bearer&expires_in=86400&scope=photos&state=NhLCrjy7ih4HUTtU

user-agent는 이곳에 redirect되며 요청을 보내게 된다. 이때, RFC2616에 의하면, # 뒤에 붙은 access_token과 같은 정보는 HTTP 요청에 포함되지 않고 user-agent에만 남는다고 한다.

근데 이게 무슨 의미가 있을까? 어차피 Authorization Server -> user-agent 로 fragment가 넘어갈때 네트워크에 노출되는것은 마찬가지 일텐데. 라고 생각했다가 다음과 같은 결론을 내릴 수 있었다.

Authorization Server는 개인 개발자가 아니라 거대한 인증 기관이다. 당연히 HTTPS 연결을 지원한다. 그런데 web-hosted client resource 같은 경우는 HTTP만 지원할 수 있다. OAuth 2.0도 이를 감안하고 HTTP에서도 작동할 수 있게 설계되었다고 한다. 말인 즉슨, [Authorization Server -> user-agent]은 HTTPS가 보장되니 상관 없지만, [user-agent -> web-hosted client resource]는 신뢰할 수 없어 중요한 Access token을 user-agent에만 남겨둔다는 것이다.

(E) Script

web-hosted client resource는, embedded script로 user-agent에 남겨진 fragment를 포함한 redirection URI에 접근할 수 있는 웹 페이지를 반환한다.

(F) User-agent executes script

user-agent는 (E)의 script를 실행해 Access token을 추출한다.

(G) Access Token

이렇게 추출된 Access token을 user-agent가 Client에 넘겨준다.

Authorization code vs Implicit

그래서 도대체 각 Grant type을 언제 써야 할까? 신뢰할 수 있는 Client? JS앱? RFC만 봐서는 감이 안온다. Identity platform을 제공하는 Okta 개발 블로그를 참고해서 정리했다.

Implicit의 단점은 Access token이 Authorization code flow에서의 "trusted back channel"같은게 아닌 URL로 직접 반환된다는 것이다. 이렇게 되면 browser history에 Access token이 남아서 탈취 당할 수 있으니 대부분 유효기간을 짧게 준다고 한다. 여기서 back channel이란 User(Resource Owner) 없이 App(Client)이 Authorization Server로부터 직접 Access token을 받는 것을 말한다.

backchannel이 없으니 Refresh token을 사용해 Access token을 갱신하는것도 불가능하다. 매번 다시 User를 로그인하게 하거나, hidden iframe 같은 트릭을 써야 하는데 이는 Implicit flow의 존재의의를 해친다. Okta JS SDK 같은 경우엔 이를 heartbeat 라는걸로 seamless하게 해결했다는데 완전 불가능한건 아닌거 같다.

Implicit flow를 고려할만한 이유중 하나는 Authorization Server가 CORS를 지원하지 않을때가 있다. Authorization code에서는 JS앱이 Authorization Server에 POST 요청을 할 수 있어야 하는데, 이를 위해 브라우저가 그런 요청을 할 수 있도록 Authorization Server가 CORS 헤더를 적절히 설정해주어야 한다.

위 얘긴 2018년 자료인데, 2019년 자료를 찾다보니 "Is the OAuth 2.0 Implicit Flow Dead?" 이런 글을 볼 수 있었다. OAuth Working Group이라는 곳에서 Implicit flow와 JS기반 앱들을 대상으로 새로운 지침을 배포했다는 것이다. 특히 Implicit flow를 더이상 쓰지 말라는 얘기였다.

어차피 원래 궁금했던건 code를 중간에 왜 쓰냐 였으니 Implicit flow가 deprecated되든 말든 이 글에선 중요치 않지만, 이를 통해 code를 안쓰는 Implicit이 뭐가 문제길래 이렇게 된건지 알아볼 수 있을지 모른다.

OAuth 2.0의 Implicit flow는 나온지 10년이 넘었는데 당시의 브라우저는 지금과 많이 달랐다. Implicit flow가 필요했던 주된 이유는 예전 브라우저의 오래된 한계 때문이었는데, JS가 오직 요청을, 해당 페이지를 로딩한, 같은 서버에게만 할 수 있었다는 것이다. 하지만 표준 Authorization code flow는 Authorization Server의 token endpoint로 POST 요청을 날릴 수 있어야 했는데 보통 이런 endpoint는 app과 다른 domain을 가졌다. 즉 예전에는 이 flow를 JS에서 쓸 방법이 없었다는 것이다.

요즘에는 Cross-Origin Resource Sharing(CORS)라는게 보편화되서, 목적지 서버가 적절히 설정되있으면 JS가 domain이 달라도 요청을 보낼 수 있게 되었다.

Implicit flow는 어디까지나 Authorization code flow의 절충안으로 나온거기 때문에, Refresh token 같은것도 쓰지 못하고 유효기간도 짧아야 하고 스코프도 좁아야 하고 별로라는 점을 알 필요가 있다.

브라우저에서 Authorization code flow를 사용할 수 있게되었다더라도 JS앱에서 여전히 고려할게 있다. Authorization code flow는 Authorization code로 Access token을 받을때 client secret을 사용했는데 JS 앱에는 그런걸 안전하게 다룰 방법이 없다는 점이다. 만약 그런 secret을 소스코드에 넣었다면, 해당 앱을 사용하는 아무나 "view source"를 눌러 볼 수 있게 된다.

다행히 이런 문제는 모바일 앱에서도 똑같이 적용되었던 문제였기에, 진작 해결됐었다. PKCE extension을 사용해 해결하는 것이다. PKCE에 대해서는 필요하면 나중에 따로 정리하겠다.

Access Token & Refresh Token

Access token은 보호된 자원에 접근하기 위한 credential이다. 어떤 자원(scope)에 언제까지(expires_in) 접근할 수 있는지에 대한 정보를 포함한다.

token은 DB같은곳에서 인증정보를 쿼리할 수 있는 식별자를 나타내거나, JWT같은 self-contained token이 될 수 있다.

Refresh token은 access token을 발급받을때 사용하는 credential이다. 다음 그림을 보자.

  +--------+                                           +---------------+
  |        |--(A)------- Authorization Grant --------->|               |
  |        |                                           |               |
  |        |<-(B)----------- Access Token -------------|               |
  |        |               & Refresh Token             |               |
  |        |                                           |               |
  |        |                            +----------+   |               |
  |        |--(C)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(D)- Protected Resource --| Resource |   | Authorization |
  | Client |                            |  Server  |   |     Server    |
  |        |--(E)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(F)- Invalid Token Error -|          |   |               |
  |        |                            +----------+   |               |
  |        |                                           |               |
  |        |--(G)----------- Refresh Token ----------->|               |
  |        |                                           |               |
  |        |<-(H)----------- Access Token -------------|               |
  +--------+           & Optional Refresh Token        +---------------+

각 단계는 일반적인 Authorization Grant flow와 유사하니 설명은 생략한다.


Access token과 Refresh token을 표로 비교해보면 다음과 같다:

Access tokenRefresh token
ShapeLonger. it's a self contained tokenShorter. it's just an identifier
QuantityMore. one per scopeLess
FrequencyOften. per request to Resource ServerRarely. only when access token expired
Sent toResource ServerAuthorization Server
Revocable?No. it's statelessYes. it's managed by Authorization Server

TLS

RFC 67449 Spec에서 TLS를 MUST로 명시한 부분이 몇 가지 있어 정리해봤다.

Authorization Endpoint

Resource Owner와 상호작용해서 Authorization Grant를 얻는데 사용된다. TLS를 쓰지 않으면
credential 정보가 평문으로 노출되니, Authorization endpoint로 요청을 보낼때는 반드시 TLS를 사용해야 한다.

Redirection Endpoint

Authorization endpoint에서 Resource Owner가 상호작용을 마쳤으면, Authorization Server는 Resource Owner의 user-agent를 Client로 redirect 시킨다. 이때 redirect 되는 곳이 Redirection endpoint다.

Redirection endpoint는 TLS를 사용하는게 권장되지만 반드시 사용해야만 하는건 아니다.
왜냐면 당시 이 Spec이 작성될 당시엔 개발자들이 TLS를 사용하는게 너무 힘들었기 때문이다. 만약 TLS를 쓸 수 없다면 Authorization Server는 Resource Owner에게 위험하다고 경고를 해주는게 좋다.

Token Endpoint

Client가 Authorization Grant나 Refresh token으로 Access token을 얻는데 사용된다.
Authorization endpoint와 마찬가지로 TLS를 쓰지 않으면 credential 정보가 평문으로 노출되니, Token endpoint에 요청을 보낼때는 반드시 TLS를 사용해야 한다.

Access Tokens

Access token credential은 네트워크상에서나 어딘가 보관될때나 반드시 기밀성이 보장되어야 하며, 오직 Authorization Server, Resource Server, Client 끼리만 알고 있어야 한다. Access token credential은 반드시 TLS을 통해서만 전달되어야 한다. Implicit을 사용할 경우엔 Access token이 URI fragment로 전달되는데 이때 외부에 노출될 위험이 있다.

Refresh Tokens

취급은 Access Tokens 때와 같다.

Authroization Codes

Authroization Code의 전달은 secure channel을 사용하는게 권장되고, Client는 redirection URI가 network resource라면 TLS를 사용하는게 권장된다. Authorization code는 user-agent redirection으로 전달되기 때문에 user-agent history나 HTTP referrer header에 노출될 위험이 있다.

Authorization code는 반드시 수명이 짧아야 하고, 한번만 사용할 수 있어야 한다. 만약 Authorization Server로 같은 Authorization code가 반복해서 들어오면, 해당 Authorization code로 발급했던 Access token들을 모두 revoke하는것이 권장된다.

궁금했던 것들 정리

이만 처음에 궁금했던 것들을 정리해보자.

Q: Refresh token은 왜 필요한가?

A: 유효기간이 짧은 Access token을 Client가 자동으로 재발급해줘서 UX를 좋게 할 수 있다.

Q: 그럼 처음부터 Access token의 유효기간을 아주 길게 하면 안되는가?
A: Access token은 stateless해서 탈취되면 서버측에서 알 도리가 없을뿐더러, revocation도 불가능하다. 따라서 Access token의 유효기간을 짧게 해서 완화할 뿐이다.

Q: 그럼 Refresh token이 탈취되면, 공격자도 Access token를 발급받을 수 있지 않나?
A: 매 요청마다 Resource Server에 Authorization Header로 보내줘야 하는 Access token과 달리 Refresh token은 Access token이 만료되었을때만 보내면 된다. 또 Refresh token으로 Access token을 발급할땐 Client의 credential도 요구하기 때문에 더욱 안전하다. 따라서 Access token 보다는 비교적 덜 노출되고, 탈취되더라도 그 자체만으로는 Access token을 발급 받을 수 없다.

Q: Access token으로 Access token을 발급받는건 안되나?
A: "안된다."라고 어디 딱 적혀있는건 못봤지만, Refresh token의 역할을 Access token이 하게되면 몇가지 문제가 있어보인다. 앞서 언급했듯이 Access token은 self contained bearer token이라 탈취되면 서버가 이를 방어하기 어렵다. 그리고 그럴려면 매번 Access token이 만료되기 전에 token endpoint에 token 발급을 요청해야 하므로 주기적인 요청이 필요하다. 반면 Refresh token은 유효기간이 기므로 굳이 주기적으로 요청하지 않더라도 실제로 Access token 만료로 요청이 실패했을때만 갱신할 수 있다.(전자는 UX면에서 좋고, 후자는 부하면에서 좋아 보인다.) 즉 단순히 Access token으로 Access token을 발급할 수 있게 하는것은 Access token의 유효기간을 늘리는것과 별반 다를게 없다. 그리고 Access token의 역할은 보호된 자원(Resource Server)에 접근을 허가하는 credential이기 때문에 Authorization Server에 인증하는데 쓰이는 것은 OAuth 2.0 명세에 어긋난다고도 볼 수 있을것 같다.

Q: Access token도 Refresh token 처럼 그냥 식별가능할 정도의 opaque string을 쓰면 안되나?
A: 그럼 그냥 Session을 쓰는것과 다를게 없다. ... RFC 6749 1.4에 따르면 그렇게 쓸수도 있다곤 하지만.. 이렇게 하면 매번 Resource Server가 해당 Access token이 유효한지 Authorization Server에 쿼리를 날려봐야하는데 너무 비효율적이다.

Q: Access token을 그럼 그대로 self contained token으로 두되, 이를 Refresh token 처럼 Authorization Server가 관리하면 어떨까? (인터넷에서 이런 질문들이 많이 보여서 넣음)
A: 의미없다. "Refresh token은 왜 필요한가?"에 대한 답을 보고 이렇게 하는게 무슨 의미가 있는지 다시 생각해보자 (나도 이렇게 정리하기 전에는 이 방법이 뭔가 그럴듯하게 느껴졌음)

Q: Authorization code flow에서 왜 바로 Access token을 안 주는가?

A: 그 편이 좀 더 안전하기 때문이다.

Q: 뭐가 더 안전하다는 것인가?
A: 아래 Authorization code flow를 다시 보자.

     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization code ---------'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------'
     +---------+       (w/ Optional Refresh Token)

...A: (C) 에서 Authorization code가 아니라 Access Token를 내려준다고 해보자. 즉 [ Authorization Server -> Redirection URI]로 Access Token이 전달되는 것이다. 이때 두가지 위험이 있는데, 먼저 Redirection URI 다시말해 Redirection endpoint는 TLS를 지원하지 않을 수 있다. (OAuth 2.0 표준에서 허용함. 옛날엔 일개 개발자가 TLS를 지원하도록 하는일은 너무 힘들었기에) 그래서 MITM에 한번 탈취당할 수 있고, 데이터가 user-agent redirection으로 전달되기 때문에 user-agent history나 HTTP referrer header에 노출될 위험이 있다.

Q: 그런 위험은 Authorization code도 똑같은거 아닌가?
A: 맞다. 그래서 OAuth 2.0 RFC 6749는 반드시 Authorization code의 수명을 짧게하고 중복사용을 막도록 명시하고 있다. 그래서 Access token과 달리 탈취당하더라도 비교적 안전하다.

Q: 왜 (C)는 위험한데 (E)는 괜찮나?
A: Authorization code를 받을때와 Access token을 받을때는 조금 다르다. (A)~(C)는 redirection 기반으로 이뤄지고 (D)~(E)는 POST 요청으로 이뤄진다. 이때 OAuth 2.0 RFC 6749는 (D)~(E) 과정에서 사용되는 Token endpoint가 반드시 TLS를 사용하도록 명시하고 있기 때문에 이 때는 비교적 안전하다.

Q: 만약 모든 개발자가 TLS를 사용한다면, 이런 번거로운 과정이 사라질까?
A: 앞서 언급했듯이, Authorization code flow가 redirection기반인 이상 history나 referrer등 여전히 노출될 수 있는 부분이 많아 그렇지 않을것 같다.

Q: Implicit은 Authorization code가 없는데도 잘 되는거 아닌가?

A: 아니다. 2019년에 이미 OAuth 2.0 Security BCP(Best Current Practice)에서 Implicit 사용을 자제하라고 나왔다. 애초에 Implicit은 OAuth 2.0이 나왔을 당시 보편화되지 않았던 CORS로 인해 타협안으로 나온것이다. Client가 Authorization Server와 도메인이 다른게 일반적인데 Authorization code flow에서는 Token endpoint에 POST 요청을 날릴때 CORS 설정이 필요하기 때문이다. 더욱이 Implicit은 보안상의 이유로 Refresh token도 없으니 더 어려운 방법을 사용해 Access token을 갱신시켜주어야 한다. CORS가 보편화된 요즘, Authorization code 대신 Implicit을 사용해야할 이유가 크지 않다.

Q: Implicit은 그럼 어떻게 Access token을 발급 받는가?
A: 아래 Implicit flow를 다시 보자.

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

...A: (A)~(C)는 Authorization code flow와 유사하다. 그러나 (C)에서 Authorization code가 아니라 Access Token in Fragment와 함께 Redirection URI가 전달된다는 점이 다르다. 여튼 User-Agent는 Redirection URI에 의해 Web-Hosted Client Resource로 redirect된다. 이때 재밌게도 fragment로 보내진 Access token은 브라우저에만 남고 HTTP 요청에는 포함되지 않는다.(RFC2616 참고) Web-Hosted Client Resource는 TLS를 사용하지 않을 수 있기 때문에 MITM에 취약할 수 있는데 fragment 덕분에 이런 위험을 줄일 수 있다. Web-Hosted Client Resource는 Script를 반환하는데 이 Script가 브라우저의 Access token을 파싱해서 Client에 전달해주는 식이다.

Q: (D)는 fragment 덕에 TLS없이도 Access token 노출을 숨길 수 있지만 (C)에서는 어떤가?
A: (A)~(C)는 Authorization endpoint와의 상호작용을 나타내며, 다시말해 TLS가 보장된다는 것이다. (D)~(E)는 위험할 수 있으니 fragment를 썼지만 (C)에서는 크게 문제될게 없다.

Reference

1개의 댓글

comment-user-thumbnail
2024년 8월 17일

글 좋네요 잘 읽고갑니다.

답글 달기