# access token, refresh token 처리

앨범 정보를 가져오거나 앨범을 재생하기 위해서 Spotify Web API를 사용하고 있다.&#x20;

특히 앨범 재생을 위해서는, 사용자가 스포티파이 계정으로 로그인하고 해당 계정을 통해 access token을 얻어 사용한다. access token을 이용해 사용자의 스포티파이 계정 정보 등에 접근할 수 있기 때문에, access token을 안전하게 사용하는 방법에 대해 찾아보게 되었다.

## 방법

### 방법 1: **로컬/세션 스토리지**에 저장

* 스토리지는 자바스크립트 코드 내에서 글로벌 변수로 읽기 / 쓰기 접근이 가능하다.
  * XSS 공격에 취약하다. 접근한 값을 이용해 사용자의 정보에 접근할 수 있다.

### 방법2: **쿠키**에 저장

* 쿠키 역시 자바스크립트 내 글로벌 변수로 읽기 / 쓰기 접근이 가능하다.
  * XSS 공격에 취약하다.
* 쿠키는 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송된다.
  * CSRF 공격에 취약하다. 유저가 악성 사이트를 방문하게 되어 사용자의 브라우저가 api 요청을 보내게 만든다면, 이때 쿠키가 자동으로 포함된다. 공격자는 유저 권한으로 액션을 수행할 수 있게 된다.

{% hint style="info" %}
**XSS(Cross-Site Scripting) 공격**

* 사용자 브라우저에서 악성 스크립트를 실행하도록 유도하는 공격 방식
  * 공격자가 악성 스크립트를 포함한 URL을 생성하여 사용자가 클릭하면, 피해자의 브라우저에서 실행됨
* 리액트에서는 JSX 내부에서 사용되는 값들은 React DOM에 의해 자동으로 이스케이프 처리(escaping)됨
  * \<script> → \<script> (실행되지 않고 문자열로 출력됨)
  * 하지만 `dangerouslySetInnerHTML`을 사용할 경우, 취약점이 발생 가능

**CSRF(Cross-Site Request Forgery) 공격**

* 사용자가 자신의 의지와 무관하게 공격자가 의도한 행동을 하도록 하는 공격 방식
  * 사용자가 로그인된 상태에서 악성 사이트 방문 시, 자동으로 원래 사이트로 요청이 전송됨
    {% endhint %}

결론적으로, **refresh token은 쿠키**로 저장하고 **access token은 클라이언트 변수**로 저장하기로 했다.&#x20;

사용자가 새로고침을 할 때마다 토큰이 사라지므로, 이를 위해 refresh token은 쿠키 저장 방식을 택했다. 대신 httpOnly 쿠키로 저장하여, 자바스크립트에서 접근할 수 없도록 하여 xss 공격을 방어할 수 있다. access token은 클라이언트에서 관리하면, JavaScript 코드에서 접근할 수 없다. 새로고침 시에는 refresh token을 사용해 새롭게 얻어오면 된다.

## 코드

### get-access-token API 작성

```typescript
// 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 공격을 방어

{% hint style="info" %}
**SameSite** 쿠키 속성

* 쿠키가 전송되는 조건을 제한 → CSRF 방어 가능
* 종류
  * `None`:  모든 요청에 전송
  * `Strict`: 크로스 사이트 요청에는 전송 X
  * `Lax`: 크로스 사이트 요청에서는 기본적으로 쿠키 전송 X. 같은 웹 사이트나 Top Level Navigation(웹 페이지 이동) 시에는 전송
    * Top Level Navigation
      * 링크(`<a>`) 클릭
      * `window.location.replace` 이동
      * `302` 리다이렉트를 이용한 이동
        {% endhint %}

### 2. 토큰 관리 hook

새로고침, 즉 토큰이 없으면 자동으로 토큰을 fetch하도록 커스텀 훅을 만들어 사용했다. 그리고 토큰을 필요로 하는 컴포넌트에서는 훅을 호출해 사용하고, accessToken 값이 null인 경우 enabled를 false로 설정하여 쿼리 함수가 호출되지 않도록 방지했다.&#x20;

```typescript
// 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
  });
  
  ...
```

## 참고자료

{% embed url="<https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0#-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%80%EB%82%98>" %}

{% embed url="<https://dj-min43.medium.com/xss-%EA%B3%B5%EA%B2%A9%EC%9D%84-%EC%A7%81%EC%A0%91-%ED%95%B4%EB%B3%B4%EB%A9%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-c2c1d9baf7ec>" %}

{% embed url="<https://seob.dev/posts/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%BF%A0%ED%82%A4%EC%99%80-SameSite-%EC%86%8D%EC%84%B1/>" %}
