-
Notifications
You must be signed in to change notification settings - Fork 4
refresh token 보완하기
-
현재는 access Token, refresh Token이 같은 키를 이용하여 생성되며, jwtService만이 verify하여 검증하는 구조를 가지고 있어 refesh token의 의미가 딱히 없는 상태이다
-
refresh Token을 DB에 저장해야하는 이유는 다음과 같다
- jwt secret key를 얻는 다면 access, refresh Token을 둘 다 위조해낼 수 있는 상황이다. access Token은 1시간 정도의 유효기간을 가지고 있어 보안에 크게 위협이 되지 않지만, 유효기간이 긴 refresh Token과 같은 경우에는 문제가 된다. 따라서 사용자의 유효한 refresh Token을 사용자 별로 하나로 특정해 놓는 것이 보안상 좋다.
- jwt는 Stateless 원칙으로, 인증 후 토큰 폐기가 어렵다. 하지만 refresh Token을 DB에서 직접 관리한다면 서버 다운 등의 이유로 사용자들의 로그아웃을 직접 실행해야 할 경우, Token을 DB에서 폐기함으로써 특정 토큰을 관리할 수 있다.
-
하지만 이렇게 구현할 경우, 사용자별로 refresh token은 하나씩만 특정되기 때문에 여러 브라우저에서 동시에 로그인을 한 상태로 진행하지 못한다는 단점이 있다.
→ 하지만 문서편집 및 생산성 도구를 여러 브라우저에서 사용하는 경우는 드물다는 판단하에 보안성을 발전시키기 위해 Refresh Token을 DB에서 관리하기로 했다.
1. user 엔티티에 refresh Token column 추가 (nullable, string type) - user.entity.ts
@Column({ nullable: true })
refreshToken: string;
2. 사용자가 로그인 후 refresh Token, access Token을 쿠키에 보관하면서 refresh Token은 따로 DB에도 저장 - token.service.ts → 토큰 서비스에서 유저 레포지토리에 접근, 특정 사용자의 토큰을 업데이트하면서 DB에도 보관하게 한다
private async updateRefreshToken(id: number, refreshToken: string) {
// 유저를 찾는다.
const user = await this.userRepository.findOneBy({ id });
// 유저가 없으면 오류
if (!user) {
throw new UserNotFoundException();
}
// 유저의 현재 REFRESH TOKEN 갱신
user.refreshToken = refreshToken;
await this.userRepository.save(user);
}
async deleteRefreshToken(id: number) {
// 유저를 찾는다.
const user = await this.userRepository.findOneBy({ id });
// 유저가 없으면 오류
if (!user) {
throw new UserNotFoundException();
}
// 유저의 현재 REFRESH TOKEN 삭제
user.refreshToken = null;
await this.userRepository.save(user);
}
(이 과정에서 유저 서비스에 해당 함수들을 배치시켰다가, 서비스끼리의 상호참조 경우가 만들어질 것 같아 토큰 서비스에 해당 함수들을 넣어, 유저 레포지토리를 직접 접근하도록 했다)
3. refresh Token 생성 방법도 보안성을 높임. 토큰 아이디를 uuid로 payload로 추가
async generateRefreshToken(userId: number): Promise<string> {
// 보안성을 높이기 위해 랜덤한 tokenId인 jti를 생성한다
const payload = { sub: userId, jti: uuidv4() };
const refreshToken = this.jwtService.sign(payload, {
expiresIn: FIVE_MONTHS,
});
-
하지만 외부에서
(1) 토큰 암호화에 사용되는 비밀키를 알아내고
(2) 해당 암호화가 진행된 시간을 유연히 알아낸다면
똑같은 Refresh Key도 만들어낼 수 있지 않은가?
-
과한 걱정일 수 있지만 혹시나 모를 상황을 대비하여 token의 id를 uuid로 랜덤 생성하여 payload에 넣는 방식으로, 토큰의 무작위성을 더욱더 높였다. 이제 DB에 직접 접근하지 않는 이상, 외부에서 특정 사용자의 Refresh Token을 알아낼 가능성은 0에 수렴한다!!
4. access Token이 만료되어 다시 발급받기 위해 refresh Token을 검증받을 때, refresh Token을 verify 한 후 DB에 보관된 refresh Token과 일치하는지도 비교하게 함. 다른 경우 invalid token error가 생성됨
- token.service.ts → 토큰 서비스에서 유저 레포지토리에 접근, 특정 사용자의 토큰과 현재 클라이언트가 보유한 refresh token을 비교할 수 있다
private async compareStoredRefreshToken(
id: number,
refreshToken: string,
): Promise<boolean> {
// 유저를 찾는다.
const user = await this.userRepository.findOneBy({ id });
// 유저가 없으면 오류
if (!user) {
throw new UserNotFoundException();
}
// DB에 있는 값과 일치하는지 비교한다
return user.refreshToken === refreshToken;
}
- refresh Token을 검증하는 과정 보충 → 토큰 verify 후, 위의 함수를 이용하여 내부 DB의 토큰값과 비교, 검증이 성공했더라도 사용자가 로그인을 성공한 후 생성했던 refresh token의 데이터와 다르면 유효하지 않은 토큰이라는 에러를 뱉어낸다.
async refreshAccessToken(refreshToken: string): Promise<string> {
// refreshToken을 검증한다
// refreshToken 1차 검증
const decoded = this.jwtService.verify(refreshToken, {
secret: process.env.JWT_SECRET,
});
// 검증된 토큰에서 사용자 ID 추출
const userId = decoded.sub;
// refreshToken 2차 검증
// DB에 저장된 refreshToken과 비교
const isValid = await this.compareStoredRefreshToken(userId, refreshToken);
if (!isValid) {
throw new InvalidTokenException();
}
5. 사용자가 로그아웃 할 시 쿠키가 비우면서 db에 있는 해당 사용자의 refresh Token도 null로 reset한다
- token.service.ts에 함수 추가, 로그아웃 api에서 해당 함수 사용
async deleteRefreshToken(id: number) {
// 유저를 찾는다.
const user = await this.userRepository.findOneBy({ id });
// 유저가 없으면 오류
if (!user) {
throw new UserNotFoundException();
}
// 유저의 현재 REFRESH TOKEN 삭제
user.refreshToken = null;
await this.userRepository.save(user);
}
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
Novel이란?
Novel 스타일링 문제
에디터 저장 및 고려 사항들
📠 실시간 협업, 통신
Yorkie와 Novel editor 연동
YJS, Websocket, React-Flow
YJS, Socket.io
WebSocket과 Socket.io에 대해 간단히 알아보기
YJS 가이드 근데 이제 Socket.io를 곁들인
🏗️ 인프라와 CI/CD
NCloud CI CD 구축
BE 개발 스택과 기술적 고민
private key로 원격 서버 접근
nCloud 서버, VPC 만들고 설정
monorepo로 변경
⌛ 캐시, 최적화
rabbit mq 사용법
🔑 인증, 인가, 보안
passport로 oAuth 로그인 회원가입 구현
FE 로그인 기능 구현
JWT로 인증 인가 구현
JWT 쿠키로 사용하기
refresh token 보완하기
🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략
🌤️ 데일리 스크럼
📑 회의록
1️⃣ 1주차
킥오프(10/25)
2일차(10/29)
3일차(10/30)
4일차(10/31)
2️⃣ 2주차
8일차(11/04)
9일차(11/05)
11일차(11/07)
13일차(11/09)
3️⃣ 3주차
3주차 주간계획(11/11)
16일차(11/12)
18일차(11/14)
4️⃣ 4주차
4주차 주간계획(11/18)
23일차(11/19)
24일차(11/20)
25일차(11/21)
5️⃣ 5주차
5주차 주간계획(11/25)
29일차(11/25)
32일차(11/28)
34일차(11/30)
6️⃣ 6주차
6주차 주간계획(12/2)
37일차(12/3)