[OMT] 로그인 Token 보안1 (AccessToken 전역관리)

2024.03.05(화)

OMT

성능개선을 마치고 로그인과 관련된 부분을 리팩토링하기 시작했다.

현재까지 나는 accessToken을 LocalStorage에 저장해왔다. 우리 프로젝트에서는 refreshToken을 따로 사용하지 않고 accessToken이 만료되면 로그아웃 되도록 진행해왔다. 이 방법은 유저로 하여금 불편함을 느낄 수 있고 여러 공격에 있어 안전한 방법이 아니기에 반드시 리팩토링을 진행해야겠다. 생각했고 그게 오늘이다.

🧐 공격? 공격엔 무슨 공격이 있을까

우선 이런 토큰에 관련한 공격은 CSRF, XSS 공격 두가지가 있다.

XSS ( Cross Site Scripting )

악의적인 사용자가 웹페이지에 악성 스크립트를 삽입

해커는 이 XSS공격을 성공한다면 자바스크립트를 실행할 수 있고 쿠키, 로컬스토리지, 세션스토리지에 접근할 수 있다.

이 중 쿠키는 HttpOnly 속성을 추가해 이 공격을 방어할 수 있다.

CSRF ( Cross Site Request Forgery )

사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격

사용자를 해커가 만든 사이트에 접속하게 하거나, 특정 링크를 클릭하도록 유도하여 Cross Site 요청을 보내는 공격 기법.

사용자의 브라우저에서 요청을 보내기 때문에 쿠키는 함께 전달될 수밖에 없고서버 입장에서는 이게 사용자가 보낸 요청인지, 해커가 보낸 요청인지 알 길이 없다.

이를 막기 위해 쿠키에 Same Site 속성을 활용하면 된다.

정답?

AccessToken = 메모리에 저장

refreshToken = 보안이 적용된 쿠키에 저장

쿠키의 보안은 크게 3가지가 있다.

Http Only

Cookie를 HTTP 통신에서만 사용될 수 있도록하는 옵션.

설정을 하지 않을 경우 XSS 공격을 통해 쿠키에 쉽게 접근할 수 있다. Cookie에 Refresh Token을 저장한다면 필수.


Secure

Secure 설정이 활성화되면 HTTPS 를 통해서만 Cookie가 발급.


SameSite

Strict Cross Site에서 요청을 보내면 쿠키를 보내지 않는다.

Lax [ 현재 크롬에서 기본으로 설정되어 있다 ] Cross Site에서 요청을 보내면, 특정한 조건이 만족할 때만 쿠키를 전달. 특정한 조건이란 HTML의 최상단에서 페이지 이동이 일어나는 경우를 의미. <a> 태그나 location.href 속성을 사용하는 상황이 있다.

None Cross Site에서 요청을 보내면 쿠키를 전달.

쿠키에 대한 자세한 설명은 이곳에서 …. 거의 3일을 삽질했다..

자 그럼 이제 AccessToken을 메모리에 저장해서 관리해보자

나는 Next.js 13버전을 사용해 개발을 진행했고 전역변수 관리는 useContext를 사용했다.

1. AuthContext.tsx 작성

...

export const AuthProvider: React.FC<{ children: ReactNode }> = ({
  children
}) => {
  const [accessToken, setAccessToken] = useState<string | null>(null);

  return (
    <AuthContext.Provider value=>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth는 반드시 AuthProvider와 사용되어야 합니다.');
  }
  return context;
};

2. layout.tsx에 AuthProvider 적용

import { AuthProvider } from '@/components/AuthContext';

...

 <body className={inter.className}>
          <AuthProvider>{children}</AuthProvider>
        </body>

3. 로그인 코드 수정

기존 코드가 accessToken을 localStorage에 저장하는 식으로 코드를 작성했다면 아래와 같이 수정해주어 메모리에 저장하게 했다.

import { useAuth } from '@/components/AuthContext';
...

const { setAccessToken } = useAuth();
const accessToken: string | null = urlSearchParams.get('token');
...

    useEffect(() => {
      const fetchData = async () => {
        try {
          if (accessToken) {
            setAccessToken(accessToken);
            const res = await axios.get(`${OAuthURL}/api/v1/users`, {
              headers: {
                Authorization: `Bearer ${accessToken}`
              }
            });

            const UserInfo = res.data.body.response;
            localStorage.setItem('UserInfo', JSON.stringify(UserInfo));
            // 다른 정보도 필요하면 Context에 저장 가능
            router.push('/home');
          }
        } catch (error) {
          console.error('Axios Error:', error);
        }
      };

      fetchData();
    }, [accessToken]);
  }
  
  ...
  

이 파트에서 중요하지 않은 내용들은 생략했다.

유저 정보는 이름, 사진 정도뿐이라 localStorage에 저장하는 것을 수정하지 않았다.

이후 요청 헤더에 accessToken이 필요할 경우

const { AccessToken } = useAuth(); 을 활용해서 요청을 진행했다.

이제 accessToken은 메모리에 저장했으니 cookie에 refreshToken이 잘 저장되어 있는지 확인하고 갱신을 시도해보자