Miscellaneous

Authentication(인증)과 Authorization(인가)

고구마엔사이다·2019년 12월 3일·조회 6,222

1. 인증

클라이언트가 자기가 '누구~'라고 주장하는 것이 '누구~'라는 것이 맞는지 확인하는 절차.

대표적인 것이 ID/PW, 공인인증서, 2-factor 인증 등이다.

REST API를 사용하는 경우 API를 호출하는 대상을 확인하는 절차가 필요하다. (API 인증)


2. 인가

그가 특정 자원에 접근할 권한이 있는지 확인하는 절차. 대표적인 것이 접근제어 등이다.

REST API에서 /users라는 리소스가 있을 때 일반 사용자는 일부 사용자 정보만 볼 수 있지만 관리자 권한으로는 전체 사용자 정보를 볼 수 있는 것과 같다.


3. API Key

3.1. 방식

앱이 사용자 인증 정보를 알고 싶다면 UserInfo(예를 들어..)라는 인증 App에 요청한다. 이 때 자신을 증명하기 위한 API key를 함께 보낸다. 그러면 인증 App은 API key를 보고 이것이 누구의 key인지를 보고 권한을 확인 후에 답을 준다.

3.2. 문제점

  • 통신구간 암호화를 한다고 해도 key가 누출될 가능성은 존재
  • 주기적으로 key를 업데이트한다고 해도 상호 잘 업데이트되어야 함 (안되면 장애가 됨)
  • key만 하고 ACL을 안하면 보안 누출될 수 있음
  • Auto scale-out 등이 되었을 때 관리 부분

4. OAuth2

우선 OAuth 1.0은 기본적으로 user - consumer - service provider의 구조로 되어 있다. OAuth 1.0 인증을 3-legged OAuth라고 하는 이유이다.

그런데 2.0으로 오면서 달라진 부분들이 있다. 호환도 안되고 용어도 다르다. 

  • Resource owner : 자원 소유자로 protected resource에 접근하는 권한을 제공
  • Resource serverr : Access token을 사용하여 요청을 수신할 때 권한을 검증할 때 적절한 결과를 응답
  • Authorization server : Client가 성공적으로 Access token을 발급받은 이후에 resource owner를 인증하고 obtaining authorization을 한다.
  • Client : resource owner의 protected resource에 접근 요청을 한 후 적절한 결과를 응답한다.

API 방식과 OAuth2 방식의 차이점은 다음과 같다.

  • API 방식 : Request server - Response server 구조
  • OAuth2 방식 : Request sverer - Response server - 인증 server 구조 

흐름을 보자.

  • Request가 발생됨
  • App 서버는 사용자가 자신에게 로그인되어 있는 사용자인지 확인 후 로그인되어 있지 않다면 인증서버에게 사용자를 redirection
  • 인증서버는 사용자가 로그인되어 있는지 확인함
  • 그렇게 인증을 거치고 나면 이 사용자가 인가를 요청한 서버에 대한 권한이 있는지 확인함
  • Grant 과정을 거치고 Authorize code를 App 서버에 전달
  • App 서버는 Authorize code를 이용하여 사용자에 인증 정보에 접근
  • 단 이 Authorize code의 생명 주기는 매우 짧음, 이 생명 주기 내에 App 서버는 인증 서버로부터 Access token을 받아야 함 -> 이 Access token은 API key와 유사

Access token은 무의미한 문자열의 형태이다.


5. 토큰 기반 인증

흐름을 알아보자.

  1. 사용자가 로그인을 시도한다.
  2. 서버는 로그인 정보에 대한 유효성을 검증한다.
  3. 검증이 완료되면 사용자에게 응답과 함께 토큰을 주는데, 이 토큰은 정상적으로 발급된 토큰임을 증명하는 signature를 가지고 있다.
  4. 클라이언트는 발급받은 토큰을 쿠키 등에 저장을 한다. 그리고 요청할 때 헤더에 토큰을 담아서 보낸다.
  5. 서버는 토큰 유효성을 검사하고 응답한다.

세션과 차이점은 세션은 서버에 저장되어 있지만 토큰은 서버에 저장되어 있지 않다는 점이다. 그래서 서버에 부하가 없다. 또 서버가 여러대이면 서버끼리 세션을 공유하거나 Sticky 기법을 써야 한다. 

토큰도 장점만 있는 것은 아니다. 서버가 강제로 만료시킬 수 없다. 만약 토큰이 탈취되었다면 이 토큰은 만료될 때까지 공격자에 의해 사용될 수 있다. 


6. JWT (Json Web Token)

6.1. 소개

일종의 규약으로 인증 흐름의 규약이 아닌 Token 작성에 대한 규약이다. RFC7519의 엄연한 규약이란 말이다. 그리고 당연히 특정 언어에 대한 의존성도 없다. 자바, 파이선, C, 루비, 스위프트 다 된다.

