diff --git a/BE/package-lock.json b/BE/package-lock.json index cb72a103..4f1add7a 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -20,11 +20,13 @@ "@nestjs/swagger": "^8.0.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.7", + "@types/cookie-parser": "^1.4.7", "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "docker": "^1.0.0", "dotenv": "^16.4.5", @@ -1961,6 +1963,15 @@ "version": "0.4.1", "license": "MIT" }, + "node_modules/@types/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "dev": true, @@ -3782,6 +3793,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "license": "MIT" diff --git a/BE/package.json b/BE/package.json index 224dc019..8c4cf8f8 100644 --- a/BE/package.json +++ b/BE/package.json @@ -30,12 +30,14 @@ "@nestjs/schedule": "^4.1.1", "@nestjs/swagger": "^8.0.1", "@nestjs/typeorm": "^10.0.2", - "@types/passport-jwt": "^4.0.1", "@nestjs/websockets": "^10.4.7", + "@types/cookie-parser": "^1.4.7", + "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "docker": "^1.0.0", "dotenv": "^16.4.5", diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 2bfffe99..b95a78ae 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -3,19 +3,24 @@ import { Post, Get, Body, - Req, ValidationPipe, UseGuards, + Req, + Res, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { AuthCredentialsDto } from './dto/authCredentials.dto'; -import { Request } from 'express'; +import { Request, Response } from 'express'; +import { ConfigService } from '@nestjs/config'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + constructor( + private authService: AuthService, + private configService: ConfigService, + ) {} @ApiOperation({ summary: '회원 가입 API' }) @Post('/signup') @@ -24,7 +29,7 @@ export class AuthController { } @ApiOperation({ summary: '로그인 API' }) - @Get('/login') + @Post('/login') loginWithCredentials( @Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto, ) { @@ -37,4 +42,36 @@ export class AuthController { test(@Req() req: Request) { return req; } + + @ApiOperation({ summary: 'Kakao 로그인 API' }) + @Get('/kakao') + async kakaoLogin( + @Body() authCredentialsDto: AuthCredentialsDto, + @Res() res: Response, + ) { + const { accessToken, refreshToken } = + await this.authService.kakaoLoginUser(authCredentialsDto); + + res.cookie('refreshToken', refreshToken, { httpOnly: true }); + res.cookie('accessToken', accessToken, { httpOnly: true }); + res.cookie('isRefreshToken', true, { httpOnly: true }); + return res.redirect(this.configService.get('CLIENT_URL')); + } + + @ApiOperation({ summary: 'Refresh Token 요청 API' }) + @Get('/refresh') + async refresh(@Req() req: Request, @Res() res: Response) { + const refreshToken = req.cookies['refreshToken']; + const accessToken = req.cookies['accessToken']; + + if (!refreshToken || !accessToken) { + return res.status(401).send(); + } + + const newAccessToken = await this.authService.refreshToken(refreshToken); + + res.cookie('accessToken', newAccessToken, { httpOnly: true }); + res.cookie('isRefreshToken', true, { httpOnly: true }); + return res.status(200).send(); + } } diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 00738ec5..026ff074 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -21,7 +21,7 @@ export class AuthService { async loginUser( authCredentialsDto: AuthCredentialsDto, - ): Promise<{ accessToken: string }> { + ): Promise<{ accessToken: string; refreshToken: string }> { const { email, password } = authCredentialsDto; const user = await this.userRepository.findOne({ where: { email } }); @@ -31,11 +31,16 @@ export class AuthService { this.setCurrentRefreshToken(refreshToken, user.id); - return { accessToken }; + return { accessToken, refreshToken }; } throw new UnauthorizedException('Please check your login credentials'); } + async kakaoLoginUser( + authCredentialsDto: AuthCredentialsDto, + ): Promise<{ accessToken: string; refreshToken: string }> { + return await this.getJWTToken(authCredentialsDto); + } async getJWTToken(authCredentialsDto: AuthCredentialsDto) { const accessToken = await this.generateAccessToken(authCredentialsDto); const refreshToken = await this.generateRefreshToken(authCredentialsDto); @@ -90,4 +95,34 @@ export class AuthService { currentRefreshTokenExpiresAt, }); } + + async refreshToken(refreshToken: string): Promise { + try { + const decodedRefreshToken = this.jwtService.verify(refreshToken, { + secret: this.configService.get('JWT_REFRESH_SECRET'), + }); + + const user = decodedRefreshToken.email + ? await this.userRepository.findOne({ + where: { email: decodedRefreshToken.email }, + }) + : await this.userRepository.findOne({ + where: { kakaoId: decodedRefreshToken.kakaoId }, + }); + + const isRefreshTokenMatching = await bcrypt.compare( + refreshToken, + user.currentRefreshToken, + ); + + if (!isRefreshTokenMatching) { + throw new UnauthorizedException('Invalid Token'); + } + + const accessToken = this.generateAccessToken(user.toAuthCredentialsDto()); + return accessToken; + } catch (error) { + throw new UnauthorizedException('Invalid Token'); + } + } } diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index fbdacb39..4dfc6541 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -1,4 +1,5 @@ import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { AuthCredentialsDto } from './dto/authCredentials.dto'; @Entity() export class User extends BaseEntity { @@ -22,4 +23,13 @@ export class User extends BaseEntity { @Column({ type: 'datetime', nullable: true }) currentRefreshTokenExpiresAt: Date; + + toAuthCredentialsDto(): AuthCredentialsDto { + if (this.kakaoId === -1) { + return { + email: this.email, + password: this.password, + }; + } + } } diff --git a/BE/src/main.ts b/BE/src/main.ts index bbc099aa..37ceb7cf 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { Logger } from '@nestjs/common'; import { AppModule } from './app.module'; import { setupSwagger } from './util/swagger'; +import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -14,6 +15,7 @@ async function bootstrap() { optionsSuccessStatus: 204, }); + app.use(cookieParser()); await app.listen(process.env.PORT ?? 3000); }