1) Access Token이 무엇인가요?
💡 Access Token은 사용자의 인증(ex: 로그인)이 완료된 후 해당 사용자를 인증하는 용도로 발급하는 토큰입니다.
이전에 저희가 구현하였던 쿠키(Cookie)에 jwt를 설정하고, 지정된 만료 시간이 지나면 인증이 만료되는 구조 또한 Access Token이라고 부를 수 있습니다.
인증 요청시 Access Token을 사용하면, 토큰을 생성할 때 사용한 비밀키(Secret Key)로 인증을 처리하게 됩니다. 이 방식은 복잡한 설계나 여러 분기 처리 없이 코드를 구현할 수 있다는 장점을 가지고 있습니다.
Access Token은 Stateless(무상태) 즉, Node.js 서버가 재시작되더라도 동일하게 작동하게됩니다. 이는 jwt를 이용해 사용자의 인증 여부는 확인할 수 있지만, 처음 토큰을 발급한 사용자가 정말 그 사용자인지는 확인할 수 없습니다. 🥲
Access Token은 그 자체로도 사용자 인증에 필요한 모든 정보를 가지고 있습니다. 그렇기 때문에, 토큰을 가지고 있는 시간이 늘어날 수록, 탈취되었을 때 피해 규모는 더욱 커지게 됩니다.
토큰이 탈취되었다 하더라도, 서버에서는 해당 토큰이 탈취된 토큰인지 알 수 없으며, 강제로 토큰을 만료시킬 수도 없습니다. 따라서 서버는 언제나 토큰이 탈취될 수 있다는 가정 하에, 피해를 최소화할 수 있는 방향으로 개발을 진행해야 합니다.
2) Refresh Token이 무엇인가요?
💡 **Refresh Token**은 사용자의 모든 인증 정보를 담고 있는 **Access Token**과는 달리, 특정 사용자가 **Access Token을 발급받기 위한 목적**으로만 사용됩니다.Refresh Token은 사용자의 인증 정보를 검증하는데 사용되며, 이를 서버에서 관리합니다.
서버는 Refresh Token을 디코딩하여 사용자의 정보를 확인하게 됩니다. 이 방식은 필요한 경우 서버에서 강제로 토큰을 만료시킬 수 있으며, 사용자의 인증 상태를 언제든지 서버에서 제어할 수 있다는 장점을 가지고 있습니다.
그렇다면 왜 직접 Access Token을 발급하지 않고, Refresh Token을 통해 Access Token을 발급하는 걸까요? → 이는 토큰이 탈취당한 경우에 대비하여 피해를 최소화하기 위함입니다.
일상생활에서 흔히 사용하는 OTP처럼, 사용자의 인증 정보는 짧은 시간동안만 사용되어야합니다. 주기적으로 토큰을 재발급함으로써, 토큰이 유출되더라도 그 피해가 오랜 시간 동안 지속되는 것이 아니라, 짧은 기간 동안만 사용 가능하도록 제한하여 피해를 최소화할 수 있게 될 것입니다.
서버에서는 언제나 토큰이 탈취될 수 있다는 사실을 항상 인지하고, 탈취를 막는 것이 어려운 상황이라면, 탈취된 토큰을 사용할 수 있는 기간을 줄임으로써 피해를 방지해야 합니다.
3) Refresh Token Project의 템플릿을 만들어봅니다!
yarn Package 설치
yarn init -y
yarn add express jsonwebtoken cookie-parser
Refresh Token Project - app.js
// app.js
import express from 'express';
import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';
const app = express();
const PORT = 3019;
// 비밀 키는 외부에 노출되면 안되겠죠? 그렇기 때문에, .env 파일을 이용해 비밀 키를 관리해야합니다.
const ACCESS_TOKEN_SECRET_KEY = HangHae99; // Access Token의 비밀 키를 정의합니다.
const REFRESH_TOKEN_SECRET_KEY = Sparta; // Refresh Token의 비밀 키를 정의합니다.
app.use(express.json());
app.use(cookieParser());
app.get('/', (req, res) => {
return res.status(200).send('Hello Token!');
});
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});
4) Refresh Token과 Access Token을 발급하는 API를 만들어봅니다!
// app.js
...
let tokenStorage = {}; // Refresh Token을 저장할 객체
/ Access Token, Refresh Token 발급 API /
app.post('/tokens', (req, res) => {
const { id } = req.body;
const accessToken = createAccessToken(id);
const refreshToken = createRefreshToken(id);
// Refresh Token을 가지고 해당 유저의 정보를 서버에 저장합니다.
tokenStorage[refreshToken] = {
id: id, // 사용자에게 전달받은 ID를 저장합니다.
ip: req.ip, // 사용자의 IP 정보를 저장합니다.
userAgent: req.headers['user-agent'], // 사용자의 User Agent 정보를 저장합니다.
};
res.cookie('accessToken', accessToken); // Access Token을 Cookie에 전달한다.
res.cookie('refreshToken', refreshToken); // Refresh Token을 Cookie에 전달한다.
return res
.status(200)
.json({ message: 'Token이 정상적으로 발급되었습니다.' });
});
// Access Token을 생성하는 함수
function createAccessToken(id) {
const accessToken = jwt.sign(
{ id: id }, // JWT 데이터
ACCESS_TOKEN_SECRET_KEY, // Access Token의 비밀 키
{ expiresIn: '10s' }, // Access Token이 10초 뒤에 만료되도록 설정합니다.
);
return accessToken;
}
// Refresh Token을 생성하는 함수
function createRefreshToken(id) {
const refreshToken = jwt.sign(
{ id: id }, // JWT 데이터
REFRESH_TOKEN_SECRET_KEY, // Refresh Token의 비밀 키
{ expiresIn: '7d' }, // Refresh Token이 7일 뒤에 만료되도록 설정합니다.
);
return refreshToken;
}
5) Access Token을 검증하는 API를 만들어봅니다!
// app. js
...
/ 엑세스 토큰 검증 API /
app.get('/tokens/validate', (req, res) => {
const accessToken = req.cookies.accessToken;
if (!accessToken) {
return res
.status(400)
.json({ errorMessage: 'Access Token이 존재하지 않습니다.' });
}
const payload = validateToken(accessToken, ACCESS_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Access Token이 유효하지 않습니다.' });
}
const { id } = payload;
return res.json({
message: ${id}의 Payload를 가진 Token이 성공적으로 인증되었습니다.,
});
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}
6) Refresh Token으로 Access Token을 재발급하는 API를 만들어봅니다!
// app. js
...
/ 리프레시 토큰 검증 API /
app.post('/tokens/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken)
return res
.status(400)
.json({ errorMessage: 'Refresh Token이 존재하지 않습니다.' });
const payload = validateToken(refreshToken, REFRESH_TOKEN_SECRET_KEY);
if (!payload) {
return res
.status(401)
.json({ errorMessage: 'Refresh Token이 유효하지 않습니다.' });
}
const userInfo = tokenStorage[refreshToken];
if (!userInfo)
return res.status(419).json({
errorMessage: 'Refresh Token의 정보가 서버에 존재하지 않습니다.',
});
const newAccessToken = createAccessToken(userInfo.id);
res.cookie('accessToken', newAccessToken);
return res.json({ message: 'Access Token을 새롭게 발급하였습니다.' });
});
// Token을 검증하고 Payload를 반환합니다.
function validateToken(token, secretKey) {
try {
const payload = jwt.verify(token, secretKey);
return payload;
} catch (error) {
return null;
}
}