인터넷 기반 서비스, 특히 Cloud Computing 환경에서 개발되는 Application이라면 웹이든 모바일이든 꼭 사용하게 되고야 마는 OAuth2와 JWT, 그것을 바탕으로 하는 인증/인가 체계의 개념에 대하여 최대한 쉽게 정리한다. (사실, 쉽게 쓴다고 해도 쉬운 내용이 아니지만 일반적으로 접할 수 있는 Protocol 자체에 대한 관점보다는 사용하는 입장과 왜 이렇게 쓰는지를 이해하기에 조금이라도 수월한 글이 되었으면 좋겠다)

아무튼, 거의 일년만의 포스팅인데 느낌은 10년 만인 것 같다. 이제 가을이 오고 그동안의 바빴던 일들도 대충 정리가 되어 숨통이 트여가고 있으니, 그간 남겨놓은 기록들을 웹으로 옮길 시간이 된 것 같다. 오늘은 그 첫번째 시간으로 OAuth2의 이해. 왜 이 글이 Reboot의 시작이 되었냐면, 얼마 전에 이 기술을 기반으로 한 프로젝트를 하나 마무리했기 때문이기도 하다. 아무튼, 시작.

IT 세상이 발전하면서, 그리고 특히나 Web을 중심으로 한 Cloud 세상이 오면서 더욱 중요해진 것 중의 하나가 “어떻게하면 흩어져 있는 Application 들의 인증 관리를 중앙에서 쉽게 할 것인가” 하는 것인데, 이것을 푸는 방식 중 대표적인 것이 바로 OAuth2 이다. Google, Facebook 등을 비롯한 대부분의 인터넷 기반 Application들이 이것 또는 그 변종을 사용하여 스스로를 인증하거나 누군가에게 인증 서비스를 제공하고 있다. 또, 근레에는 OpenID Connect라는(예전의 URL 기반 OpenID와는 다르다) 것이 등장하여 OAuth의 개념을 보다 편리하게 쓸 수 있도록 한 기술도 등장한 상태이다.

네안데르탈 API Key

근래의 Web Application은 초기 인터넷처럼 하나의 큰 Layer(3-Tier 구조와는 조금 다른 얘기)로 구성하기 보다는 Layer를 나누고 Sector를 나누는 방식으로 이루어진다. 풀어서 말하면, 화면 구성은 앞쪽 Layer의 ‘U’가 하고, 인증은 그 옆에 선 ‘A’가 하고, 업무 하나는 ‘J’가, 다른 하나는 ‘K’나 ‘L’이 나란히 뒤에 서서 처리하는 모양이 된다.

이렇게 분할하여 개발(MSA와 유사한 개념이나, MSA의 개념이 정립되기 전, 또는 SOA가 정립되는 과정에서도 그래왔음)을 하게 되면, 각각의 부분을 전체의 이해 없이도 개발할 수 있다는 점이라든지, 뭐랄까 흔히 얘기하는 Divide and Conquer가 가능해지기 때문에 개발과 유지보수가 매우 편리해진다. 그런데 현실은 그냥 그림으로 바기와는 좀 다르다. 이 서로 떨어진 것들이 어떻게 화합과 조화를 이루며 공존할 것인가가 매우 큰 과제가 되기 때문이다.

어쨌든, 그것을 푸는 가장 첫 번째 방식은 Application을 쪼개되, DBMS는 공유하는 것이었다. 앱을 따로 만들어도 DBMS(또는 그에 준하는 무언가)를 통해 자료를 공유하면 매우 간단하게 “통합”을 할 수 있다. 그런데 이 핵심적인 부분을 공유하다 보니, 만약 내 앱을 고치가가 그 중 하나의 Table을 주정하고 싶을 경우, 그 영향도를 쉽게 알 수 없으니 막막해지게 된다.
이렇게, DBMS를 공유하는 방식은 얼핏 생각해도 “쪼갠 게 쪼겐 게 아니다.”라는 결론에 이른다.(고는 하지만 지금도 그런 경우가 꽤 많은 것 같다)

그래서 등장한 것이, Data Tier에서 공유하지 않고 Logic Tier에서 공유하는 방식인데, 그게 일반적으로 API 방식으로 이루어진다. “사용자 정보든, 뭐든, 필요로 하는 녀석이 API로 요청해라. 그럼 내가 알아서 찾아서 줄게” 뭐, 이런 얘기다. 내 DBMS는 내가 알아서 관리하고 뒤지되, 정해진 API 규역으로 요청한 것만 잘 지키면 서로 다른 Module이나 Application 간의 공유가 간편해지고 각각의 독립성으 ㄹ보장할 수 있게 된다.

