티스토리 뷰

반응형

외/내부적으로 개인이 진행한 스터디 기록을 확인했으면 한다는 수요가 있어

현재 하루스터디 서비스에선 Google OAuth 로그인을 통해 개인 스터디 기록 조회 기능을 제공하고 있다.

 

하지만 처음 인증 관련 로직을 구현해 도입하는 과정에서 이해가 부족한 부분이 많았다.

다행히 최근 인증 플로우를 점검하면서 생각이 짧았던 포인트들을 많이 찾아내고 정리할 수 있었다.

이 기회에 인증 로직이 처음부터 어떻게 바뀌었는지, 어떻게 바뀌어야 하는지 정리해보려고 한다.

 


1.  Access, Refresh Token 방식과 JWT 사용 구조

 

맨 처음 인증 로직을 작성할 때는 토큰과 세션 방식의 차이를 인식해가는 수준이었다.

 

경험이 없으니 토큰과 세션 중 어떤 것을 사용할지 결정하기도 어려워 함께 사용할 OAuth 2.0에서 사용하는 방식인 Access Token, Refresh Token 기반의 이중화된 토큰 방식을 사용하기로 결정했다.

 

클라이언트 측에서 사용자 id를 활용하는 로직이 있어 JWT에 Id와 발급/만료 일시만 적은 Access Token(한 시간)을 만들어 Session Storage에 넣도록 하였다. Refresh Token(일주일)은 단방향 복호화만 가능하도록 만들었다. 처음에는 Local Storage에 담아두고 한시간에 한 번 Access Token이 만료되었다는 응답을 받으면 송신하도록 계획했으나 프론트 팀원들이 구현 로직의 편의를 위해 Cookie에 담아두고 사이트에 접속하면 항상 보내도록 하는게 어떻겠냐고 요청하였고 받아들였다.

 

한 시간마다 Access Token이 만료되나 자동으로 Refresh Token을 통한 재발급 로직이 수행 및 Refresh Token 수명을 증가시키는 처리를 하게 되고, 사용자가 연속 7일 이상 접속하지 않으면 Refresh Token까지 만료되어 아예 새로 로그인해야 하는 방식이다.

 

지금 생각해보면 말도 안되는 구조다. Refresh Token이 어차피 사이트 접속 시 매번 같이 담겨오므로 네트워크를 타는 횟수가 줄어들지도 않아 보안적인 이점도 없고 별도의 DB저장도 하지 않으므로 이중화를 하는 의미가 없는 방식이었다.

 

 

2. Refresh Token DB 저장 및 Rolling

 

그나마 다행히도 1번 구조를 구현하기 전에 백엔드 구현 기획이 바뀌어 Refresh Token에 대한 서버 저장을 하는 것으로 바뀌었다.

 

위의 구조에서는 Refresh Token이 탈취되었을 때는 특정 사용자가 회원을 탈퇴하기 전까지 무한으로 Access, Refresh Token을 발급받을 수 있는 위험이 있다. 조금이나마 낫고 당연한 방식으로 DB에 회원들의 Refresh Token을 저장, 특정 사용자가 Refresh Token을 사용해 Access Token을 재발급 시 기존의 Refresh Token을 만료시키는 Rolling 방식으로 구현하였다.

 

여전히 정상 사용자가 아닌 해커가 Refresh Token을 먼저 탈취해서 사용했을 시의 문제는 존재하나, 부분적인 세션 방식을 채택하게 되면서 필요하다면 특정 사용자의 로그인을 한 시간 이내 해제시킬 수 있는 여지가 생겼다.

 

 

시스템은 일단 동작했고 허점은 알고 있었으나 당장 문제가 나타나진 않았다.

 

 

3. Cookie 관련 보안 설정 변경

 

조금 공부를 한 뒤 로그인 관련 로직을 훑어보던 도중 허점이 너무 많아 놀랐다.

이 시점의 로그 아웃 기능은 매우 단순했다. 로그 아웃을 누르면 프론트 로직을 통해 Session Storage 내부의 Access Token을 제거하고, Cookie를 읽어 Refresh Token을 직접 제거한다.

 

일단 Refresh Token 자체를 Cookie에 넣는 것이 좋지 않다.

쿠키는 JS를 통해 브라우저 단에서 읽을 수 있는데 이 경우 사용자가 XSS공격을 위한 파일을 조회하거나 링크로 이동했을 때 나의 사이트의 쿠키까지 읽혀 보안 사고가 발생한다.

 

또한 우리 사이트는 HTTPS를 사용하므로 굳이 HTTP 통신에서의 쿠키 사용을 허용할 이유가 없었다.

이를 위해 WAS에서 쿠키를 생성할 때 간단하게 브라우저에서의 쿠키 접근을 제한하고 HTTPS에서만 전송이 가능하도록 바꿨다.

 

 

이제 브라우저 자체적으로 로그아웃 시 쿠키의 Refresh Token 접근 및 삭제가 불가능하므로 별도의 로그아웃 API를 호출하도록 바꾸었고 서버에서 쿠키의 Refresh Token을 제거하도록 설정하였다.

 

 

4. 로그아웃 시의 Refresh Token 만료

 

