Skip to content

Commit eff528d

Browse files
authored
Merge pull request #24 from boostcampwm-2022/10-be-oauth-구현체-구현
BE - oauth 구현체 구현 (kakao, naver)
2 parents b76e0df + 44c7c8f commit eff528d

File tree

15 files changed

+310
-65
lines changed

15 files changed

+310
-65
lines changed

backend/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"license": "UNLICENSED",
88
"scripts": {
99
"prebuild": "rimraf dist",
10-
"build": "nest build",
10+
"build": "nest build && tsc-alias",
1111
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
1212
"start": "nest start",
1313
"start:dev": "nest start --watch",
@@ -31,6 +31,7 @@
3131
"@nestjs/typeorm": "^9.0.1",
3232
"@types/cookie-parser": "^1.4.3",
3333
"@types/helmet": "^4.0.0",
34+
"axios": "^1.1.3",
3435
"class-transformer": "^0.5.1",
3536
"class-validator": "^0.13.2",
3637
"cookie-parser": "^1.4.6",
@@ -68,6 +69,7 @@
6869
"ts-jest": "28.0.8",
6970
"ts-loader": "^9.2.3",
7071
"ts-node": "^10.0.0",
72+
"tsc-alias": "^1.7.1",
7173
"tsconfig-paths": "4.1.0",
7274
"typescript": "^4.7.4"
7375
},
@@ -86,6 +88,12 @@
8688
"**/*.(t|j)s"
8789
],
8890
"coverageDirectory": "../coverage",
89-
"testEnvironment": "node"
91+
"testEnvironment": "node",
92+
"moduleNameMapper": {
93+
"@config": "<rootDir>/config/index",
94+
"@constant": "<rootDir>/constant/index",
95+
"@types": "<rootDir>/types/index",
96+
"^src/(.*)$": "<rootDir>/$1"
97+
}
9098
}
9199
}

backend/src/auth/auth.module.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@ import { AuthController } from './controller/auth.controller';
44
import { TypeormUserRepository } from 'src/user/repository/typeorm-user.repository';
55
import { USER_REPOSITORY_INTERFACE } from '@constant';
66
import { UserModule } from 'src/user/user.module';
7-
import { OauthGoogleService } from './service/oauth/google-oauth.service';
87
import { OauthNaverService } from './service/oauth/naver-oauth.service';
8+
import { TypeOrmModule } from '@nestjs/typeorm';
9+
import { OauthKakaoService } from './service/oauth/kakao-oauth.service';
910
import { JwtService } from '@nestjs/jwt';
1011
import { ConfigService } from '@nestjs/config';
1112
import { JwtAccessStrategy } from './jwt/access-jwt.strategy';
1213
import { JwtRefreshStrategy } from './jwt/refresh-jwt.strategy';
1314