그런데 이렇게 API를 열고 보니… 접속자의 인증과 인가 관리가 필요해진다. 여기서 잠깐, 인증과 인가, 영어로는 Authentication과 Authorization이라고 표현하는데, “인증“이라 함은 “나 맞아“를 증명하는 단계이고 “인가“는 인증이 된 상태에서 “나 반장이야” 뭐 이런거? 좋지 않은 군대의 예를 들면 나는 그 시절에 “비취인가”라고 불리는, 비밀을 취급하도록 “허용된 자격“을 가지고 있었다. 뭐, 흔한 3급이었지만. 이게 인증과 인가다.

이런 API 통신의 인증과 인가 관리를 위해 가장 먼저 등장한, 그리고 가장 널리 보편적으로 쓰이는 기술이 바로 (네안데르탈) API Key 이다.

그림에서 보는 바와 같이, Application(Client)라고 써있는 박스가 업무를 담당하는 App 인데, 이 App이 사용자 인증정보를 알고 싶으면 User Info(Resource)라고 써있는 인증 App에게 물어본다.(번호가 이상하지만 3번) 그런데 이렇게 물어볼 때, 자신을 증명하기 위해 API Key라는 허가증을 같이 보내게 된다. 인증 App은 요청과 함께 온 API Key를 자신의 인증 저장소와 맞춰봐서, 이게 누구의 Key인지, 그가 가진 권한이 뭔지 확인한 후에 이에 부합하는 응답을 준다.

훌륭하다. 몇 가지 부분만 빼면, 정말 훌륭하고 아름다운 그림이다. (아, 내가 머리를 자르러 가서 애써 그린 이 그림 말고, 그 동작 방식의 그림 말이다.)

문제가 되는 부분은, 아무리 통신구간 암호화를 한다고 해도… 이 Key가 유출된 경우를 대비하지 않을 수가 없다는 점이다. 그래서 주기적으로 Key를 업데이트해야 한다는 문제도 있고, 이 업데이트가 양쪽에서 잘 맞아 이루어지지 않으면 서비스 장애로 이어진다는 문제도 있다. (깜박하고 안 바꿨는데요, 뭐 이런 거) 또, Key 하나로 이런 절차를 감당하다 보니… 별도의 ACL 등을 활용한 접속제러 등을 하지 않을 경우,(가령, 허가된 IP로부터의 접근에 대해서만 허용한다든지…) 보안 문제로 이어질 수 있다. 그런데 그걸 다 관리하려면 얼마나 복잡해… 특히나 Cloud 의 상징인 Auto Scaling 등으로 서버의 수가 변하고 IP가 유동적이라면 사실 상 관리가 되지 않는다.

똘똘한 OAuth2

항상 가려운 부분이 있으면 긁을 수 있는 효자손이 등장해 왔으니, API Key의 단점을 보완해줄 누군가가 나타났을 것인데, 그렇게 OAuth2의 시대가 열렸다. 앞서 가려웠던 부분을 조금 다른 방향으로, 그리고 단지 Application 대 Application의 약속을 넘어 사용자(관리자 말고 사용자)가 적당히 개입하는 방식으로 발전한 것인데…

앞서 본 API Key가 요청하는 서버와 요청받는 서버 정도로 단순한 구성이라면, 여기서는 요청하는 서버, 요청받는 서버, 그리고 인증하는 서버 등으로 조금 세분화가 이루어진다. (그래야, 중립적인 위치에서 인증을 전담하는 구조가 성립하므로…)

아래 그림의 번호를 따라 흐름을 보면, 먼저 0번의 로그인 요청이 있다. 꼭 로그인이 아니더라도, 가령, “상품 목록을 보여줘” 이런 것이 될 수도 있다. 그러면 업무서버(Application)는 일단 이 사용자가 자신에게 로그인이 되어 있는지를 본다. 그런데 “로그인이 되어있지 않다”, 그렇다면 파란 선의 1.을 따라서 인증서버에게 사용자를 돌려보낸다(Redirection). 단지 로그인이 아니라 인가가 없는 상태를 포함해서 하는 이야기이다. (참고로, 그림에서 파란선은 모두 Redirection을 표시한 것이고, 빨간 선은 사용자나 사용자의 브라우져가 개입하지 않는 서버간 직접 통신, 굵은 선은 사람이 개입하는 선이다.)

