From e07cdba8911ebd37e1f6f4e5b1f235b7537fdde9 Mon Sep 17 00:00:00 2001 From: victolee0 <39608452+victolee0@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:54:06 +0900 Subject: [PATCH] =?UTF-8?q?[BE#449]=20=EC=95=A0=ED=94=8C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20(#450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 애플 로그인 구현 * chore: 검증 실패 예외 처리 * chore: 이모지 대응 * fix: email & auth type으로 유저 존재 확인 - auth type으로 유저 존재 확인 - access token에 auth type 변수로 변경 --- BE/src/admin/admin.controller.ts | 7 ++- BE/src/auth/auth.controller.ts | 14 ++++- BE/src/auth/auth.service.ts | 68 +++++++++++++++++++++++-- BE/src/auth/guard/bearer-token.guard.ts | 5 +- BE/src/common/config/typeorm.config.ts | 1 + BE/src/users/entity/users.entity.ts | 4 +- BE/src/users/users.service.ts | 9 +++- 7 files changed, 94 insertions(+), 14 deletions(-) diff --git a/BE/src/admin/admin.controller.ts b/BE/src/admin/admin.controller.ts index 5b58ed8..6407052 100644 --- a/BE/src/admin/admin.controller.ts +++ b/BE/src/admin/admin.controller.ts @@ -21,7 +21,7 @@ export class AdminController { async createMockUsers(@Body('emails') emails: Array<{ email: string }>) { const createdUsers = []; for (const { email } of emails) { - const { access_token } = await this.authService.loginWithGoogle({ + const { access_token } = await this.authService.loginWithOAuth({ email, auth_type: 'google', }); @@ -31,7 +31,10 @@ export class AdminController { for (let idx = 0; idx < createdUsers.length; idx++) { const email = createdUsers[idx].email; - const me = await this.usersService.findUserByEmail(email); + const me = await this.usersService.findUserByEmailAndAuthType( + email, + 'google', + ); for (let i = 1; i <= MATES_MAXIMUM; i++) { const friendIdx = (idx + i) % createdUsers.length; const friendNickname = createdUsers[friendIdx].nickname; diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 0164b4c..b7f3df0 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -30,6 +30,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ConfigService } from '@nestjs/config'; import { ENV } from 'src/common/const/env-keys.const'; import { getImageUrl } from 'src/common/utils/utils'; +import { identity } from 'rxjs'; @ApiTags('로그인 페이지') @Controller('auth') @@ -45,7 +46,7 @@ export class AuthController { @ApiExcludeEndpoint() googleAuth(@Req() req) { const user = req.user; - return this.authService.loginWithGoogle(user); + return this.authService.loginWithOAuth(user); } @Post('google/app') @@ -54,7 +55,16 @@ export class AuthController { @ApiResponse({ status: 401, description: '인증 실패' }) async googleAppAuth(@Body('access_token') accessToken: string) { const email = await this.authService.getUserInfo(accessToken); - return this.authService.loginWithGoogle({ email, auth_type: 'google' }); + return this.authService.loginWithOAuth({ email, auth_type: 'google' }); + } + + @Post('apple/app') + @ApiOperation({ summary: 'Apple 아이폰용 로그인 (완)' }) + @ApiResponse({ status: 201, description: '인증 성공' }) + @ApiResponse({ status: 401, description: '인증 실패' }) + async appleAppAuth(@Body('identity_token') identity_token: string) { + const email = await this.authService.getAppleUserInfo(identity_token); + return this.authService.loginWithOAuth({ email, auth_type: 'apple' }); } @Get('logout') diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index af5aa43..f31c998 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -1,7 +1,13 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { UsersModel } from 'src/users/entity/users.entity'; import { UsersService } from 'src/users/users.service'; +import crypto from 'crypto'; +import jwt from 'jsonwebtoken'; @Injectable() export class AuthService { @@ -35,13 +41,16 @@ export class AuthService { email: user.email, nickname: user.nickname, type: 'access', - auth_type: 'google', + auth_type: user.auth_type, }; return this.jwtService.sign(payload); } - public async loginWithGoogle(user) { - const prevUser = await this.usersService.findUserByEmail(user.email); + public async loginWithOAuth(user) { + const prevUser = await this.usersService.findUserByEmailAndAuthType( + user.email, + user.auth_type, + ); if (!prevUser) { const id = user.email.split('@')[0]; const userEntity = { @@ -49,6 +58,7 @@ export class AuthService { id.slice(0, 20) + Buffer.from(user.email + user.auth_type).toString('base64'), email: user.email, + auth_type: user.auth_type, } as UsersModel; const newUser = await this.usersService.createUser(userEntity); return { @@ -80,4 +90,54 @@ export class AuthService { throw error; } } + + public async getAppleUserInfo(JWT: string): Promise { + const { header, payload } = this.jwtService.decode(JWT, { complete: true }); + + if (!header || !payload) + throw new UnauthorizedException('유효하지 않은 토큰입니다.'); + if (payload['iss'] !== 'https://appleid.apple.com') + throw new UnauthorizedException('유효하지 않은 토큰입니다.'); + if (payload['exp'] < Date.now() / 1000) + throw new UnauthorizedException('유효하지 않은 토큰입니다.'); + + try { + const url = 'https://appleid.apple.com/auth'; + const res = await fetch(`${url}/keys`, { + method: 'GET', + }); + if (!res.ok) { + throw new NotFoundException('Apple 키를 찾을 수 없습니다.'); + } + const { keys } = await res.json(); + + const { kty, n, e } = keys.find((key) => key.kid === header['kid']); + const pem = this.createPem(kty, n, e); + const decoded = jwt.verify(JWT, pem, { + algorithms: ['RS256'], + }); + return decoded['email']; + } catch (error) { + if (error.message === 'invalid signature') { + throw new UnauthorizedException('토큰 검증 실패'); + } + throw error; + } + } + + private createPem(kty, n, e) { + const JWK = crypto.createPublicKey({ + format: 'jwk', + key: { + kty, + n, + e, + }, + }); + + return JWK.export({ + type: 'pkcs1', + format: 'pem', + }); + } } diff --git a/BE/src/auth/guard/bearer-token.guard.ts b/BE/src/auth/guard/bearer-token.guard.ts index 7a348b4..196012c 100644 --- a/BE/src/auth/guard/bearer-token.guard.ts +++ b/BE/src/auth/guard/bearer-token.guard.ts @@ -26,7 +26,10 @@ class BearerTokenGuard implements CanActivate { const result = this.authService.verifyToken(token); - const user = await this.usersService.findUserByEmail(result.email); + const user = await this.usersService.findUserByEmailAndAuthType( + result.email, + result.auth_type, + ); if (!user) { throw new UnauthorizedException('해당 유저는 회원이 아닙니다.'); diff --git a/BE/src/common/config/typeorm.config.ts b/BE/src/common/config/typeorm.config.ts index 6813447..68224a0 100644 --- a/BE/src/common/config/typeorm.config.ts +++ b/BE/src/common/config/typeorm.config.ts @@ -13,6 +13,7 @@ export const typeormConfig = (config: ConfigService): TypeOrmModuleOptions => ({ username: config.get(ENV.DATABASE_USERNAME), password: config.get(ENV.DATABASE_PASSWORD), database: config.get(ENV.DATABASE_NAME), + charset: 'utf8mb4_unicode_ci', entities: [StudyLogs, Categories, UsersModel, Mates], timezone: 'Z', synchronize: true, diff --git a/BE/src/users/entity/users.entity.ts b/BE/src/users/entity/users.entity.ts index 760719c..86bd31b 100644 --- a/BE/src/users/entity/users.entity.ts +++ b/BE/src/users/entity/users.entity.ts @@ -29,9 +29,7 @@ export class UsersModel { example: 'google_email@email.com', description: 'OAuth로 로그인 한 구글 계정 아이디', }) - @Column({ - unique: true, - }) + @Column() @IsString() @IsEmail() email: string; diff --git a/BE/src/users/users.service.ts b/BE/src/users/users.service.ts index 8f37c66..f4092e6 100644 --- a/BE/src/users/users.service.ts +++ b/BE/src/users/users.service.ts @@ -25,6 +25,7 @@ export class UsersService { nickname: user.nickname, email: user.email, image_url: null, + auth_type: user.auth_type, }); return await this.usersRepository.save(userObject); } catch (error) { @@ -88,9 +89,13 @@ export class UsersService { }; } - async findUserByEmail(email: string): Promise { + async findUserByEmailAndAuthType( + email: string, + auth_type: string, + ): Promise { + const authEnumStr = auth_type.toUpperCase(); const selectedUser = await this.usersRepository.findOne({ - where: { email }, + where: { email, auth_type: auth_type[authEnumStr] }, }); return selectedUser;