14-
const userRepository: ClassProvider = {
15+
export const UserRepository: ClassProvider = {
1516
provide: USER_REPOSITORY_INTERFACE,
1617
useClass: TypeormUserRepository,
1718
};
1819

1920
@Module({
20-
imports: [UserModule],
21+
imports: [UserModule, TypeOrmModule.forFeature([TypeormUserRepository])],
2122
controllers: [AuthController],
23+
providers: [AuthService, UserRepository, OauthKakaoService, OauthNaverService],
2224
providers: [
2325
AuthService,
2426
userRepository,

backend/src/auth/controller/auth.controller.spec.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

backend/src/auth/controller/auth.controller.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Response } from 'express';
12
import {
23
Controller,
34
Get,
@@ -28,18 +29,19 @@ export class AuthController {
2829
constructor(private readonly authService: AuthService) {}
2930

3031
@Get('oauth/redirect/:type')
31-
@Redirect('/', 301)
32-
redirectOauthPage(@Param('type') type: string) {
33-
return this.authService.getSocialUrl(type);
32+
@HttpCode(301)
33+
redirectOauthPage(@Param('type') type: string, @Res() res: Response) {
34+
const pageUrl = this.authService.getSocialUrl(type);
35+
res.redirect(pageUrl);
3436
}
3537

3638
@Get('oauth/callback/:type')
3739
async socialStart(
38-
@Query('authorization_code') authorizationCode: string,
40+
@Query('code') authorizationCode: string,
3941
@Param('type') type: string,
4042
@Res({ passthrough: true }) res: Response
4143
) {
42-
// const user = await this.authService.socialStart({ type, authorizationCode });
44+
const user = await this.authService.socialStart({ type, authorizationCode });
4345
const accessToken = this.authService.createJwt({
4446
payload: { nickname: 'user.nickname', email: 'user.email' },
4547
secret: JWT_ACCESS_TOKEN_SECRET,
Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,100 @@
1+
import { USER_REPOSITORY_INTERFACE } from '@constant';
12
import { Test, TestingModule } from '@nestjs/testing';
3+
import { UserInfo } from 'src/types/auth.type';
4+
import { MockRepository } from 'src/types/mock.type';
5+
import { JoinUserBuilder, UserEntity } from 'src/user/entities/typeorm-user.entity';
6+
import { UserRepository } from 'src/user/repository/interface-user.repository';
27
import { AuthService } from './auth.service';
8+
import { OauthKakaoService } from './oauth/kakao-oauth.service';
9+
import { OauthNaverService } from './oauth/naver-oauth.service';
10+
11+
const mockUserRepository = () => ({
12+
saveUser: jest.fn(),
13+
findUserById: jest.fn(),
14+
findAllUser: jest.fn(),
15+
});
316

417
describe('AuthService', () => {
5-
let service: AuthService;
18+
let authService: AuthService;
19+
let userRepository: MockRepository<UserRepository<UserEntity>>;
20+
let oauthGoogleService: OauthKakaoService;
21+
let oauthNaverService: OauthNaverService;
622

723
beforeEach(async () => {
824
const module: TestingModule = await Test.createTestingModule({
9-
providers: [AuthService],
25+
providers: [
26+
AuthService,
27+
{
28+
provide: USER_REPOSITORY_INTERFACE,
29+
useValue: mockUserRepository(),
30+
},
31+
OauthKakaoService,
32+
OauthNaverService,
33+
],
1034
}).compile();
1135

12-
service = module.get<AuthService>(AuthService);
36+
authService = module.get(AuthService);
37+
userRepository = module.get(USER_REPOSITORY_INTERFACE);
38+
oauthNaverService = module.get(OauthNaverService);
39+
oauthGoogleService = module.get(OauthKakaoService);
40+
});
41+
42+
describe('valid case', () => {
43+
it('의존성 주입 테스트', () => {
44+
expect(authService).toBeDefined();
45+
expect(userRepository).toBeDefined();
46+
expect(oauthNaverService).toBeDefined();
47+
expect(oauthGoogleService).toBeDefined();
48+
});
49+
50+
it('유저의 OAuth로 시작 테스트', async () => {
51+
const user = makeMockUser({ id: 'testId', oauthType: 'naver' } as UserInfo);
52+
53+
jest.spyOn(oauthNaverService, 'getAccessTokenByAuthorizationCode').mockResolvedValue(
54+
'success'
55+
);
56+
jest.spyOn(oauthNaverService, 'getSocialInfoByAccessToken').mockResolvedValue(user);
57+
jest.spyOn(userRepository, 'saveUser').mockResolvedValue(user);
58+
59+
const joinedUser = await authService.socialStart({
60+
type: 'naver',
61+
authorizationCode: 'test',
62+
});
63+
64+
expect(user).toEqual(joinedUser);
65+
});
1366
});
1467

15-
it('should be defined', () => {
16-
expect(service).toBeDefined();
68+
describe('error case', () => {
69+
it('유저의 OAuth 승인 취소 테스트', async () => {
70+
try {
71+
await authService.socialStart({ type: 'naver', authorizationCode: undefined });
72+
} catch (err) {
73+
expect(err).toBeInstanceOf(Error);
74+
expect(err.message).toBe('social 인증이 되지 않았습니다.');
75+
}
76+
});
77+
78+
it('옳바른 OAuth타입이 아닐 때의 오류 테스트', async () => {
79+
try {
80+
await authService.socialStart({ type: 'invalid', authorizationCode: 'authCode' });
81+
} catch (err) {
82+
expect(err).toBeInstanceOf(Error);
83+
expect(err.message).toBe('');
84+
}
85+
});
1786
});
87+
88+
const makeMockUser = (userInfo: UserInfo): UserEntity => {
89+
const { id, email, password, nickname, oauthType } = userInfo;
90+
const userEntity = new JoinUserBuilder()
91+
.setId(id)
92+
.setEmail(email)
93+
.setPassword(password)
94+
.setNickname(nickname)
95+
.setOauthType(oauthType)
96+
.setDefaultValue()
97+
.build();
98+
return userEntity;
99+
};
18100
});

backend/src/auth/service/auth.service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { Inject, Injectable } from '@nestjs/common';
33
import { UserInfo } from 'src/types/auth.type';
44
import { UserEntity } from 'src/user/entities/typeorm-user.entity';
55
import { UserRepository } from 'src/user/repository/interface-user.repository';
6-
import { OauthGoogleService } from './oauth/google-oauth.service';
76
import { OauthNaverService } from './oauth/naver-oauth.service';
87
import { OauthService } from './oauth/interface-oauth.service';
8+
import { OauthKakaoService } from './oauth/kakao-oauth.service';
99
import { JwtService } from '@nestjs/jwt';
1010
import { ConfigService } from '@nestjs/config';
1111
import { CreateJwtDto } from '../dto/create-jwt.dto';
@@ -18,8 +18,8 @@ export class AuthService {
1818
@Inject(USER_REPOSITORY_INTERFACE)
1919
private readonly userRepository: UserRepository<UserEntity>,
2020

21-
private readonly oauthGoogleService: OauthGoogleService,
22-
private readonly oauthNaverService: OauthNaverService,
21+
private readonly oauthKakaoService: OauthKakaoService,
22+
private readonly oauthNaverService: OauthNaverService
2323

2424
private jwtService: JwtService,
2525
private configService: ConfigService
@@ -46,6 +46,8 @@ export class AuthService {
4646
async socialStart({ type, authorizationCode }: { type: string; authorizationCode: string }) {
4747
this.setOauthInstanceByType(type);
4848

49+
if (!authorizationCode) throw new Error('social 인증이 되지 않았습니다.');
50+
4951
const accessToken = await this.oauthInstance.getAccessTokenByAuthorizationCode(
5052
authorizationCode
5153
);
@@ -88,8 +90,8 @@ export class AuthService {
8890
case OAUTH_TYPE.NAVER:
8991
this.oauthInstance = this.oauthNaverService;
9092
break;
91-
case OAUTH_TYPE.GOOGLE:
92-
this.oauthInstance = this.oauthGoogleService;
93+
case OAUTH_TYPE.KAKAO:
94+
this.oauthInstance = this.oauthKakaoService;
9395
break;
9496
default:
9597
throw new Error();

backend/src/auth/service/oauth/google-oauth.service.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
import { UserSocialInfo } from 'src/types/auth.type';
22

33
export interface OauthService {
4+
/**
5+
* 해당하는 oauth type에 따른 authorization page url을 반환합니다.
6+
*/
47
getSocialUrl(): string;
8+
9+
/**
10+
* social login을 승인 시 반환되는 authorization code를 전달하여 access token을 받아서 반환합니다.
11+
* @param authorizationCode social login으로 얻은 authorization code
12+
* @return accessToken - social 인증으로 얻은 accessToken
13+
*/
514
getAccessTokenByAuthorizationCode(authorizationCode: string): Promise<string>;
15+
16+
/**
17+
* accessToken으로 해당 social에 profile 조회 api를 통해 user 정보를 얻어 반환합니다.
18+
* @param accessToken
19+
* @return userInfo
20+
*/
621
getSocialInfoByAccessToken(accessToken: string): Promise<UserSocialInfo>;
722
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
AUTHORIZATION_TOKEN_TYPE,
3+
KAKAO_ACCESS_TOKEN_URL,
4+
KAKAO_AUTHORIZE_PAGE_URL,
5+
KAKAO_PROFILE_API_URL,
6+
OAUTH_CALLBACK_URL,
7+
OAUTH_TYPE,
8+
} from '@constant';
9+
import { Injectable } from '@nestjs/common';
10+
import axios from 'axios';
11+
import { UserSocialInfo } from 'src/types/auth.type';
12+
import { OauthService } from './interface-oauth.service';
13+
14+
@Injectable()
15+
export class OauthKakaoService implements OauthService {
16+
private clientId = process.env.KAKAO_CLIENT_ID;
17+
private clientSecret = process.env.KAKAO_CLIENT_SECRET;
18+
private callbackUrl = [
19+
process.env.SERVER_ORIGIN_URL,
20+
OAUTH_CALLBACK_URL,
21+
OAUTH_TYPE.KAKAO,
22+
].join('/');
23+
24+
getSocialUrl(): string {
25+
const queryString = `?response_type=code&client_id=${this.clientId}&redirect_uri=${this.callbackUrl}`;
26+
return KAKAO_AUTHORIZE_PAGE_URL + queryString;
27+
}
28+
29+
async getAccessTokenByAuthorizationCode(authorizationCode: string): Promise<string> {
30+
const queryString =
31+
`?grant_type=authorization_code` +
32+
`&client_id=${this.clientId}&client_secret=${this.clientSecret}` +
33+
`&redirect_uri=${this.callbackUrl}&code=${authorizationCode}`;
34+
35+
const { access_token } = await axios
36+
.get(KAKAO_ACCESS_TOKEN_URL + queryString)
37+
.then((res) => res.data);
38+
console.log(access_token);
39+
return access_token;
40+
}
41+
42+
/**
43+
* @link https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
44+
* @param accessToken
45+
* @returns userInfo
46+
*/
47+
async getSocialInfoByAccessToken(accessToken: string): Promise<UserSocialInfo> {
48+
const headers = { Authorization: `${AUTHORIZATION_TOKEN_TYPE} ${accessToken}` };
49+
const res = await axios.get(KAKAO_PROFILE_API_URL, { headers }).then((res) => res.data);
50+
51+
const user = res['kakao_account'] as UserSocialInfo;
52+
user.id = res.id;
53+
user.oauthType = OAUTH_TYPE.KAKAO;
54+
return user;
55+
}
56+
}

0 commit comments

Comments
 (0)