기존 엑세스 토큰이 만료되어 새로운 엑세스 토큰을 생성하기 시작하는 부분의 코드이다.
db에 저장되어 있는 리프레시 토큰을 accountid를 통해 불러오기 위해, 만료된 엑세스 토큰을 기반으로 accountid를 불러와야 했다.
if (error.name === 'TokenExpiredError') {
// 클라이언트가 전달한 엑세스 토큰이 만료
// 다시한번 엑세스 토큰 추출
const { authorization } = req.headers;
const [tokenType, token] = authorization.split(' ');
// 리프레시 토큰을 데이터베이스에서 조회
// accountid가 null or undefined일때 오류 대신 undefined 반환
const accountId = jwt.decode(token)?.accountid;
const storedToken = await prisma.refreshToken.findFirst({
where: { accountid: accountId },
});
기존 내가 작성했던 코드, 이전 프로젝트를 참고하여 할수있는걸 해봤지만 성공하지 못했다. 엑세스 토큰까지 토큰 db에 넣어 accountid과 연계한다면(현재는 리프레시토큰만 accountid와 연계)될 수도 있다. 하지만 5분 마다 만료되는 엑세스 토큰을 db에 생성될때마다 입력하도록 하는 게 적절한지는 모르겠다.
때문에 현재 상태에서 해결책을 알기위해 뤼튼을 통해 알게된 decode를 통해 작성해보았다.
jwt는 헤더, 페이로드, 서명으로 구성되어 있다.
페이로드는 사용자 정보와 관련된 클레임을 포함한다. (클레임 종류는 일단 생략)
const accountId = jwt.decode(token)?.accountid;를 통해 만료된 토큰에서 정보를 가져올 수 있어 데이터베이스 조회가 필요하지는 않다. 이렇게 코드를 작성하면 만료된 토큰에서도 accountid를 불러올 수 있지만 이 방법은 토큰의 서명을 검증하지 않기 때문에 보안상 문제가 있을 수 있다. 공격자가 만료된 토큰을 재사용할 수 있는 가능성이 있다한다.
다른 방법으로는 로그인 성공 시 사용자 정보를 담은 커스텀헤더(표준 http 헤더 외에 추가적인 정보를 전달하기 위해 사용, "X-" 접두사를 붙여서 구분)를 사용하는 것.
router.post('/sign-in', async (req, res) => {
const { email, password } = req.body;
const accountData = await prisma.accounts.findFirst({ where: { email } });
if (!accountData)
return res.status(401).json({ message: '존재하지 않는 email입니다.' });
// 입력받은 사용자의 비밀번호와 데이터베이스에 저장된 비밀번호를 비교합니다.
else if (!(await bcrypt.compare(password, accountData.password)))
return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });
// 로그인에 성공하면, 사용자의 userId를 바탕으로 토큰을 생성합니다.
const accesstoken = jwt.sign(
{
accountid: accountData.accountid,
},
// JWT를 서명하는 데 사용되는 비밀 키
// 서버가 비밀 키를 사용하여 토큰 변조 여부를 알 수 있다
process.env.SERVER_ACCESS_KEY,
{ expiresIn: '1m' }
);
// 기존 리프레시 토큰 삭제
await prisma.refreshToken.deleteMany({
where: { accountid: accountData.accountid },
});
// 리프레시 토큰 생성
const refreshtoken = jwt.sign(
{
accountid: accountData.accountid,
},
process.env.SERVER_REFRESH_KEY, // 리프레시 토큰을 위한 비밀 키
{ expiresIn: '3m' } // 예: 7일 동안 유효
);
// 리프레시 토큰을 토큰테이블에 저장
// 해당 리프레시 토큰과 연결된 account_id도 함께 저장
await prisma.refreshToken.create({
data: {
token: refreshtoken,
accountid: accountData.accountid,
},
});
res.setHeader('Authorization', `Bearer ${accesstoken}`);
// 커스텀헤더를 통해 로그인 성공한 계정의 이메일(id)을 클라이언트에게 전달
res.setHeader('x-info', accountData.email);
return res.status(200).json({ message: '로그인 성공', accesstoken });
});
export default router;
로그인 성공 후 클라이언트에게 응답을 보낼 때, x-info라는 이름의 커스텀 헤더를 추가하고 그 값으로 accountData 객체에 저장된 이메일 주소를 설정.
이렇게 설정된 헤더는 클라이언트가 응답을 받을 때 확인할 수 있으며, 추가적인 정보를 전달하는 데 사용된다.
...
} catch (error) {
if (error.name === 'TokenExpiredError') {
// 클라이언트가 전달한 엑세스 토큰이 만료된 경우
const { 'x-info': email } = req.headers;
const getaccountid = await prisma.accounts.findFirst({
where: { email },
});
try {
// 리프레시 토큰을 데이터베이스에서 조회
const storedToken = await prisma.refreshToken.findFirst({
where: { accountid: getaccountid.accountid },
});
...
이렇게 불러와 엑세스 토큰이 만료된 사용자의 정보를 얻고, 그를 사용하여 리프레시 토큰을 조회한다.
두가지 방법 중 어떤 방법이 보안상, 리소스상 이점이 있는지는 모호해서 일단 후자를 적용한 후 판단이 서거나 더 좋은 방법이 있다면 적용하는 방향으로 진행할 예정.
일단은 인증 과정이 의도한대로 잘 적용된 것을 확인하였다.
유효기간이 짧은 엑세스 토큰이 만료되었을때 리프레시 토큰이 유효하다면 아래의 인증 미들웨어의 검증을 거쳐 새로운 엑세스 토큰을 생성해준다.
// src > middlewares > auth.js
import jwt from 'jsonwebtoken';
import { prisma } from '../utils/prisma/index.js';
import dotenv from 'dotenv';
dotenv.config();
export default async function authM(req, res, next) {
try {
const { authorization } = req.headers;
console.log(authorization); // 추출 확인용
// Authorization 헤더가 없을 경우
if (!authorization) throw new Error('토큰이 존재하지 않습니다.');
const [tokenType, token] = authorization.split(' ');
if (tokenType !== 'Bearer')
throw new Error('토큰 타입이 일치하지 않습니다.');
// 엑세스 토큰 검증
const decodedToken = jwt.verify(token, process.env.SERVER_ACCESS_KEY);
const accountsData = decodedToken.accountid;
// 데이터베이스에서 사용자 정보 조회
const accounts = await prisma.accounts.findFirst({
where: { accountid: accountsData },
});
// 사용자가 존재하지 않을 경우
if (!accounts) {
throw new Error('토큰 사용자가 존재하지 않습니다.');
}
// req.accounts 사용자 정보를 저장합니다.
req.account = accounts;
res.setHeader('Authorization', `Bearer ${decodedToken}`);
// console.log('인증된 정보 :', req.account); // 확인용
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
// 클라이언트가 전달한 엑세스 토큰이 만료된 경우
const { 'x-info': email } = req.headers;
const getaccountid = await prisma.accounts.findFirst({
where: { email },
});
try {
// 리프레시 토큰을 데이터베이스에서 조회
const storedToken = await prisma.refreshToken.findFirst({
where: { accountid: getaccountid.accountid },
});
// 리프레시 토큰이 존재하지 않을 경우
if (!storedToken) {
return res.status(401).json({ message: '로그인이 필요합니다.' });
}
// 리프레시 토큰 검증
const decodedRefreshToken = jwt.verify(
storedToken.token,
process.env.SERVER_REFRESH_KEY
);
// 검증된 리프레시토큰과 연결된 accountid를 바탕으로 새로운 엑세스 토큰 생성
const newAccessToken = jwt.sign(
{ accountid: decodedRefreshToken.account_id },
process.env.SERVER_ACCESS_KEY,
{ expiresIn: '1m' }
);
// 데이터베이스에서 계정 정보 조회
const newAccounts = await prisma.accounts.findFirst({
// 검증된 리프레시 토큰과 연계된 accounid로 계정정보 조회
where: { accountid: decodedRefreshToken.account_id },
});
//조회한 계졍정보 할당
req.account = newAccounts;
// console.log('인증된 정보 :', req.account); // 확인용
res.setHeader('Authorization', `Bearer ${newAccessToken}`);
next();
} catch (refreshError) {
// 리프레시 토큰이 만료된 경우
if (refreshError.name === 'TokenExpiredError') {
return res.status(401).json({
message: '리프레시 토큰이 만료되었습니다. 다시 로그인하세요.',
});
}
return res
.status(401)
.json({ message: '리프레시 토큰 검증 중 오류가 발생했습니다.' });
}
} else if (error.name === 'JsonWebTokenError') {
// 예외
return res.status(401).json({ message: '토큰이 조작되었습니다.' });
} else {
return res.status(401).json({ message: '비정상적인 요청입니다.' });
}
}
}
이후 로그인에 성공하여 엑세스 토큰, 리프레시 토큰이 생성될 때, 리프레시 토큰이 db에 쌓여 불러올때 문제가 되지 않도록 이전 리프레시 토큰은 삭제하는 로직을 추가하였다.
추가적인 작업으로는 회원가입 시 비밀번호를 한번더 입력하여 비밀번호를 확인하는 과정,
팀 조회 등이 있다.
'TIL' 카테고리의 다른 글
내일배움캠프 6주차 목요일 TIL (0) | 2024.12.05 |
---|---|
내일배움캠프 6주차 수요일 TIL (0) | 2024.12.04 |
내일배움캠프 6주차 월요일 TIL (1) | 2024.12.02 |
내일배움캠프 5주차 금요일 (1) | 2024.11.29 |
내일배움캠프 5주차 수요일 TIL (0) | 2024.11.27 |