# [React/Node.js] 쿠키를 이용해 refresh-token 구현

## JSON 로그인 프론트엔드 + 백엔드 구현

프로젝트를 진행하면서 JWT (Json Web Token) 방식으로 로그인을 구현하기로 하였다. <s>(JWT에 대한 설명은 생략, 이 글을 읽는 사람들은 에러를 해결하기 위해 온거겟지?)</s>

백엔드에서 `access-token`은 `header`의 `Authorization`에 담아서 보내주고, `refresh-token`은 `cookie`에 담아서 보내기로 하였다. (보안 이슈 고려 body에 넣지 않음)

프론트에서는 `access-token`을 `localStorage`에 저장하고, `refresh-token`은 `cookie`에 저장하기로 하였다. (아래 표와 같음)

|  | 프론트엔드(저장 위치) | 백엔드(response 방식) |
| --- | --- | --- |
| ***access-token*** | localStorage | header.authorization |
| ***refresh-token*** | cookie | cookie |

> #### 프로젝트 과정에서 해당 방식으로 JWT를 구현하기 위해 삽질했던 기록을 작성한다.

> 똑똑한 사람들을 위해 최종 코드 먼저 제공

#### 백엔드

```jsx
return res
	.status(403)
	.cookie('refreshToken', refreshToken, {
    	expires: new Date(Date.now() + 259200),
	    httpOnly: true,
	})
	.header('Authorization', newAccessToken)
	.json({ message: 'Renewed expired access token' });
```

#### 프론트엔드

```jsx
import axios from "axios";
export const API = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
  },
  withCredentials: true,
});
```

---

---

처음에 설계했던 방법은 다음과 같다.

#### 프론트엔드

```jsx
import axios from "axios";
export const API = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
  },
  withCredentials: true,
});
```

#### 백엔드

```jsx
return res
  .status(403)
  .cookie("refreshToken", refreshToken, {
    httpOnly: true,
  })
  .header("Authorization", newAccessToken)
  .json({ message: "Renewed expired access token" });
```

`request`는 간단했다. 우리는 login post를 요청하였고 `headers`에서 `Authorization` 항목을 받아오고 싶었다.

하지만, CORS ERROR와 마주했다.

### <mark>[삽질 No.1] CORS 설정</mark>

```jsx
// login request code
try {
      const res = await API.post("/auth/login", userInput);
      // access token 저장
      localStorage.setItem("accessToken", res.headers.get("Authorization"));      
    } 
catch (error) {
  // status에 따른 Error Handling
  const errorResponse = error.response;
  const statusCode = errorResponse.status;
  console.log(statusCode);
  switch (statusCode) {
    case 40X:
      ...
	  break;
  }
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1705842043261/590bf02c-c688-433a-b2c6-577785471e75.png align="center")

#### <mark>❓CORS란?</mark>

**CORS**를 이해하기 위해서는 **SOP**를 이해해야 합니다.

브라우저에서는 보안적인 이유로 cross-origin HTTP Request들을 제한합니다. 말 그대로, 서로 다른 origin (간단하게 url) 끼리의 HTTP Request를 제한하는 것을 의미한다.

이를 SOP (Same - Origin Policy)라고 합니다.

즉, SOP는 동일한 출처 사이에서만 리소스를 공유할 수 있다는 규칙을 지니고 있습니다.

→ CSRF(Cross Site Request Frogery, 크로스 사이트 요청 위조 공격)을 대비해 만들어짐 (공격자의 요청이 사용자의 요청인 것처럼 속이는 공격 방식)

⭐예전에는 프론트엔드 / 백엔드가 분리되지 않아서, 처리를 같은 도메인 내에서 했기 때문에 해당 정책이 효과적으로 공격들을 막아낼 수 있었다(CSRF, XSS). 하지만, 이제는 프론트엔드 / 백엔드가 분리되고, 다양한 API들을 사용하기 위해서 동일하지 않은 **출처** 사이에서도 리소스를 공유해야 했다.

이때, SOP를 지키면서 몇가지 예외사항을 적용하는 것이 CORS이다.

✏️**출처**는 Protocol + Host + Port ([HTTPS://www.domain.com:3000)이다](HTTPS://www.domain.com:3000).

즉, 동일하지 않은 출처 사이에서도 HTTP Request를 가능하게끔 하는 것이 CORS이다. <s>(매번 에러가 발생해서 나쁜 놈인줄 알았지만 사실은 좋은 놈이였다)</s>

단, 모든 출처에서 접근할 수 있게 만들면 SOP가 없는것과도 같으니, ⭐**가능한 URL들을 지정해주는 방법으로 최소한의 접근만 허용하는 것이다.**

해당 프로젝트에서 어떻게 적용해야 할까?

#### 1.CORS설정으로 프론트에서는 백엔드를, 백엔드에서는 프론트엔드를 등록한다.

1. → 이때, 양쪽에서 등록해야 함을 주의하자.
    

```jsx
// 프론트엔드 (package.json)
{
  ...,
  "proxy": "http://localhost:3000" //백엔드 url
}
```

```jsx
// 백엔드 (cors_config.js)
const cors = require("cors");
exports.options = cors({
  origin: [
    "http://localhost:5173", // 프론트 url
  ],
});
```

이렇게 했는데도, CORS Error가 발생했다.

→ `The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include’`

#### 2.`credentials: true` 설정하기

**⭐CORS는 기본적으로 보안상의 이유로 쿠키를 request로 보낼 수 없도록 설계되었다.**

하지만, 다른 도메인을 가진 API 서버에 자신을 인증해야 하는 정상적인 응답을 받을 수 있는 상황에서는 쿠키를 통한 인증이 필요하므로, 해당 설정을 수정해야 한다.

```jsx
// 프론트엔드
import axios from "axios";
export const API = axios.create({
  baseURL: import.meta.env.VITE_BASE_URL,
  headers: {
    Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
  },
  withCredentials: true, // 추가하기
});
```

```jsx
// 백엔드
const cors = require("cors");
exports.options = cors({
  origin: [
    "http://localhost:5173",
  ],
  credentials: true, // 추가하기
});
```

#### 1.header의 `Authorization`에 접근 권한 설정하기

**⭐ header는 기본적으로 값들이 접근가능하지 않도록 설정되어 있는 경우가 많다.**

백엔드의 CORS 설정에서 `exposedHeaders` 값으로 노출하는 값을 변경할 수 있다.

```jsx
// 백엔드
const cors = require("cors");
exports.options = cors({
  origin: [
    "http://localhost:5173",
  ],
  exposedHeaders: ["Authorization"],
  credentials: true,
});
```

> ### 최종 코드

참고로, set-Cookie에 보낸 refreshToken은 브라우저에서 자동으로 쿠키에 저장해준다.

```jsx
try {
    const res = await API.post("/auth/login", userInput);      
    // access token 저장
    localStorage.setItem("accessToken", res.headers.get("Authorization"));
    // refresh token 저장 : 브라우저 자동
    // ... 유저 정보 저장 / redirect 등
  } 