기본적인 Access token은 무의미한 문자열로 Token에 대한 진위와 유효성을 매번 확인해야 한다. 그런데 JWT는 Token 내부에 위조여부 확인을 위한 값, 유효성 검증을 위한 값, 또는 인증정보 자체를 담아 보내기도 하여 인증 서버를 거치는 Token 확인 단계가 생략될 수 있다. 따라서 일단 Token을 받으면 다시 Token이 맞는지 확인 과정을 거칠 필요가 없어진다. 이는 곧 불필요한 네트워크 부하가 줄어드는 효과가 생긴다.

정리하자면 필요한 정보를 자체적으로 지니고 있는, 자가 수용적, 영어로는 self-contained이다. 

최근에 세션 기반 인증을 대체하는 수단으로 널리 사용되고 있다. 세션은 인증 서비스가 세션을 생성하고 진입점(API Gateway)에서 세션 유효성을 확인하는 방식인데 반해, JWT는 인증 서비스가 토큰을 발급하고 진입점에서 토큰 유효성을 확인한다.

6.2. 구조

. 을 구분자로 크게 3가지의 문자열로 구성되어 있다.

  • Header
  • Payload
  • Signature

예를 들면 xxxx.yyyy.zzzz이다.

6.2.1. Header

두가지 정보를 담고 있다. 

  • typ : 토큰의 타입을 지정하는 것으로 값은 JWT이다.
  • alg : 해싱 알고리즘을 지정한다. 보통 HMAC SHA256 혹은 RSA가 사용된다. 이 알고리즘은 토큰을 검증할 때 사용되는 signature 부분에서 사용된다.

예를 보자.

{
  "alg" : "HS256",
  "typ" : "JWT"
}

이 Header는 UTF-8 인코딩되어 있어야 한다. JSON의 기본 인코딩이 UTF-8이기 때문이다. 

6.2.2. Payload

토큰에 담을 정보 들어있다. 정보의 각 조각은 claim이라고 하며 이는 key/value 쌍으로 이루어져있다. 토큰에는 여러 claim을 넣을 수 있는데 claim의 종류는 다음과 같다.

  • registered claim : 서비스에 필요한 정보는 아니고 토큰에 대한 정보를 담기 위하여 이름이 미리 정해진 claim이다. 옵셔널하게 사용할 수 있다.
  • public claim : public claim들은 고유한 이름(collision-resistant)을 가지고 있어야 한다. 그래서 이름을 URI 형식으로 짓는다.
  • private cliam : 서버와 클라이언트 협의하에 사용되는 claim이다. 예를 들면 로그인 아이디 같은 것이다. public claim과 달리 임의의 정보이기 때문에 이름이 중복될 수 있는데 이로 인해 충돌이 날 수 있으니 주의할 필요가 있다.

앞서 Header 부가 JSON이고 그래서 UTF-8 인코딩이 되어 있어야 한다고 했는데 Payload의 경우 반드시 JSON이 아니어도 되기 때문에 UTF-8 인코딩 문자열일 필요는 없다. 보통 Base64 URL-Safe 인코딩이면 된다.

registered claim은 reserved claim이라고도 한다. key는 모두 3자리 문자로 iss(토큰발급자), aud(토큰대상자), exp(토큰만료시간), jti 등이 있다.

6.2.3. Signature

JWT의 마지막 부분으로 Header의 base64 인코딩 값과 Payload의 base64 인코딩 값을 합친 후 비밀키를 가지고 해시를 생성한다.

따라서 Signature는 비밀키를 가진 곳에서만 할 수 있고 공개키를 가진 어느 곳에서나 이 Signature에 대한 검증을 할 수 있다. 지금 이것은 서명이다! 암호화가 아니란 말이다. 암호화는 공개키를 가진 아무나 암호화를 할 수 있지만 비밀키를 가진 사람만 복호화가 가능하다. 정 반대임..

참고로 base64 인코딩을 하는 이유는 JSON은 "\n"과 같은 개행문자를 포함하고 있어서 HTTP Header에 넣기가 어렵다. 그래서 base64 인코딩을 통해 하나의 문자열로 변환하여 사용한다. 물론 base64가 암호화는 아니다. 그리고 Signature는 비밀키(Secret Key)를 알지 못하면 복호화할 수 없다.

이러한 시나리오를 생각할 수 있을 것이다.

  • 해커가 홍길동의 데이터를 보려고 한다.
  • Payload에 있는 홍길동 ID를 해커의 ID로 변경하여 인코딩하여 서버에 보냈다.
  • 서버는 최초 암호화된 Signature를 검사한다. 봤더니 Payload에는 해커의 ID가 들어있지만 Signature는 홍길동의 Payload를 기반으로 암호화되어 있기 때문에 유효하지 않은 토큰이 된다. 즉 해커는 홍길동의 비밀키를 모르기 때문에 토큰 조작은 불가능하다.

