Skip to content

Commit

Permalink
[BE#449] 애플 로그인 구현 (#450)
Browse files Browse the repository at this point in the history
* feat: 애플 로그인 구현

* chore: 검증 실패 예외 처리

* chore: 이모지 대응

* fix: email & auth type으로 유저 존재 확인

- auth type으로 유저 존재 확인
- access token에 auth type 변수로 변경
  • Loading branch information
victolee0 authored Dec 13, 2023
1 parent cb57ea6 commit e07cdba
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 14 deletions.
7 changes: 5 additions & 2 deletions BE/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand All @@ -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;
Expand Down
14 changes: 12 additions & 2 deletions BE/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand Down
68 changes: 64 additions & 4 deletions BE/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -35,20 +41,24 @@ 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 = {
nickname:
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 {
Expand Down Expand Up @@ -80,4 +90,54 @@ export class AuthService {
throw error;
}
}

public async getAppleUserInfo(JWT: string): Promise<string> {
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',
});
}
}
5 changes: 4 additions & 1 deletion BE/src/auth/guard/bearer-token.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('해당 유저는 회원이 아닙니다.');
Expand Down
1 change: 1 addition & 0 deletions BE/src/common/config/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const typeormConfig = (config: ConfigService): TypeOrmModuleOptions => ({
username: config.get<string>(ENV.DATABASE_USERNAME),
password: config.get<string>(ENV.DATABASE_PASSWORD),
database: config.get<string>(ENV.DATABASE_NAME),
charset: 'utf8mb4_unicode_ci',
entities: [StudyLogs, Categories, UsersModel, Mates],
timezone: 'Z',
synchronize: true,
Expand Down
4 changes: 1 addition & 3 deletions BE/src/users/entity/users.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ export class UsersModel {
example: '[email protected]',
description: 'OAuth로 로그인 한 구글 계정 아이디',
})
@Column({
unique: true,
})
@Column()
@IsString()
@IsEmail()
email: string;
Expand Down
9 changes: 7 additions & 2 deletions BE/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -88,9 +89,13 @@ export class UsersService {
};
}

async findUserByEmail(email: string): Promise<UsersModel> {
async findUserByEmailAndAuthType(
email: string,
auth_type: string,
): Promise<UsersModel> {
const authEnumStr = auth_type.toUpperCase();
const selectedUser = await this.usersRepository.findOne({
where: { email },
where: { email, auth_type: auth_type[authEnumStr] },
});

return selectedUser;
Expand Down

0 comments on commit e07cdba

Please sign in to comment.