[Network] API 서버 인증(Authentication) & 인가(Authorization)

토이 프로젝트를 진행하는데, API서버에서의 회원가입, 로그인 및 유료, 무료 사용자등을 구분해야 할 일이 생겨서 이참에 인증과 인가를 제대로 공부하고, 정리해보려고 한다. 그리고, API 서버 구축, 보안 관점에서 보려고 한다.

인증 & 인가

우선 기본 용어부터 알아보자.

인증(Authentication)

  • 말 그대로 인증이다. 즉, 클라이언트가 우리 서버에 접속해도 되는지 검사하는 것.
  • 웹에서의 인증은 네트워크 보안과 떼놓을 수 없는 주제이다. 즉, 외부인(해커)의 공격을 원천 차단하여 서버를 보호하고, 클라이언트의 정보를 보호하는 것에 초점을 둔다.

인가(Authorization)

  • 인가는 인증과 목적이 다르다. 클라이언트가 리소스를 접근, 획득할 수 있는지에 대한 권한을 인가라고 한다.
  • 예를 들어, 무료 이용자가 유료 이용자의 권한을 동일하게 누리면 안된다. 따라서 DB에 권한정보를 분류해서 접근을 나누도록 해야 한다.

웹 인증 방법 (feat. HTTP, HTTPS)

인증과 인가는 알았으니 다양한 인증 방법에 대해서 알아보자. 인증을 하려면 클라이언트가 서버에게 “인터넷”을 통해서 요청을 보내야 한다. 인터넷을 통해서 요청을 보낸다는 것은, 패킷이 내가 소유하지 않은 장비들을 거친다는 것이고, 자연스럽게 누군가가 패킷을 도청할 수 있다는 말이 된다.

따라서 웹에서는 인증 방법이 꾸준히 발전해왔고, 이를 차근차근 알아보자.

HTTP BASIC 방식

가장 기초가 되는 방식이다.

  1. 클라이언트는 서버에게 인증이 필요한 요청을 보낸다. ex): GET /user/profile
  2. 서버는 클라이언트에게 401 응답코드로 인증이 필요하다는 것을 전달한다. 여기서 WWW-Authenticate 헤더에 어떻게 인증해야 하는가에 대한 정보를 포함하여 전송한다. Basic realm을 사용한다. → realm은 보호되는 영역을 설명하거나 보호의 범위를 알리는데 사용됩니다.
  3. 클라이언트는 회원 정보(아이디, 패스워드)를 base64 형식으로 인코드 한 것을 송신한다. Authorization 헤더에 Basic 으로 보낸다.
  4. 서버는 인증에 성공했다면 200, 실패했다면 다시 401을 반환한다.

base64 인코딩은 특별한 다른 정보 없이 복호화가 가능하기 때문에 암호화가 아니다. 따라서 도청 당하면 인코딩한 정보에서 회원 정보가 그대로 노출될 수 있다. 이는 웹 사이트에서 요구되는 보안 등급에 미치지 못한다는 면에서 그닥 사용되지 않는다고 한다.

또한 Basic 인증은 일반 브라우저에서는 로그아웃 할 수 없다는 문제도 있다. 따라서 Basic 인증을 사용한다면 기존 이미 인증된 기록을 인증 실패로 덮어씌우는 방법을 사용할 수 있다고 한다. 링크는 하단 참조에 걸어두었다. 간략하게 알아본 바에 대해서 설명하자면, 로그아웃 버튼을 클릭하면 새로운 쓰레기 인증값을 전송하도록 해서, 인증 실패를 하게 하여 기존 인증을 없애버리는 방법이다.

How to log out user from web site using BASIC authentication?

HTTP DIGEST 방식

앞서 소개한 BASIC 방식의 약점을 보완하기 위해 HTTP/1.1에서 소개되었다.

  1. 클라이언트는 서버에게 인증이 필요한 요청을 보낸다.
  2. 서버는 클라이언트에게 401 응답코드로 인증이 필요하다는 것을 전달한다. 여기서 WWW-Authenticate 헤더에 Digest realm을 사용하고, 챌린지 코드(nonce)를 해당 헤더에 포함하여 보낸다. 즉, realm, nonce를 무조건 포함해서 보내야 한다. nonce는 401 반환시 생성되는 유니크한 문자열이다. 이는 서버 구현 시 특정 규칙대로 생성하면 된다.
  3. 클라이언트는 Authorization 헤더에 username, realm, nonce, uri, response를 포함해서 보낸다. realm과 nonce는 서버에서 받은 그대로 보낸다. response가 중요한데, 이는 Request-Digest라고 불리는데, 클라이언트가 패스워드 문자열을 MD5로 계산한 것이다.
  4. 서버는 인증정보를 확인하고 성공했다면 200, 실패했다면 401을 반환한다.