2번까지의 구현에서 가장 큰 문제는 Refresh Token이 탈취되었을 때 사용자는 Access Token이 만료되면 로그인이 풀리고 Refresh Token에 대한 대응을 아예 할 수 없다는 것이다. 다행히 3번으로 로직이 변경되며 로그아웃 시 API 호출을 하게 되었다.

 

쿠키에 저장된 Refresh Token의 수명을 0으로 줄여 클라이언트 측의 Refresh Token 삭제가 수행되는 것은 물론이고 DB 내에서 Rolling 처리를 위해 보유 중이던 Refresh Token도 제거하게 되었다. 이를 통해 정상 사용자가 로그인 후 로그 아웃만 진행하면 해당 계정의 사용자는 아예 OAuth 로그인을 다시 진행해야 하므로 이상 현상 발생 시의 사용자 수준에서의 대응 방법이 생긴 것이다.

 

 

5. 알고 보니 의미 없는 JWT

 

도입 초기에는 JWT를 왜 사용해야 하는지 장단점을 100% 이해하지 못하고 '자료가 많으니까', '다들 쓰니까' 따라서 사용했다. 이제는 충분히 이해하였는데 이해를 바탕으로 백/프론트에서의 JWT 사용 흐름을 점검해보니 필요 없겠다는 결론을 내렸다.

 

JWT의 장단점은 아래와 같다.

장점

  1. 복호화하여 클라이언트 측에서 토큰의 내용을 알아볼 수 있다
  2. 클라이언트 측에서 변조 후 전송 시 서버가 가진 secrete key로 복호화가 되지 않으므로 변조 감지가 가능하다.

단점

  1. 페이로드가 단순한 토큰 방식보다 크다

단순 토큰 방식의 문제인 변조에 대한 취약점을 페이로드의 일부분은 단방향 암호화하여 해결하면서도 원하는 부분은 클라이언트에서 볼 수 있는 것이 주요한 특징인데,

전체 사용 플로우를 알아보니 JWT로 만들어진 Access Token을 프론트에서는 정작 읽어보지 않는다는 것이었다. 사용자의 id만을 담았으나 이마저도 별도의 API를 통해 가져오고 페이지 내에서 저장하여 사용 중이었다.

 

사이트 우상단에 띄울 id를 포함한 기본 정보를 별도로 조회가 가능했다

 

이것이 가장 충격적이었다.

기획과 개발 간의 이해가 충분히 이루어지지 않았었지만 로직은 모두 구현 가능한 경우였다.

 

JWT의 가장 큰 목적인 변조 방지 및 정보 조회 기능이 필요 없어졌으므로 쓸 이유도 없어졌다.

민감한 정보가 포함된 것도 아니어서 시급하지는 않지만 기회가 되면 단방향 암호화된 정보를 토큰으로 넘겨주고 Spring Interceptor 단에서 복호화해(Spring Security를 쓴다면 거기서) DB 조회 없이 사용하는 방향으로 페이로드를 줄여야겠다.

 

 

6. 정말 하이재킹되어 토큰 탈취가 일어날 수 있을까?

 

Access - Refresh의 이중화 구조를 통해 토큰의 DB 조회 로직 제거와 세션의 서버 측의 강제 로그아웃 장점을 모두 넣을 수도 있으나,

처음에는 상대적으로 Refresh의 전송 빈도가 낮은 것 또한 토큰 탈취로부터 안정성을 높여준다고 생각했다. (물론 우리 로직에선 Cookie에 넣어 매번 보낸다...)

 

하지만 우리의 사이트가 기본적으로 HTTPS를 사용하는 시점에 페이로드를 하이재킹하여 내부를 열어보고 토큰을 얻어낼 여지는 없다고 봐도 된다. 사용자의 컴퓨터 자체가 악성 프로그램에 의해 공격받거나 공유 컴퓨터에서 로그아웃을 하지 않는 등의 문제에 대한 대응은 토큰 내부에 민감 정보를 넣지 않는 수준 이상으로는 어렵다. 동일한 유효한 토큰이 포함된 패킷 자체를 가로채 복사해서 계속 보내는 등의 공격은 패킷을 열어보는 것은 아니니 다른 문제다.

 

HTTPS에 대한 이해가 생기고 나니 굳이 이중화 구조를 선택할 필요도 없었다는 결론을 스스로 내리게 되었다.

세션이면 세션, 토큰이면 토큰, 하나만을 선택해

세션 방식을 쓰고자 한다면 NoSQL 기반의 빠른 조회속도를 활용해 앞단에서의 DB I/O를 최소화하고

토큰이면 단방향으로 암호화하여 서버에서만 해석가능하게 한 뒤 수명을 충분히 길게 주는 식으로 처리함이 적절할 듯 하다.

 

 


 

충분히 이해는 하였으나 가장 효율적이라고 생각되는 구조로 바로 넘어가기에는 당장 네트워크 상의 보안 문제도 없을 뿐더러 성능적 문제가 발생하는 부분도 아니어서 무리가 있다.

그래도 프로젝트 내의 코드를 되짚어보며 적절한 인증 방식에 대해 이해를 높일 수 있었다.

 

 

 

참고

RFC 6750 - OAuth 2.0 Bearer Token Usage 시의 Security Considerations

https://datatracker.ietf.org/doc/html/rfc6750#section-5

반응형