Login Auth Token이 만료되었을 때 Refresh Token을 통하여 다시 Auth Token을 생성토록한다.
- Refresh Token을 서버에 저장한다. 다른 기기에서 로그인하면 기존 로그인 기기의 Refresh Token과 비교하여 틀리므로 여러 기기의 로그인을 방지한다.
- 서버에 여러개의 Refresh Token을 저장할 수 있다면 기기 제한을 할 수 있겠다. 또한 변조된 refresh token의 사용을 막을 수 있다.
User Entity 업데이트
libs/domain/src/lib/entities/user/user.entity.ts 에 refreshToken 컬럼을 추가한다.
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity('user_iot')
export class UserEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ unique: true, length: 50 })
username!: string;
@Column()
password!: string;
@Column({ length: 255 })
email!: string;
@Column({ name: 'first_name', length: 100 })
firstName!: string;
@Column({ name: 'last_name', length: 100 })
lastName!: string;
@Column({ default: 'GUEST' })
role!: string;
@CreateDateColumn({ name: 'created_at', select: false })
createdAt?: Date;
@CreateDateColumn({ name: 'updated_at', select: false })
updatedAt?: Date;
// refresh token 저장
@Column({
name: 'current_hashed_refresh_token',
nullable: true
})
currentHashedRefreshToken?: string;
}
User Service에 refreshToken 매칭
Cookie의 REFRESH_TOKEN이 서버에 저장된 값과 맞으면 해당 user정보를 반환하는 코드를 libs/domain/src/lib/entities/user/user.service.ts 에 추가한다.
// user.service.ts 일부
async getUserIfRefreshTokenMatches(refreshToken: string, id: number): Promise<User | undefined> {
const user = await this.findOneById(id);
const isRefreshTokenMatching = await bcryptCompare(
refreshToken,
user.currentHashedRefreshToken as string
);
if (isRefreshTokenMatching) {
return user;
}
return;
}
JWT Refresh Strategy와 Guard 추가
Guard에서 사용할 Refresh Strategy를 libs/domain/src/lib/auth/strategies/jwt-refresh.strategy.ts 파일 생성후 추가한다.
import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { UserService } from '@rnm/domain';
import { loadConfigJson } from '@rnm/shared';
import { TokenPayload, User } from '@rnm/model';
const config: any = loadConfigJson();
@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') {
constructor(
private readonly userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
return request?.cookies?.REFRESH_LOGIN_TOKEN;
}]),
secretOrKey: config?.AUTH?.REFRESH_SECRET,
passReqToCallback: true,
});
}
async validate(request: Request, payload: TokenPayload): Promise<User | undefined> {
const refreshToken = request.cookies?.REFRESH_LOGIN_TOKEN;
return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.id as number);
}
}
Refresh Guard도 libs/domain/src/lib/auth/guards/jwt-auth-refresh.guard.ts 파일 생성하고 추가한다.
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') { }
파일 추가후에는 항시 libs/domain/src/index.ts 안에 export를 해야 한다.
export * from './lib/constants/core.contant';
export * from './lib/entities/user/user.entity';
export * from './lib/entities/user/user.service';
export * from './lib/entities/entity.module';
export * from './lib/models/request.model';
export * from './lib/auth/auth.service';
export * from './lib/auth/auth.middleware';
export * from './lib/auth/auth.module';
export * from './lib/auth/guards/local-auth.guard';
export * from './lib/auth/guards/jwt-auth.guard';
export * from './lib/auth/guards/jwt-auth-refresh.guard'; // <== 요기
export * from './lib/auth/strategies/local.strategy';
export * from './lib/auth/strategies/jwt.strategy';
export * from './lib/auth/strategies/jwt-refresh.strategy'; // <== 요기
export * from './lib/service/gateway/api/service/gateway-api-app.service';
export * from './lib/service/dashboard/api/service/dashboard-api-app.service';
export * from './lib/configuration/api/service/configuration-api-app.service';
export * from './lib/service/back-office/api/service/backoffice-api-app.service';
RefreshToken과 AuthToken을 Cookie에 실어 보내기
두가지 Token을 response cookie에 실어 보내기위해 먼저 cookie 생성하는 코드를 libs/domain/src/lib/auth/auth.service.ts 에 추가한다.
// auth.service.ts 일부
getCookieWithJwtAccessToken(payload: TokenPayload, hasAuthorization = false) {
const token = this.jwtService.sign(payload, {
secret: config?.AUTH?.SECRET || 'iot_app',
expiresIn: config?.AUTH?.EXPIRED_ON || '1d'
});
if (hasAuthorization) {
return [`LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`, `Authorization=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`];
} else {
return [`LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`];
}
}
getCookieWithJwtRefreshToken(payload: TokenPayload) {
const token = this.jwtService.sign(payload, {
secret: config?.AUTH?.REFRESH_SECRET || 'io_app_refresh',
expiresIn: config?.AUTH?.REFRESH_EXPIRED_ON || '7d'
});
const cookie = `REFRESH_LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.REFRESH_EXPIRED_ON}`;
return {
cookie,
token
}
}
getCookiesForLogOut() {
return [
'LOGIN_TOKEN=; HttpOnly; Path=/; Max-Age=0',
'REFRESH_LOGIN_TOKEN=; HttpOnly; Path=/; Max-Age=0'
];
}
로apps/gateway/api/src/app/auth/auth.controller.ts 안에 하기 로직을 추가한다.
- 로그인 했을 때 해당 Cookie를 등록한다.
- 로그아웃할 때 해당 Cookie 내용을 삭제한다.
- Auth Token (Forbidden)오류 발생시 RefreshToken을 통해 Auth Token 재생성한다.
import { Controller, Get, HttpCode, Post, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common';
import { AuthService, JwtAuthGuard, LocalAuthGuard, JwtRefreshGuard, RequestWithUser, UserService } from '@rnm/domain';
import { TokenPayload } from '@rnm/model';
@Controller('api/auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService
) { }
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Req() req: RequestWithUser): Promise<any> {
const { user } = req;
if (user) {
const payload: TokenPayload = { username: user.username, sub: user.id, email: user.email, role: user.role };
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(payload);
const {
cookie: refreshTokenCookie,
token: refreshToken
} = this.authService.getCookieWithJwtRefreshToken(payload);
const loginUsernameCookie = this.authService.getCookieWithLoginUsername(payload);
await this.userService.setCurrentRefreshToken(refreshToken, user.id);
// 반드시 req.res로 쿠키를 설정
req.res.setHeader('Set-Cookie', [...accessTokenCookie, refreshTokenCookie, loginUsernameCookie]);
return {
payload,
accessTokenCookie,
refreshTokenCookie
};
} else {
throw new UnauthorizedException({
error: 'User does not exist'
});
}
}
@UseGuards(JwtAuthGuard)
@Post('logout')
@HttpCode(200)
async logout(@Req() req: RequestWithUser, @Res() res): Promise<any> {
await this.userService.removeRefreshToken(req.user.id);
res.setHeader('Set-Cookie', this.authService.getCookiesForLogOut());
}
@UseGuards(JwtAuthGuard)
@Get()
authenticate(@Req() req: RequestWithUser) {
const user = req.user;
return user;
}
// Refresh Guard를 적용한다.
@UseGuards(JwtRefreshGuard)
@Get('refresh')
refresh(@Req() request: RequestWithUser) {
const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(request.user);
request.res.setHeader('Set-Cookie', accessTokenCookie);
return request.user;
}
}
테스트하기
Postman으로 테스트를 하면 accessTokenCookie가 나온다.
accessTokenCookie를 복사하여 다른 명령 전송시에 Headers에 Cookie를 등록하여 사용한다.
소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-7
<참조>
- Refresh Token 만들기
소스: https://github.com/mwanago/nestjs-typescript
문서: https://wanago.io/2020/09/21/api-nestjs-refresh-tokens-jwt/
'React > Architecture' 카테고리의 다른 글
[MS-9] Login 화면 개발 (0) | 2021.09.30 |
---|---|
[MS-8] NestJS Auth/Role 기능 Gateway에 추가하기 (0) | 2021.09.30 |
[MS-6] NestJS의 JWT 기반 Auth Server 환경구축 (0) | 2021.09.27 |
[MS-5] NestJS에 TypeORM 사용하기 (0) | 2021.09.27 |
[MS-4] Gateway에 Prisma ORM 사용하기 (0) | 2021.09.24 |