access token, refresh token 처리
앨범 정보를 가져오거나 앨범을 재생하기 위해서 Spotify Web API를 사용하고 있다.
특히 앨범 재생을 위해서는, 사용자가 스포티파이 계정으로 로그인하고 해당 계정을 통해 access token을 얻어 사용한다. access token을 이용해 사용자의 스포티파이 계정 정보 등에 접근할 수 있기 때문에, access token을 안전하게 사용하는 방법에 대해 찾아보게 되었다.
방법
방법 1: 로컬/세션 스토리지에 저장
스토리지는 자바스크립트 코드 내에서 글로벌 변수로 읽기 / 쓰기 접근이 가능하다.
XSS 공격에 취약하다. 접근한 값을 이용해 사용자의 정보에 접근할 수 있다.
방법2: 쿠키에 저장
쿠키 역시 자바스크립트 내 글로벌 변수로 읽기 / 쓰기 접근이 가능하다.
XSS 공격에 취약하다.
쿠키는 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송된다.
CSRF 공격에 취약하다. 유저가 악성 사이트를 방문하게 되어 사용자의 브라우저가 api 요청을 보내게 만든다면, 이때 쿠키가 자동으로 포함된다. 공격자는 유저 권한으로 액션을 수행할 수 있게 된다.
결론적으로, refresh token은 쿠키로 저장하고 access token은 클라이언트 변수로 저장하기로 했다.
사용자가 새로고침을 할 때마다 토큰이 사라지므로, 이를 위해 refresh token은 쿠키 저장 방식을 택했다. 대신 httpOnly 쿠키로 저장하여, 자바스크립트에서 접근할 수 없도록 하여 xss 공격을 방어할 수 있다. access token은 클라이언트에서 관리하면, JavaScript 코드에서 접근할 수 없다. 새로고침 시에는 refresh token을 사용해 새롭게 얻어오면 된다.
코드
get-access-token API 작성
// src/api/spotify/auth/get-access-token/route.ts
const { token, error } = await getUserAccessToken(authorizationCode);
if (error || token === undefined) {
return NextResponse.json({
error: `fail to get access token with authorization code: ${error}`
}, { status: 500 });
}
cookieStore.set(KEY_SPOTIFY_REFRESH_TOKEN, token.refresh_token!, {
httpOnly: true,
sameSite: 'strict'
});
return NextResponse.json({ token }, { status: 200 });
sameSite: strict
외부 사이트에서 세션/인증 쿠키를 강제로 포함하는 요청을 차단 → CSRF 공격을 방어
2. 토큰 관리 hook
새로고침, 즉 토큰이 없으면 자동으로 토큰을 fetch하도록 커스텀 훅을 만들어 사용했다. 그리고 토큰을 필요로 하는 컴포넌트에서는 훅을 호출해 사용하고, accessToken 값이 null인 경우 enabled를 false로 설정하여 쿼리 함수가 호출되지 않도록 방지했다.
// useSpotifyAccessToken
const useSpotifyAccessToken = () => {
const accessToken = useTypedSelector(state => state.spotify.accessToken);
const dispatch = useDispatch<AppDispatch>();
useEffect(() => {
if (accessToken === null) {
dispatch(fetchAccessToken());
}
}, [accessToken, dispatch]);
return { accessToken };
};
// 스포티파이 토큰이 필요한 컴포넌트
const Page = () => {
const { accessToken } = useSpotifyAccessToken();
const { data, isError, isFetching } = useQuery({
queryKey: [],
queryFn: async () => {},
enabled: accessToken !== null
});
...
참고자료
Last updated