Digest는 basic보다는 높지만, HTTPS에 비하면 낮다. Digest는 패스워드 도청의 문제는 해결할 수 있지만, 패킷 자체를 위장하는 것에 대해서는 방지할 수 있는 기능을 제공하지 않는다.

SSL 클라이언트 인증

패킷 자체를 위장하는 것을 방지하기 위해 SSL 클라이언트를 통한 인증이 등장했다. SSL 클라이언트 인증을 통해 사전에 등록된 클라이언트에서의 액세스인지 확인할 수 있다. 즉 해커가 위장하기 어렵다는 내용이다.

SSL/TLS 란?

SSL/TLS은 Secure Socket Layer의 줄임말로, L5, L4 사이에서 보안을 담당하는 프로토콜이다. IETF에 의해 TLS라는 이름으로 표준화 되었다.

SSL에서는 공개키 암호화 방식과 대칭키 암호화 방식을 적절히 섞어서 사용한다.

  • 공개키 암호화: 클라이언트와 서버간에 사용하는 키가 다른 경우. 비대칭키. 클라이언트가 공개키를 이용해서 암호화를 하고 수신측은 비밀키를 이용하여 해독한다.
  • 대칭키 암호화: 클라이언트와 서버간에 사용하는 키가 같은 경우. 공개키 방식에 비해 보안이 취약하나 훨씬 빠르다.

공개키가 거의 1000배는 느리다고 하니, 모든 통신을 공개키 방식으로 처리하는 것을 비효율적이다. 따라서 대칭키를 사용하는데, 대칭키 를 주고 받을 때만 공개키 암호화 방식을 사용해서 주고 받는다. 이후 여러 요청은 대칭키를 이용하여 수행한다.

그런데, 만약 공개키가 해커가 보낸것이라면?? 가짜 사이트에서 보낸 것이라면??

그럴 때를 대비해서 CA(Certificate Authority)인증 기관이 존재하는 것이다. 인증서를 해당 서버의 공개키에 대해 발행하고, 이로써 가짜 공개 키 위장을 막을 수 있다. 하지만 여기서 공개키를 다운로드 하는 것은 위험하기 때문에, 브라우저는 이미 주요 기관의 공개키를 사전에 내장한 채로 제품을 내놓는다고 한다!

SSL 클라이언트 인증 방법

  1. 클라이언트는 서버에게 인증이 필요한 요청을 보낸다.
  2. 서버는 클라이언트에게 클라이언트 증명서를 요구하는 “Certificate Request”라는 메세지를 송신한다.
  3. 유저는 클라이언트 증명서를 선택한다.
  4. 서버는 검증하고, 통과한다면 클라이언트의 공개키를 취득한다. 그리고 HTTPS에 의한 암호를 개시한다.

SSL은 단독으로 사용되지는 않고, 폼 베이스 인증 방식과 더불어 2-factor 인증의 하나로 이용한다. 다만 SSL은 클라이언트 증명서를 이용해야 하므로 비용이 발생한다.

무료 SSL?

그리고 서버 구현을 할 때 HTTPS를 사용하고자 하는 나같은 초보 개발자들을 위해 다음과 같은 무료 SSL 인증서 발급 사이트가 있다고 한다. 나중에 프로젝트에 적용해봐야겠다.

Let’s Encrypt

폼 베이스 인증

클라이언트가 서버 상의 웹 앱에 자격 정보(Credential)를 송신하여 그 자격 정보의 검증 결과에 따라 인증을 하는 방식이다. 주로 아이디, 이메일과 함게 패스워드를 입력하여 송신하고 검증 결과를 토대로 성공 여부를 결정한다.

그런데 여기서 HTTP 를 사용하면 아이디, 이메일, 패스워드가 그대로 노출이 될 텐데 어떻게 해야 하는가?? 이를 암호화 해야 겠다는 생각이 들어야 정상이다. 그런데 앞서 살펴본 SSL이 보낼 때 key를 이용해서 암호화를 해서 보내주는 것을 확인했다. 따라서 폼 베이스 인증을 할 때는 무조건 HTTPS를 사용해야 안전함을 알 수 있다.

사실 폼 베이스 인증은 HTTP 표준은 아니다. 그러나 BASIC, DIGEST는 보안상의 문제로 사용되지 않는다. 그래서 폼 베이스 인증을 사용할 수 밖에 없었고 HTTPS와 결합하여 사용할 수 있다.

쿠키(Cookie) & 세션(Session)