catch (error) {
    // status에 따른 Error Handling
    const errorResponse = error.response;
    const statusCode = errorResponse.status;
    console.log(statusCode);
    switch (statusCode) {
      case 404:
        alert("해당 이메일로된 아이가 없습니다");
        break;
      case 401:
        alert("비밀번호가 틀렸습니다.");
        break;
      case 400:
        alert("Bad request (요청 형식이 잘못됨)");
        break;
    }
  }
```

### <mark>[삽질 No.2] 로그아웃 기능</mark>

로그아웃 기능을 구현할때, ✏️**access Token과 refresh Token을 삭제**하고 싶다.

access Token은 localStorage에 있으므로, 쉽게 삭제 가능하다.

**😡문제는 refresh Token이다.**

쿠키에 있는 refresh token에 접근해서 삭제하고 싶었다.

암만 찾아봐도 방법이 없었다. 실제로 불가능했기 때문이다.

**⭐백엔드에서 refresh Token을 쿠키에 담아서 보낼때** `httpOnly:true` **를 달아서 보냈는데, 해당 속성을 달아주면 프론트에서 암만 노력해도 접근할 수 없다. 일부러 막아둔 것이다.**

잘 생각해보면 저장할 때도 자동으로 저장하는 이유가, 프론트에서 접근하지 못하니까 친절하게 알아서 저장해주는 것이였다.

❓그러면.. 어떻게 삭제해???

쿠키에는 2가지 종류가 있었다.

| 세션 쿠키 | 사용자가 사이트 탐색 시에 관련한 설정들과 선호사항을 저장하는 임시 쿠키정도라고 생각하면 된다. 브라우저를 닫는 순간 삭제된다. |
| --- | --- |
| 지속 쿠키 | 지속쿠키는 세션쿠키와 다르게 삭제되지 않고 더 길게 유지가 가능하다. 지속쿠키는 디스크에 저장되며, 브라우저를 닫거나 컴퓨터를 재시작해도 남아닜다. 사용자 로그인 항상 유지와 같은 곳에 사용한다 |
| 차이점 | 세션쿠키와 지속쿠키는 파기시점 외에 차이점이 없다. Discard라는 파라미터가 설정되어 있거나, 파기까지 남은시간인 Expires또는 Max-Age라는 파라미터가 없으면 세션쿠키이다. |

즉, 우리는 지금까지 세션 쿠키를 사용하였지만, `httpOnly` 속성으로 인해 직접 쿠키를 제어할 수 없으므로, **지속 쿠키**를 생성하기로 하였다.

즉, `refresh-token`을 삭제하는 대신 ⭐**만료기간을 설정하는 방식**으로 진행하였다.

백엔드분들한테 `refreshToken`의 기간을 설정해달라고 요청하였다.

```tsx
return res
	.status(403)
	.cookie('refreshToken', refreshToken, {
		expires: new Date(Date.now() + 259200), // 기간 설정
		httpOnly: true,
	})
	.header('Authorization', newAccessToken)
	.json({ message: 'Renewed expired access token' });
```

지금까지, JWT를 구현할 때 삽질했던 기록들을 정리했습니다.

나중에 또 삽질할때 봐야겠다 :)

---

개인 블로그: [https://velog.io/@jihostudy/posts](https://velog.io/@jihostudy/posts)

edited by 김지호