이렇게 1번을 통해 간접적으로 Authorize 요청을 받은 인증서버는, 이 사용자가 회원인지, 그리고 인증서버에 로그인이 되어있는지를 확인한다. (이 과정은 그름에는 생략되어 있다.) 인증을 거쳤다면 이 사용자가 최초에 인가를 요청한 서버(업무)에 대한 사용의사가 있는지(또는 권한이 있는지) 확인한다. (이 과정을 Grant라고 하는데, 대체로 인증서버는, 사용자의 의지를 확인하는 수준에서 Grant 처리를 하게 되고, 각 업무 서버는 다시 권한 관리를 할 수도 있다.) 만약, 사용자가 아직 Grant한 상태가 아니라면 사용자에게 Grant를 요청하게 된다. (흐름 1.1)

Grant는 사용자의 의지
Grant는 인가와는 조금 다르다. 인가는 서비스 제공자의 입장에서 사용자의 권한을 보는 것이지만, Grant는 사용자가 자신의 인증 정보(이름, 메일주소, 전화번호 등 개인정보가 포함되어 있을 수 있다)를 자신이 사용하려는 App에게 제공할지 말지를 경정하는 과정이다. (인가와 관련된 얘기는 다음에)

이렇게 사용자가 Grant 요청을 받게 되면, 이 순간에 한하여 사용자 개입이 일어난다. “업무서버가 내 인증정보를 읽을 수 있도록 허용해“라는 의미로 사용자가 직접 Grant를 해줘야 하기 때문인데, 이렇게 “응, 허가해”라고 답을 하고 나면 흐름 1.3을 타고 다시 인증서버의 인가 처리기에게 되돌려 보내진다. 그러면 이 처리기는 흐름 1.5를 따라 인증과 인가가 되었다는 결과와 이후의 과정에서 사용하게 될 인가 코드(Authorize Code)를 업무서버에게 전달하게 된다. 그런데 주의해서 볼 부분은, 1.3과 1.5 사이의 흐름 1.4인데, 이 인가 코드를 자신의 저장소에 저장해두게 된다.

이렇게 Authorize Code를 받은 업무서버는 이 코드를 이용하여 사용자의 인증 정보에 접근할 수가 있다…가 아니라, 이 코드는 보안을 위해 매우 짧은 기간 동안에만 유효하고, 그 기간 안에 업무서버는 다시 Access Token이라는 것을 인증서버에게 받아가야 한다. 이 Access Token은 앞서 본 API Key와 유사하에 사용되게 된다.

