diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts index 3f6a36c9..29267584 100644 --- a/server/src/auth/auth.controller.ts +++ b/server/src/auth/auth.controller.ts @@ -9,6 +9,8 @@ import { ProfileUploadRequiredException } from 'src/exceptions/profile-upload-re import { PresignedUrlResponseDto } from 'src/presigned-url/dto/presigned-url-response.dto'; import { PresignedUrlService } from 'src/presigned-url/presigned-url.service'; import { SignupProfilePresignedUrlRequestDto } from 'src/presigned-url/dto/signup-profile-presigned-url-request.dto'; +import { InvalidKakaoIdTokenException } from 'src/exceptions/invalid-kakao-idtoken.exception'; +import { InconsistentKakaoUuidException } from 'src/exceptions/inconsistent-kakao-uuid.exception'; import { InvalidGoogldIdTokenException } from 'src/exceptions/invalid-google-idToken.exception'; import { InconsistentGoogldUuidException } from 'src/exceptions/inconsistent-google-uuid.exception'; import { AuthService } from './auth.service'; @@ -32,12 +34,14 @@ export class AuthController { */ @Post('signup') @ApiSuccessResponse(201, '회원가입 성공', SignupResponseDto) - @ApiFailResponse('업로드 필요', [ProfileUploadRequiredException]) - @ApiFailResponse('회원가입 실패', [UserConflictException]) @ApiFailResponse('인증 실패', [ + InvalidKakaoIdTokenException, + InconsistentKakaoUuidException, InvalidGoogldIdTokenException, InconsistentGoogldUuidException, ]) + @ApiFailResponse('업로드 필요', [ProfileUploadRequiredException]) + @ApiFailResponse('회원가입 실패', [UserConflictException]) signUp( @Body() signupRequestDto: SignupRequestDto, ): Promise { @@ -51,6 +55,8 @@ export class AuthController { @ApiSuccessResponse(201, '로그인 성공', SigninResponseDto) @ApiFailResponse('인증 실패', [ LoginFailException, + InvalidKakaoIdTokenException, + InconsistentKakaoUuidException, InvalidGoogldIdTokenException, InconsistentGoogldUuidException, ]) diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 2c5f6e95..93cb535a 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -9,6 +9,12 @@ import { InvalidRefreshTokenException } from 'src/exceptions/invalid-refresh-tok import { UserInfoDto } from 'src/user/dto/user-info.dto'; import { checkUpload } from 'src/ncpAPI/listObjects'; import { ProfileUploadRequiredException } from 'src/exceptions/profile-upload-required-exception'; +import * as assert from 'assert'; +import axios from 'axios'; +import { InvalidKakaoIdTokenException } from 'src/exceptions/invalid-kakao-idtoken.exception'; +import { InconsistentKakaoUuidException } from 'src/exceptions/inconsistent-kakao-uuid.exception'; +import { createPublicKey } from 'crypto'; +import { PlatformEnum, SignupRequestDto } from './dto/signup-request.dto'; import { OAuth2Client } from 'google-auth-library'; import { InvalidGoogldIdTokenException } from 'src/exceptions/invalid-google-idToken.exception'; import { InconsistentGoogldUuidException } from 'src/exceptions/inconsistent-google-uuid.exception'; @@ -97,6 +103,59 @@ export class AuthService { return { jwt, profile }; } + async verifyKakaoIdToken(idToken: string) { + try { + const tokens = idToken.split('.'); + assert(tokens.length === 3); + const [header, payload] = tokens + .slice(0, 2) + .map((token) => + JSON.parse(Buffer.from(token, 'base64').toString('utf-8')), + ); + assert( + payload.iss === process.env.KAKAO_ISS && + payload.aud === process.env.KAKAO_APP_KEY && + payload.exp > Date.now() / 1000, + ); + const jwks = (await axios.get(process.env.KAKAO_KEY_URL)).data.keys; + + const key = jwks.find((jwk) => jwk.kid === header.kid); + assert(key); + const keyObject = createPublicKey({ + key, + format: 'jwk', + }); + const secret = keyObject.export({ type: 'pkcs1', format: 'pem' }); + await this.jwtService.verifyAsync(idToken, { + algorithms: [key.alg], + secret, + }); + const id = Number(payload.sub); + + return this.formatAsUUID(id, id); + } catch (e) { + throw new InvalidKakaoIdTokenException(); + } + } + + formatAsUUID(mostSigBits: number, leastSigBits: number) { + const most = mostSigBits.toString(16).padStart(16, '0'); + const least = leastSigBits.toString(16).padStart(16, '0'); + return `${most.substring(0, 8)}-${most.substring(8, 12)}-${most.substring( + 12, + )}-${least.substring(0, 4)}-${least.substring(4)}`; + } + + async verifyUuid(platform: PlatformEnum, idToken: string, uuid: string) { + switch (platform) { + case PlatformEnum.KAKAO: + if (uuid !== (await this.verifyKakaoIdToken(idToken))) + throw new InconsistentKakaoUuidException(); + break; + default: + } + } + async signin(signinRequestDto: SigninRequestDto): Promise { const { uuid, platform, idToken } = signinRequestDto; await this.verifyUuid(platform, idToken, uuid); diff --git a/server/src/auth/dto/signup-request.dto.ts b/server/src/auth/dto/signup-request.dto.ts index 9d1ca577..58b47385 100644 --- a/server/src/auth/dto/signup-request.dto.ts +++ b/server/src/auth/dto/signup-request.dto.ts @@ -1,7 +1,7 @@ import { IsEnum, IsNotEmpty } from 'class-validator'; import { UserDto } from 'src/user/dto/user.dto'; -enum PlatformEnum { +export enum PlatformEnum { GOOGLE = 'GOOGLE', KAKAO = 'KAKAO', } diff --git a/server/src/exceptions/enum/exception.enum.ts b/server/src/exceptions/enum/exception.enum.ts index 4ceaecb5..20401af7 100644 --- a/server/src/exceptions/enum/exception.enum.ts +++ b/server/src/exceptions/enum/exception.enum.ts @@ -7,6 +7,8 @@ enum ErrorCode { InvalidToken = 1002, BadTokenFormat = 1003, InvalidRefreshToken = 1005, + InvalidKakaoIdToken = 1007, + InconsistentKakaoUuid = 1017, InvalidGoogleIdToken = 1006, InconsistentGoogleUuid = 1016, VideoNotFound = 4000, @@ -49,6 +51,8 @@ const ErrorMessage = { [ErrorCode.EncodingActionFail]: '인코딩 액션 실패', [ErrorCode.GreenEyeApiFail]: 'greeneye api 요청 실패', [ErrorCode.GreenEyeActionFail]: 'greeneye 액션 실패', + [ErrorCode.InvalidKakaoIdToken]: '유효하지 않은 카카오 idToken', + [ErrorCode.InconsistentKakaoUuid]: '카카오 idToken과 uuid가 일치하지 않음', }; export { ErrorCode, ErrorMessage }; diff --git a/server/src/exceptions/inconsistent-kakao-uuid.exception.ts b/server/src/exceptions/inconsistent-kakao-uuid.exception.ts new file mode 100644 index 00000000..cc35abd7 --- /dev/null +++ b/server/src/exceptions/inconsistent-kakao-uuid.exception.ts @@ -0,0 +1,9 @@ +import { HttpStatus } from '@nestjs/common'; +import { ErrorCode } from 'src/exceptions/enum/exception.enum'; +import { BaseException } from './base.exception'; + +export class InconsistentKakaoUuidException extends BaseException { + constructor() { + super(ErrorCode.InconsistentKakaoUuid, HttpStatus.UNAUTHORIZED); + } +} diff --git a/server/src/exceptions/invalid-kakao-idtoken.exception.ts b/server/src/exceptions/invalid-kakao-idtoken.exception.ts new file mode 100644 index 00000000..2d32ed68 --- /dev/null +++ b/server/src/exceptions/invalid-kakao-idtoken.exception.ts @@ -0,0 +1,9 @@ +import { ErrorCode } from 'src/exceptions/enum/exception.enum'; +import { HttpStatus } from '@nestjs/common'; +import { BaseException } from './base.exception'; + +export class InvalidKakaoIdTokenException extends BaseException { + constructor() { + super(ErrorCode.InvalidKakaoIdToken, HttpStatus.UNAUTHORIZED); + } +}