HTTP는 stateless 프로토콜이다. 따라서 사용자가 로그인을 했었는지, 저장을 못한다. 따라서 쿠키와 세션을 통한 인증 유지 방식이 도입된다. 쿠키를 이용하여 사용자를 식별할 수 있는 정보를 Set-Cookie 헤더에 주입하여 반환한 후 다음 요청때 쿠키를 확인하여 인증을 유지할 수 있다.

  1. 클라이언트가 자격 정보(Credential: 이메일, 비밀번호 등)를 서버에 넘긴다. 이때 HTTPS를 사용해야한다는 점을 앞서 말한 것.
  2. 서버는 검증하고, 세션 ID를 Set-Cookie 헤더를 통해 클라이언트에게 전송한다.
  3. 클라이언트는 이를 가지고 있다가, 다음 인증이 필요한 요청을 보낼 때 같이 포함하여 보낸다

그러나 쿠키는 XSS(Cross Site Scripting)공격을 통해 탈취가 가능하다. HttpOnly 옵션을 넣어 브라우저(Javascript)를 통해 쿠키를 확인, 조작하지 못하게 만들면 안전해진다. 하지만 여전히 네트워크 패킷을 직접 감청하는 것은 해결이 안된다. 고로, HTTPS를 사용해야 한다는 결론이 여기서도 나오게 된다. 개발 부주의로 HTTP로 코드를 작성하는 것을 조심해야 한다.

세션과 쿠키의 차이점을 물어본다면 해당 사용자의 정보를 어디에, 즉 쿠키를 어디에 저장하여 관리하는지를 토대로 말할 수 있다. 세션은 서버에서 관리하는 것, 쿠키는 클라이언트에 쿠키를 저장하여 관리하는 것이 가장 큰 차이라고 볼 수 있다.

쿠키만 이용하는 방식은 쿠키에 사용자의 정보를 필연적으로 넣어야 한다. 넣지 않는다면 결국 매번 DB를 확인해야 하는, 즉 인증 유지가 안되는 것과 마찬가지이다. 쿠키에 사용자 정보를 넣는다면 탈취및 해독이 되었을 경우 사용자의 정보가 노출 된다는 점이 가장 리스키하다.

세션은 패킷이 탈취된다면, 사용자의 정보는 노출이 되지 않는다. 그저 세션 아이디만 노출될 뿐. 따라서 탈취자가 이를 이용해서 재 액세스해서 추가적인 작업이 필요하다. 이를 막기 위해서는 서버에서 TTL, expire등으로 세션을 만료시키면 더이상 해당 사용자의 인증이 유효하지 않은 상태가 되므로 상대적으로 쿠키만 사용하는 것 보다 안전하다고 볼 수 있다. 따라서 세션의 expire time을 짧게 준다면 그나마 해결이 된다. 하지만 만료 되기 전에 털리면 털리는 것은 똑같다.

세션을 사용하는 경우 세션 아이디를 관리해야 한다는 문제점이 있는데, 이는 Redis와 같은 인메모리 디비를 많이 사용한다고 한다. 그러나 여러 서버로 scale-out이 된 경우 각 서버마다 캐시 일관성 문제가 발생하게 된다. 로드밸런서가 같은 사용자를 같은 서버로 보내줄리 없기 때문에 이전에 인증을 해도 새롭게 인증을 또 해야 하는 상황이 발생할 수 있다. 따라서 인증 서버를 따로 두는 방식의 형태를 취하기도 한다. 그러나 이 경우 모든 다른 서버들이 인증 관련 요청이 들어올 때마다 인증 서버에 검사 요청을 하게 되고 비효율적이다.

토큰(Token)

따라서 토큰이라는 인증 방식이 등장한다. 앞서 분산 환경에서 병목을 막기 위해서 토큰을 어떻게 활용하는지는 API 서버에서의 인증을 보며 찬찬히 알아보자.

API 서버 인증 및 보안

살펴보기 앞서, 내 프로젝트에서의 보안

앞서 웹 생태계에서의 인증, 인가에 대해 살펴보았고 보안관점에서 어떻게 사용해야 하는가를 알아보았다. 그러나 내가 지금 구현해야 하는 것은 API 보안이다. 자세히 살펴 보기 전에 내가 토이 프로젝트를 구현하며 전체적인 인증 흐름이 어떻게 진행되야 하는가에 대해 고민해보았다.

Authentication Scenario

앞 섹션에서는 전체적인 내용을 살펴 보았다면 지금은 프론트 서버에서 API 서버로 어떻게 인증을 할 것인가에 대해 알아보자. 위 그림과 같이 SPA 프레임워크에서 API 서버로 인증을 하기 위해서는 서버간 통신이 필요하다. 여기서 두 가지 방법이 등장하게 된다.

  • API Key: 사용자가 해당 서버에 접속할 수 있는 key를 발급 받는다
  • API Token: 토큰을 발급하여 토큰의 정보로 인증한다.