Access Token으로 넘어가기 전에, 중요한 포인트가 있는데, 이 Authorize Code의 수명과 전달 방식이다. 그림에서 보듯, 이 전달은 웹브라우져를 경유하게 된다. (흐름 1.5; Browser 박스를 지나는 흐름은 모두 그런 것임) 이 때에도 역시 HTTP Redirect가 활용되는데, 이 Redirect할 위치를 업무서버와 인증서버가 알고 있으면서 그것을 검증하고, Client인 업무서버의 주도적인 전달이 아닌 Redirect를 활용한 인증서버 주도의 흐름이 되다 보니, 다른 곳으로 샐 가능성이 상대적으로 줄어든다.(말하자면, 인증서버가 업무서버를 IP가 아닌 논리적 URL을 가지고 인증하는 것으로, API Key와 함께 자주 쓰이는 “접속 IP 제한”과 유사한 역할을 하는 것이다.

아무튼, Authorize Code를 받은 Application, 업무서버는 다시 이 코드를 이용해서 빠른 시간 안에 인증을 해준 서버에게 “네가 보내준 이거 잘 받았어. 이거 맞지? 이제 물건 줘“라고 요청하게 되는데, 이게 2번 흐름, Request Token 단계이다. 뭔가… 아버지가 남긴 칼날 조각을 들고 고구려로 찾아가는 유류… 같다. 인증서버는 이 Code를 다시 흐름 2.1을 따라 자신의 저장소에 저장해둔 정보와 일치하는지 확인하고, 이번에는 긴 유효기간을 갖는, 그리고 앞으로 실제 리소스 접근에 사용하게 될 Access Token을 업무서버에게 전달한다. 그리고 그 전에, 이번에도 이 Token을 자신의 저장소에 저장한다.

이제, Application은 이 Token을 들고 실제 사용자 정보가 있는 서버에 찾아가서 “나 공증된 Token 가지고 왔어, 리소스 줘“라고 요청한다. (흐름 3) 그러나 리소스 서버 역시 호락호락하지는 않아서, 단지 정부 발급 증명서라고 해서 그냥 믿기 보다는 얼른 무전기를 꺼내서 중앙에 물어보게 된다. “번호 XYZ9876 맞나요?” 왜냐하면, 이 Token이라는 것은 그 자체로는 의미가 없는 복잡한 문자열일 뿐이며, 그것의 유효성은 인증서버의 저장소에만 있기 때문이다. 이렇게 확인을 거친 후, 리소스 서버는 업무서버가 원하는 답을 건내준다.

이미 말한 바와 같이, 이 Token은 무의미한 문자열로, 기본적으로 정해진 규칙에 의해 발급된 것이 아니다. 그래서 증명확인이 필요할 때마다, 인증서버에 어떤 식이든, DBMS 접근이든 또다른 API를 활용하든 접근하여 유효성을 확인해야 한다. (심지어 공증여부 뿐만 아니라, 유효기간 문제도 있다.)

조금 복잡하긴 하지만, 키 분실 위협이라든지 보안 관점에서는 많은 부분이 개선되었지만 여전히 만족스럽지 못한 부분이 있다. (물론, 이 정도면 꽤나 훌륭하다.)

문자 쓰는 JWT, JSON Web Token

그러다가 등장한 것이 JWT, JSON Web Token이다. 이 규약은, 인증 흐름의 규약이 아닌 Token 작성에 대한 규약이다. 이미 말한 바와 같이, 기본적으로 Access Token은 의미가 없는 문자열로 이루어져 있으며, Token의 진위나 유효성을 매번 확인해야 하는 것임에 반하여, 이 JWT는 Token 그 안에 위조여부 확인을 위한 값, 유효성 검증을 위한 값, 심지어는 인증정보 자체를 담아서 제공함으로써, Token 확인 단계를 인증서버에게 묻지 않고도 할 수 있도록 만든 것이다. (이 글에서는 JWT의 3단 구조와 의미, 그 데이터의 세부적인 부분은 생략하고, OAuth2의 어떤 부분이 개선된 것인지만 설명한다.)

아래, JWT를 이용한 흐름을 그려봤는데, 뭐가 다른가? 이미 Token 안에 인증정보, 사용자 정보를 넣어버렸기 때문에, 그리고 그와 함께 Token 발급자 정보와 서명, 유효기간 등의 모든 관련 정보를 담고 있기 때문에, 아예 3번 대의 흐름이 그림에서 완전히 사라졌다. 아무튼, 다시 한 번 말하지만, 중요한 것은 일단 Token을 받으면 다시 “이 신분증이 맞나요?” 하며 확인 과정을 거칠 필요가 없다는 점이다.

이렇게, 서버에 직접 연결하여 인증을 확인하지 않아도 되게 되면 장점이 많다. 만약, Access Token 검사를 위해 인증서버 저장소에 접근해야 한다면, 그 부하도 부하지만 Latency 발생도 피할 수 없고, 업무서버를 여러 대 두거나 지리적으로 멀리 두는 것이 조금 힘들어진다.

모바일 시대

인증이 간단해지니 이제 모바일 앱에서 이 인증을 적용하게 되면, 또는 Browser 기반 Framework을 사용한 Client Side App에서도 이 인증을 적용하여 비교적 간단하고 안전하게 인증/인가 처리를 할 수 있게 된다.

아래 그림은 다시 그린 그림이 아니라 위의 그림을 조금 밀고 당겨본 것이다. 이렇게, App의 위치가 사용자 쪽으로 이동해도 앞선 흐름이 전혀 깨지지 않고 큰 문제 없이 동작한다는 것을 직관적으로 느낄 수 있다.

Cloud Computing 시대를 살면서, 이 OAuth2는 제공자와 사용자 모두에게 매우 중요하다는 생각이 든다. 제공자의 관점에서 Cloud 를 사용하는 사용자에게 “서버가 가끔 죽기도 해요”, “Auto Scaling 해야죠”라고 말할 때, 문제가 되는 부분 중 하나가 바로 인증정보와 세션정보의 공유 또는 유지이다. 간단하지 않은 부분이다. 이것을 단지 사용자의 몫으로 넘긴다면 사용자로써는 플랫폼을 선택하면서 숙제를 떠안게 되는 격이므로, 사용자가 그것을 쉽게 풀 수 있는 길을 제공하는 것이 중요하다 할 수 있다.

또한, 사용자의 관점에서도, 만약 사용자가 중급 이상 규모의 서비스를 제공한다고 한다면, 그것을 하나의 통으로 개발/관리하는 것은 효율적일 수 없다. 서비스의 연속성을 지키면서 동시에 유지보수성이나 즉응성을 높이려면 서비스의 분산 처리 및 유기적인 연계 관리가 필수적인데, 이를 위한 첫 번째 단계가 인증과 세션정보에 대한 유연한 관리일 것 같다.

느슨한 인증 통합을 제공하는 OAuth2와 JWT, 그리고 그와 관련된 기술과 사상은 이런 양쪽의 입장에서 매우 중요한 요소가 아닐 수 없다.

글이 길어지고 힘드니, 얼른 대충 맺는다. ㅠ.ㅠ