Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Server/auth] accessToken 재발행 로직 구현, 로그아웃 로직 구현 #87

Merged
merged 10 commits into from
Nov 21, 2022
38 changes: 28 additions & 10 deletions server/apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,54 @@ import { SignInDto, SignUpDto } from './dto';
import { responseForm } from '@utils/responseForm';
import { Response } from 'express';
import { getUserBasicInfo } from '@user/dto/user-basic-info.dto';
import { JwtAuthGuard } from '@api/src/auth/guard';
import { JwtAccessGuard, JwtRefreshGuard } from '@api/src/auth/guard';

@Controller('api/user/auth')
export class AuthController {
constructor(private authService: AuthService) {}

@Post('signup')
@Post('signup') // 회원가입
async signUp(@Body() signUpDto: SignUpDto) {
await this.authService.signUp(signUpDto);
return responseForm(200, { message: '회원가입 성공!' });
}

@Post('signin')
@Post('signin') // 로그인
async signIn(@Body() signInDto: SignInDto, @Res({ passthrough: true }) res: Response) {
const result = await this.authService.signIn(signInDto);
const { refreshToken, accessToken } = await this.authService.signIn(signInDto);

// refreshToken 쿠키에 구워줌
res.cookie('refreshToken', result.refreshToken, {
path: '/refresh',
res.cookie('refreshToken', refreshToken, {
path: '/api/user/auth/refresh',
httpOnly: true,
secure: false,
maxAge: 3600000, // 1시간 만료
maxAge: 360000000, // 100시간 만료
});

return responseForm(200, { message: '로그인 성공!', accessToken: result.accessToken });
return responseForm(200, { message: '로그인 성공!', accessToken });
}

@Get('me')
@UseGuards(JwtAuthGuard)
@Post('refresh') // AccessToken 재발행
@UseGuards(JwtRefreshGuard)
async refresh(@Req() req: any) {
const accessToken = req.user;
if (accessToken === null) {
return responseForm(401, { message: '로그인 필요' });
}
return responseForm(200, { message: 'accessToken 재발행 성공!', accessToken });
}

@Get('me') // 자신의 유저 정보 제공
@UseGuards(JwtAccessGuard)
async getMyInfo(@Req() req: any) {
return getUserBasicInfo(req.user);
}

@Post('signout')
@UseGuards(JwtAccessGuard)
async singOut(@Req() req: any, @Res({ passthrough: true }) res: Response) {
await this.authService.signOut(req.user._id); // DB에서 refreshToken 제거
res.cookie('refreshToken', 'expired', { maxAge: -1 }); // client에서 refreshToken 제거
return responseForm(200, { message: '로그아웃 성공!' });
}
}
5 changes: 3 additions & 2 deletions server/apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from '@schemas/user.schema';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from '@api/src/auth/strategy';
import { JwtAccessStrategy, JwtRefreshStrategy } from '@api/src/auth/strategy';
import { SignToken } from '@api/src/auth/helper/signToken';

@Module({
imports: [
Expand All @@ -15,6 +16,6 @@ import { JwtStrategy } from '@api/src/auth/strategy';
JwtModule.register({}),
],
controllers: [AuthController],
providers: [AuthService, UserRepository, JwtStrategy],
providers: [AuthService, UserRepository, JwtAccessStrategy, JwtRefreshStrategy, SignToken],
})
export class AuthModule {}
42 changes: 10 additions & 32 deletions server/apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
import { SignInDto, SignUpDto } from './dto';
import * as argon from 'argon2';
import { UserRepository } from '@repository/user.repository';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { SignToken } from '@api/src/auth/helper/signToken';

@Injectable()
export class AuthService {
constructor(
private jwt: JwtService,
private userRepository: UserRepository,
private config: ConfigService,
) {}
constructor(private userRepository: UserRepository, private signToken: SignToken) {}
async signUp(signUpDto: SignUpDto) {
// 비밀번호 암호화
const hash = await argon.hash(signUpDto.password);
Expand All @@ -38,37 +33,20 @@ export class AuthService {
throw new ForbiddenException('비밀번호가 일치하지 않습니다.');
}
// accessToken, refreshToken 발행
const { accessToken, refreshToken } = await this.signToken(user._id, user.nickname);
const accessToken = await this.signToken.signAccessToken(user._id, user.nickname);
const refreshToken = await this.signToken.signRefreshToken(user._id);

// DB에 refreshToken 업데이트
this.userRepository.updateOne({ _id: user._id }, { refreshToken });

return { accessToken, refreshToken };
}

async signToken(
_id: number,
nickname: string,
): Promise<{ accessToken: string; refreshToken: string }> {
const accessTokenPayload = {
_id,
nickname,
};

const refreshTokenPayload = {
_id,
};

const accessToken = await this.jwt.signAsync(accessTokenPayload, {
expiresIn: '15m',
secret: this.config.get('JWT_SECRET'),
});

const refreshToken = await this.jwt.signAsync(refreshTokenPayload, {
expiresIn: '1hr',
secret: this.config.get('JWT_SECRET'),
});

return { accessToken, refreshToken };
async signOut(userId: string) {
try {
await this.userRepository.updateOne({ _id: userId }, { refreshToken: '' });
} catch (error) {
throw new ForbiddenException('잘못된 접근입니다.');
}
}
}
3 changes: 2 additions & 1 deletion server/apps/api/src/auth/guard/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './auth.guard';
export * from './jwt-access.guard';
export * from './jwt-refresh.guard';
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { AuthGuard } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
export class JwtAccessGuard extends AuthGuard('jwt-access-token') {}
5 changes: 5 additions & 0 deletions server/apps/api/src/auth/guard/jwt-refresh.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AuthGuard } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}
30 changes: 30 additions & 0 deletions server/apps/api/src/auth/helper/signToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class SignToken {
constructor(private jwt: JwtService, private config: ConfigService) {}
async signAccessToken(_id: number, nickname: string): Promise<string> {
const accessTokenPayload = {
_id,
nickname,
};
const accessToken = await this.jwt.signAsync(accessTokenPayload, {
expiresIn: '15m',
secret: this.config.get('JWT_SECRET'),
});
return accessToken;
}

async signRefreshToken(_id: number): Promise<string> {
const refreshTokenPayload = {
_id,
};
const refreshToken = await this.jwt.signAsync(refreshTokenPayload, {
expiresIn: '100hr',
secret: this.config.get('JWT_SECRET'),
});
return refreshToken;
}
}
3 changes: 2 additions & 1 deletion server/apps/api/src/auth/strategy/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './auth.strategy';
export * from './jwt-access.strategy';
export * from './jwt-refresh.strategy';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config';
import { ForbiddenException, Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt-access-token') {
constructor(config: ConfigService, private userRepository: UserRepository) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
Expand Down
41 changes: 41 additions & 0 deletions server/apps/api/src/auth/strategy/jwt-refresh.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UserRepository } from '@repository/user.repository';
import { SignToken } from '@api/src/auth/helper/signToken';

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') {
constructor(
config: ConfigService,
private userRepository: UserRepository,
private signToken: SignToken,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request) => {
return request?.cookies?.refreshToken;
},
]),
secretOrKey: config.get('JWT_SECRET'),
passReqToCallback: true,
});
}

async validate(req, payload: any) {
const refreshToken = req.cookies?.refreshToken;
const user = await this.userRepository.findById(payload._id);
if (!user) {
throw new ForbiddenException('잘못된 요청입니다.');
}

if (refreshToken !== user.refreshToken) {
throw new UnauthorizedException('refreshToken이 일치하지 않습니다.');
}

const accessToken = await this.signToken.signAccessToken(user._id, user.nickname);

return accessToken;
}
}
2 changes: 2 additions & 0 deletions server/apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApiModule } from './api.module';
import { ValidationPipe } from '@nestjs/common';
import * as Sentry from '@sentry/node';
import { SentryInterceptor } from '../../webhook.interceptor';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
const app = await NestFactory.create(ApiModule);
Expand All @@ -14,6 +15,7 @@ async function bootstrap() {
});
app.useGlobalInterceptors(new SentryInterceptor());
}
app.use(cookieParser());
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
Expand Down
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@sentry/node": "^7.20.0",
"@slack/client": "^5.0.2",
"@slack/webhook": "^6.1.0",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/jest": "28.1.8",
"@types/node": "^16.0.0",
Expand Down