API Key

가장 기초적인 방법이다. 사용자는 API Key를 발급받고 인증이 필요할 때 이를 첨부해서 요청을 보내는 것이다. 서버는 이 API Key를 읽어서 식별한다. API Key 가 도난당한다면 한번에 모든 서버의 리소스가 뚫리기 때문에, 보안상 권장되는 방법은 아니다.

개인적으로 비트코인 자동 매매 봇 프로젝트를 개발했던 경험이 있는데 bitMEX API를 사용하기 위해서는 API Key, Secret Key 이렇게 두 개를 발급받아야 했던 것 같다. 그래서 그냥 중요하지 않은 정보들은 API Key 로 접근하고, 개인정보, 주문 등 여러 인증이 필요한 경우, Secret Key를 사용해서 권한 인증을 추가로 진행했다.

API Token

이 방식은 위의 그림과 유사하다. 다만 서버가 보통 2개로 나뉘어져 있다. 토큰을 발급하는 서버와, API 인증 서버로 나뉜다.

  1. 개발자, 클라이언트는 Credential을 통해 우선 로그인 인증을 한다.
  2. 서버측(토큰 관리,발급 서버)은 Token을 발급한다.
  3. 개발자는 생성된 토큰을 Authorization 헤더에 토큰을 넣어 API 인증 서버에 전송한다.

JWT(JSON Web Token)

JWT란 Json Web Token의 줄임말로 Header, Payload, Signature로 구성된다. JWT는 base64 url-safe 를 통해 인코딩 된다.

  • 헤더에는 JWT를 검증하기 위한 알고리즘 정보가 들어있다. JWT의 Signature는 RSA, SHA 등 여러 암호화를 통해 생성되는데, 어떤 알고리즘을 사용하는지 표시하는 부분이다.
  • 페이로드는 사용자의 패스워드를 제외한 여러 노출되도 되는 정보들이 들어간다. 그러나 식별이 가능해야한다.
  • 마지막으로 시그니처가 보안의 핵심이다. 헤더와 페이로드의 값을 합쳐서 헤더에 선언한 알고리즘과, 비밀키를 통해 암호화 한다.

위 3가지를 합치면 JWT가 완성된다. 이를 사용하는 방법은 여러가지로 구현할 수 있는데 대표적으로는 아래와 같다. (그리고 토이프로젝트에 사용할 방법이다)

  1. 인증 서버에 로그인 → 인증 되면 Access Token(JWT)을 발급, Refresh Token은 인메모리 디비에 저장. Access Token의 페이로드에 사용자 이메일, 권한 정도를 넣어서 해싱하면 다른 인증이 필요한 서비스에서
  2. Access Token 을 이용하여 여러 인증이 필요한 서비스에 접근하여 리소스 사용. 각 서비스에서는 비밀 키를 이용해서 토큰을 해독하면 사용자 식별과 권한 정보를 한번에 알 수 있다.

위와 같이 구현한다면 단점은 클라이언트로 토큰을 반환할때 정보가 인터넷 상에 노출되는 단점이 있어서 Access Token과 JWT를 구분하고 이를 게이트웨이를 통해 관리해주는 방법도 있다고 한다. 아래 포스트에서 해당 방법과 JWT에 대해 정말 자세히 설명되어있으니 참고하자!

JWT를 소개합니다. : NHN Cloud Meetup

Bearer 이란?

이런 단어를 볼 때마다 영어를 잘해야 겠다고 느낀다. Bearer이란 보유자라는 뜻이 있다. 즉 Bearer Token이란 이 토큰의 보유자에게 액세스 권한 부여 라는 의미를 갖고 있다. 아래 스택오버플로우를 통해 알게 된 사실을 간단하게 소개하려고 한다.

JWT가 등장하기 이전, 토큰은 그저 특수문자를 포함하지 않은 서버가 생성한 문자열일 뿐이었다. 토큰에는 특별한 “뜻(claim)”이 없었고, DB를 액세스 해야 그 뜻을 알아볼 수 있었다. 그러나 JWT는 payload에 JSON형태로 claim을 담기 시작했고 이를 통해 DB 액세스를 할 필요가 없어진 것이라고 한다.

What’s the difference between JWTs and Bearer Token?

따라서 Authorization 헤더에 토큰을 보내기 위해서는 Bearer 을 붙여서 보내면 된다는 것이 결론이다! JWT는 그런 많고 많은 토큰 중 하나일 뿐이다.