6.3. 최종 JWT

위의 Header, Payload, Signature 값을 다시 base64 인코딩하여 최종 JWT가 완성된다.

6.4. JWT 인증 방식

6.4.1. 일정 시간 미사용시 재인증

  • UI 페이지 이동 없을 때 : SPA인 경우 UI에서 타이머 적용하여 만료시간 체크
  • UI 페이지 이동 있을 때 : 서버에서 접속 기록 관리 및 체크

6.4.2. 중복 로그인 처리

registered claim의 jti 항목을 사용한다.

시나리오 : 홍길동이 jti:abc로 접속을 했다가 다시 jti:def로 접속한다.

  • Push 기반 : 강제 로그아웃시킬 이전 대상을 찾아 Push 메시지 보내 UI에서 로그아웃 시킨다 (중복 접속 로그아웃 이벤트)
  • 토큰 저장소 기반 : 유효한 토큰의 jti를 별도로 관리하여 로그인 시 업데이트하고 API Gateway에서 토큰 jti 체크 (jti:abc 요청 차단)

6.4.3. 특정 시간대 접속 방지

registered claim의 exp를 사용할 수 있다.

from datetime import datetime, timedelta

token = jwt.encode(
	{'login_id':data['login_id'], 'exp':datetime.utcnow() + timedelta(days=3)},
    SECRET_KEY, algorithm = 'HS256')

6.5. 단점

아래 단점은 사실 토큰의 장점이라고 한 것들이.. 단점이 됐다.

6.5.1. Self-contained

사실 토큰 자체에 정보가 있다는 것은 JWT의 장점이지만 어찌보면 단점이다. Payload 부에 Claim set이 저장되기 때문에 정보가 많아지만 토큰의 길이가 길어지고 네크워크 부하를 줄 수 있다.

또한 Payload 부는 암호화되어 있지 않다. 단지 base64 인코딩이다. 따라서 Payload를 탈취하면 디코딩하여 정보를 볼 수 있다. 이에 대한 대책은 중요 데이터를 넣지 않거나, JWE를 통해 암호화해야 한다. 그런데 JWE(JSON Web Encryption)이 많이 활용되는 것 같진 않다. 대부분 HTTPS 통신을 하고 있어서..

6.5.2. Stateless

무상태성. 토큰을 한번 만들면 서버에서 제어가 안되기 때문에, 그에 따라 임의로 삭제할 수도 없고 난감해지는 상황이 발생할 수 있다. 꼭 토큰에 만료시간을 넣도록 하자.

6.5.3. 클라이언트 사이드

토큰을 클라이언트 사이드에 저장해야 한다.

6.6. Access Token 만료 시간의 해결책

6.5.1. Access Token

자원에 Access 하는데 필요한 정보를 포함하고 있는 토큰이다. 만료 날짜를 가지고 있다.

탈취에 따른 보안 취약점을 해결 방안으로 유효기간 조정이 있다. 하지만 유효기간이 짧으면 로그인을 자주 해서 새로운 토큰을 받아야 하기 때문에 불편하다.

{
  "code":401,
  "error":"invalid_token",
  "error_description":"The access token provided has expired."
}

그러면 유효기간을 짧게 하면서도 좋은 방안이 있을까? 한다면.

6.5.2. Sliding Session + Access Token

유효한 Access 토큰을 가진 사용자 요청에 대해 서버가 새로운 Access 토큰을 발급해주는 방식이다. 매 요청마다 새로 발급해주는 것은 뭔가 낭비요소가 심해 보이고, 글 작성 전이라던지(글을 작성하다가 만료되는 참사 방지) 쇼핑몰에서 카트에 물건을 담을 경우 등의 시점에 슬라이딩 시키는 것이다.

다만 접속이 단발성으로 이루어지면 효과가 적다. 또 긴 만료시간을 갖는 Access 토큰을 사용하는 경우 유효기간의 의미가 없어지는 상황이 된다. 

6.5.3. Refresh Token

Access 토큰과 동일한 JWT 토큰인데, Access 토큰과 동시에 발급이 됩니다. 하지만 Access 토큰보다 유효기간이 길고, Access 토큰의 유효기간이 지났을 때 새로 발급해주는 일종의 키이다.

예를 들어 Access 토큰의 유효기간은 2시간, Refresh 토큰의 유효기간은 1주라고 하자. 사용자가 2시간 사용하고 나면 Access 토큰은 만료된다. 그러면 Refresh 토큰을 통해 다시 Access 토큰을 받을 수 있다.

물론 Refresh 토큰의 유효기간도 지난다면 사용자는 아예 새로 로그인해야 한다.

참고로 Access 토큰이 탈취당하면 정보는 노출된다. 하지만 유효기간이 짧기 때문에 사용성에 한계가 있다. 

