๊ฐ์
์ ์ ๊ฐ ๋ก๊ทธ์ธ ํ์ ๋, access token
๋ง์ ์ด์ฉํด์ ๊ถํ์ ์ธ์ฆํ๊ณ , ๋ง๋ฃ๊ฐ ๋๋ฉด ๋ค์ ๋ก๊ทธ์ธํ๋ ๊ณผ์ ์ ๊ฑฐ์น๋ค๋ฉด ๋ฌด์จ ์ผ์ด ์ผ์ด๋ ๊น?
๋ง์ฝ ๋ง๋ฃ์๊ฐ์ด ์งง๋ค๋ฉด, ์ ์ ๋ ์ฌ๋ก๊ทธ์ธํด์ผ ํ๋ ์ํฉ์ด ๊ณ์ ์ฌ ์ ๋ฐ์ ์๋ค. ๊ทธ๋ฌ๋ฉด ์ ์ ์
์ฅ์์ ๋ถํธํ ์ ์๋ค.
๋ง์ฝ access token์ด ๊ณต๊ฒฉ์์๊ฒ ํ์ทจ๋๋ค๋ฉด, ๊ณต๊ฒฉ์๋ ๋ก๊ทธ์ธ ์์ด๋ ๋ชจ๋ ์๋น์ค์ ์ ๊ทผ ๊ถํ์ ์ป์ ์ ์๋ค. ์ด๊ฒ์ ์๋นํ ๋ฌธ์ ์ด๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ refresh-token
์ ํ๋๋ ์์ฑํ๋ฉด ๋๋ค. refresh-token
์ access-token
์ ์ฌ๋ฐ๊ธํ ์ ์๋ ํ ํฐ์ด๋ค.
๊ทธ๋ผ, refresh-token
์ด ํ์ทจ๋๋ค๋ฉด ์ด๋ป๊ฒ ํด์ผํ ๊น?
refresh-token
์ stateless
์ด๊ธฐ ๋๋ฌธ์ ๊ฐ์ฅ ๊ฐ๋จํ๊ฒ ์๊ฐํด๋ณผ ์ ์๋ ๋ฐฉ๋ฒ์ผ๋ก๋, secret ๊ฐ์ ๋ณ๊ฒฝํด์ ๋ชจ๋ ํ ํฐ์ ๋ฌดํจํ ํ ์ ์๋ค. ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ ๋ชจ๋ ์ ์ ์๊ฒ ๋ค์ ๋ก๊ทธ์ธํ๊ฒ ํ๋ ๋์ฐธ์ฌ๊ฐ ์ผ์ด๋ ์ ์๊ธฐ ๋๋ฌธ์ ํ๋ช
ํ ๋ฐฉ๋ฒ์ ์๋๋ค.
ํ๋์ ํด๊ฒฐ์ฑ
์ refresh-token
์ db์ ์ ์ฅํด์ ๊ด๋ฆฌํ๊ณ ๋ก๊ทธ์ธํ ๋๋ง๋ค ๋ณ๊ฒฝํ๋ฉด ๋๋ค. ์ด๋ฐ ๋ฐฉ๋ฒ์ ์ฑํํ๋ฉด, ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ ํด๊ฒฐํ ์ ์๋ค.
์๋ฅผ ๋ค์ด, ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ํ๋์ ๊ณ์ ์ ๊ณต์ ํ๋ค๊ณ ํ์ ๋, ๋ชจ๋ ์ฌ๋์ด ๊ฐ์๊ธฐ ํด๋น ๊ณ์ ์ผ๋ก ๋์์ ์ ์ํด ์ดํ๋ฆฌ์ผ์ด์
์ ์ด์ฉํ๋ค๋ฉด, ๋น์ง๋์ค์ ์์ข์ ์ํฅ์ ๋ผ์น ์ ์๋ค. refresh-token
์ ๋ก๊ทธ์ธํ ๋๋ง๋ค ๋ณ๊ฒฝํ๋ค๋ฉด, ์ด์ ์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ ์ ํจํ์ง ์์ refresh-token
์ ๊ฐ๊ฒ ๋ ๊ฒ์ด๊ณ , ์ฌ๋ก๊ทธ์ธ์ ์ ๋ํ ๊ฒ์ด๋ค.
๊ทธ๋ผ refresh-token
์ด db์์ ์ ์ถ๋๋ ๋ฌธ์ ๋ ์ด๋ป๊ฒ ํด๊ฒฐํด์ผ ํ ๊น?
password ์ฒ๋ผ hash
ํด์ ์ ์ฅํ๋ฉด ๋๋ค.
๊ตฌํ
configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
| // .env
JWT_ACCESS_TOKEN_SECRET=access-secret
JWT_ACCESS_TOKEN_EXPIRATION_TIME=3600
JWT_REFRESH_TOKEN_SECRET=refresh-secret
JWT_REFRESH_TOKEN_EXPIRATION_TIME=3600
// app.module.ts
....
JWT_ACCESS_TOKEN_SECRET: Joi.string().required(),
JWT_ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().required(),
JWT_REFRESH_TOKEN_SECRET: Joi.string().required(),
JWT_REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().required(),
....
|
์์ ๊ฐ์ด access token
/ refresh token
์ ๋ํด ๋ณ์๋ฅผ ์์ฑํด์ฃผ๊ณ , ํ์ํ๋ค๋ฉด ์ด๋ฅผ validation๊น์ง ํ๋ ๋ก์ง์ ๊ตฌํํด์ฃผ๋ฉด ๋๋ค.
/login
access token ๋ฐ๊ธ
1
2
3
4
5
6
7
| async getJwtAccessToken(userId: number){
const payload : TokenPayload = {userId};
const token = await jwtService.sign(payload, {
secret : configService.get("JWT_ACCESS_TOKEN_SECRET"),
expiresIn : configService.get("JWT_ACCESS_TOKEN_EXPIRATION_TIME")
});
}
|
refresh token ๋ฐ๊ธ
1
2
3
4
5
6
7
| async getJwtRefreshToken(userId: number){
const payload : TokenPayload = {userId};
const token = await jwtService.sign(payload, {
secret : configService.get("JWT_REFRESH_TOKEN_SECRET"),
expiresIn : configService.get("JWT_REFRESH_TOKEN_EXPIRATION_TIME")
});
}
|
signOptions
ํ์ฌ ์ ๋ ๋ก์ง์์ sign() ๋ฉ์๋๋ฅผ ์คํํ ๋ signOption
์ secret
๊ฐ์ด ๋ค์ด๊ฐ์๋ ๊ฑธ ๋ณผ ์ ์๋ค. ์ด๋ ๊ฒ ํจ์ผ๋ก์จ ๋ค๋ฅธ secret๊ฐ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ํ ํฐ์ ์์ฑํ๋๋ฐ ๊ด๋ จ ๊ธฐ๋ฅ์ @nest/jwt@^7.1.0
์์๋ถํฐ ๊ฐ๋ฅํ๋ค.
user db์ ํ์ฌ ํ ํฐ์ hash ํ๊ณ ์ ์ฅ
๋ค์๊ณผ ๊ฐ์ด ํ์ฌ ๋ค์ด์จ refreshToken์ ์๋ก ์ํธํํ๊ณ ์ ์ฅํ๋ฏ๋ก, refresh token์ด ํ์ทจ๋์์ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ๋ฅผ ์ด๋์ ๋ ํด๊ฒฐํ ์ ์๋ค.
1
2
3
4
5
6
7
| // userService.ts
async setCurrentRefreshToken(refreshToken: string, userId: number) {
const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10);
await this.usersRepository.update(userId, {
currentHashedRefreshToken
});
}
|
cookie์ access token, refresh token์ ๋ฃ๊ณ response
1
2
3
4
5
6
7
8
9
10
11
12
13
| @HttpCode(200)
@UseGuards(LocalAuthenticationGuard)
@Post('log-in')
async logIn(@Req() request: RequestWithUser) {
const {user} = request;
const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(user.id);
const refreshTokenCookie = this.authenticationService.getCookieWithJwtRefreshToken(user.id);
await this.usersService.setCurrentRefreshToken(refreshToken, user.id);
request.res.setHeader('Set-Cookie', [accessTokenCookie, refreshTokenCookie]);
return user;
}
|
/refresh
refresh token validation
ํ์ฌ refresh token์ด validate ํ๋์ง ํ์ธํ๊ธฐ ์ํด์, ์๋ก์ด strategy๋ฅผ ์์ฑํด์ผ ํ๋ค. ์ด strategy๋ฅผ JwtRefreshStrategy
๋ผ ํ๋ค๋ฉด, ์ด strategy์์๋ user service์ ํ์ฌ ๋ค์ด์จ token์ด ์ ์ db์ ์ ์ฅ๋ ํ ํฐ๊ณผ ๋๊ฐ์ ์ง ๋น๊ตํ๋ค. ์ด ๊ณผ์ ์ ํตํด์, token์ด ๊ฐ์ทจ๋๊ฑฐ๋ ๋์ผํ ์ฌ์ฉ์๊ฐ ๋์์ ์ ์ํ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| //
@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(
Strategy,
'jwt-refresh-token'
) {
constructor(
private readonly configService: ConfigService,
private readonly userService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
return request?.cookies?.Refresh;
}]),
secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'),
passReqToCallback: true,
});
}
async validate(request: Request, payload: TokenPayload) {
const refreshToken = request.cookies?.Refresh;
return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.userId);
}
}
|
passReqToCallback
์ ํตํด validate
๋ฉ์๋์์ cookie์ ์ ๊ทผํ ์ ์๊ฒ ๋์๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
| // user service
async getUserIfRefreshTokenMatches(refreshToken: string, userId: number) {
const user = await this.getById(userId);
const isRefreshTokenMatching = await bcrypt.compare(
refreshToken,
user.currentHashedRefreshToken
);
if (isRefreshTokenMatching) {
return user;
}
}
|
1
2
3
4
5
6
7
8
| @UseGuards(AuthGuard('jwt-refresh-token'))
@Get('refresh')
refresh(@Req() request: RequestWithUser) {
const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(request.user.id);
request.res.setHeader('Set-Cookie', accessTokenCookie);
return request.user;
}
|
/logout
๋ก๊ทธ์์์ ์ ์ db์ ์ ์ฅ๋ refresh token์ ์์ ๊ณ , response๋ก token์ ๋ฌดํจํํ๋ ์ ๋ณด๋ฅผ ์ ์ฅํด ์๋ตํ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // authentication service
public getCookiesForLogOut() {
return [
'Authentication=; HttpOnly; Path=/; Max-Age=0',
'Refresh=; HttpOnly; Path=/; Max-Age=0'
];
}
// user service
async removeRefreshToken(userId: number) {
return this.usersRepository.update(userId, {
currentHashedRefreshToken: null
});
}
// authentication controller
@UseGuards(JwtAuthenticationGuard)
@Post('log-out')
@HttpCode(200)
async logOut(@Req() request: RequestWithUser) {
await this.usersService.removeRefreshToken(request.user.id);
request.res.setHeader('Set-Cookie', this.authenticationService.getCookiesForLogOut());
}
|