[Access Token] + [Refresh Token] 시 흐름은 다음과 같다.

  1. 사용자가 로그인을 한다.
  2. 서버는 로그인 정보를 확인하고 Access 토큰, Refresh 토큰을 발급한다. 그리고 Refresh 토큰은 서버쪽 DB에 저장해준다. (사용자 DB 등)
  3. 사용자는 Refresh 토큰을 안전장소에 보관한 후 평소에는 Access 토큰을 헤더에 실어 요청한다.
  4. 잘 사용을 한다.
  5. Access 토큰이 만료되었다. 사용자는 이를 모르고 만료된 Access 토큰을 헤더에 실어 요청한다. 
  6. 서버는 Access 토큰이 만료되었음을 확인하고 권한이 없다고 응답한다.
  7. 사용자는 Refresh 토큰과 Access 토큰을 함께 서버에 보낸다.
  8. 서버는 사용자가 보낸 Access 토큰이 조작되지 않았는지 확인한다. 그리고 사용자가 보낸 Refresh 토큰과 앞의 2.에서 DB에 저장한 Refresh 토큰을 비교한다. 토큰이 동일하고 유효하다면 Access 토큰을 새로 발급해준다.
  9. 사용자는 새로 받은 Access 토큰을 실어 요청을 계속한다. 

(위 과정 중 Access 토큰 만료는 사용자단에서 Payload를 통해 유효기간을 확인할 수 있기 때문에 서버를 통해 만료를 확인하지 않고 직접 재발급 요청을 할 수도 있다) 

좋긴 하지만 단점이 있다. 검증 프로세스가 있어서 사용자와 서버 모두 뭔가 훨씬 복잡하다. 그리고 Access 토큰 재발급을 위한 몇차례 request-response가 있어 낭비 요소가 있다.

자, Refresh 토큰은 매우 중요한 역할을 한다는 것을 알 수 있다. 그리고 엄격하게 관리되어 함도 알 수 있다. 게다가 서버 체크 방식이기 때문에 속도의 중요성이 있다. 사실 JWT는 빠른 인증 처리를 장점으로 내세우기 때문에.. Refresh 토큰 기법은 사용한다는 것은 JWT의 장점을 fully 사용하는 것은 아니라고 할 수 있다. 또한 Refresh 토큰은 JWT의 스펙도 아니다. 어찌됐든 Refresh 토큰을 사용한다면 빠르게 인증해야 하므로 정보는 적게 담는다. 

그와 별개로 Refresh 토큰은 서버에 저장되어 있는 정보이기 때문에 Access 토큰과 달리 강제로 만료시킬 수 있다.

6.5.4. Sliding Session + Access Token + Refresh Token

Refresh 토큰의 만료를 연장하는 방식이다 Refresh 토큰을 슬라이딩하기 때문에 Access 토큰에 대한 빈번한 슬라이딩은 필요 없다.

지속성에 대한 장점이 있으나 결국 보안이 문제다. 간간히 접속해도 계속 연장되기 때문이다. 

6.6. JWT + MSA

모놀리스 아키텍처와 달리 MSA는 Access 토큰이 가리키는 권한을 확인하기 위해서 모든 서비스들이 특정 권한 서버와 통신해야 한다. 서비스가 늘어날 수록 권한 서버가 받는 부하는 커진다.

6.7. JWT + 클라우드

AWS Cognito와 연계 가능하다.

사용자는 Cognito를 통해 토큰을 생성/갱신한다. 수신한 Access, ID 토큰을 Header에 전달하여 인증/인가한다.

6.8. 성능

  • 클라이언트가 토큰을 주기적으로 확인하여 (exp 필드) 토큰이 만료되면 직접 토큰을 삭제하여 "로그아웃.." 같은 문구를 띄운다. 서버로 요청수를 줄이고 속도도 빨라지겠다.
  • 단순 조회는 토큰 검사 없이 하고 싶다면 : get 메소드에서는 interceptor를 타지 않게 코딩 등

6.9. 보안

근본적인 보안성을 얘기하자면, JWT는 그다지 안전하지 않다. JWT는 MSA 등장에 따라 편의와 보안을 타협하는 과정에서 태어난 인증 방식이다.

HTTPS, XSS에도 불구하고 토큰이 탈취되었다면 무효화할 방법은 없다. 다만 토큰 발급 시 접속 IP를 기록해두고 이후 요청 IP와 기록 IP를 비교하는 방법을 사용할 수 있다. Access 토큰, Refresh 토큰이 분리되어 있다면 Refresh 토큰에만 ID 정보를 적용할 수 있다. 

브라우저 핑거 프린팅을 사용한다.


7. 모바일

모바일 앱에도 비교적 간단하고 안전한 인증이 가능해진다.

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.