Skip to content

Commit

Permalink
Merge pull request #904 from dangdangwalk/backend/DANG-1270
Browse files Browse the repository at this point in the history
backend/DANG-1270: OAuth 서비스 메서드 캡슐화 및 의존성 관리 개선
  • Loading branch information
do0ori authored Dec 16, 2024
2 parents 8bcd31b + 9c1283e commit ca5439c
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 150 deletions.
35 changes: 18 additions & 17 deletions backend/server/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';

import { AuthService } from './auth.service';
import { OauthService } from './oauth/oauth.service.interface';
import { OauthService } from './oauth/oauth.service.base';
import { AccessTokenPayload, RefreshTokenPayload, TokenService } from './token/token.service';
import { OauthAuthorizeData } from './types/oauth-authorize-data.type';
import { OauthData } from './types/oauth-data.type';
Expand All @@ -25,16 +25,9 @@ describe('AuthService', () => {
beforeEach(async () => {
mockOauthServices = new Map<string, OauthService>();

const mockTokenResponse = {
access_token: mockUser.oauthAccessToken,
expires_in: 3600,
refresh_token: mockUser.oauthRefreshToken,
refresh_token_expires_in: 3600,
scope: 'scope',
token_type: 'bearer',
};

const mockUserInfo = {
const mockOauthLoginData = {
oauthAccessToken: mockUser.oauthAccessToken,
oauthRefreshToken: mockUser.oauthRefreshToken,
oauthId: mockUser.oauthId,
oauthNickname: 'test',
email: 'test@mail.com',
Expand All @@ -43,13 +36,21 @@ describe('AuthService', () => {

OAUTH_PROVIDERS.forEach((provider) => {
const mockOauthService = {
requestToken: jest.fn().mockResolvedValue(mockTokenResponse),
requestUserInfo: jest.fn().mockResolvedValue(mockUserInfo),
requestTokenExpiration: jest.fn().mockResolvedValue(undefined),
requestTokenRefresh: jest.fn().mockResolvedValue(mockTokenResponse),
requestUnlink: provider === 'kakao' ? jest.fn().mockResolvedValue(undefined) : undefined,
login: jest.fn().mockResolvedValue(mockOauthLoginData),
signup: jest.fn().mockResolvedValue({
oauthId: mockUser.oauthId,
oauthNickname: 'test',
email: 'test@mail.com',
profileImageUrl: 'test.jpg',
}),
logout: jest.fn().mockResolvedValue(undefined),
reissueTokens: jest.fn().mockResolvedValue({
oauthAccessToken: 'new_' + mockUser.oauthAccessToken,
oauthRefreshToken: 'new_' + mockUser.oauthRefreshToken,
}),
deactivate: provider === 'kakao' ? jest.fn().mockResolvedValue(undefined) : undefined,
};
mockOauthServices.set(provider, mockOauthService as OauthService);
mockOauthServices.set(provider, mockOauthService as unknown as OauthService);
});

const module: TestingModule = await Test.createTestingModule({
Expand Down
23 changes: 7 additions & 16 deletions backend/server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { Inject, Injectable, NotFoundException, UnauthorizedException } from '@n
import { ConfigService } from '@nestjs/config';
import { Transactional } from 'typeorm-transactional';

import { KakaoService } from './oauth/kakao.service';
import { OauthService } from './oauth/oauth.service.interface';
import { OauthService } from './oauth/oauth.service.base';
import { AccessTokenPayload, RefreshTokenPayload, TokenService } from './token/token.service';
import { AuthData } from './types/auth-data.type';
import { OauthAuthorizeData } from './types/oauth-authorize-data.type';
Expand Down Expand Up @@ -38,13 +37,11 @@ export class AuthService {
async login({ authorizeCode, provider }: OauthAuthorizeData): Promise<AuthData | OauthData | undefined> {
const oauthService = this.getOauthService(provider);

const { access_token: oauthAccessToken, refresh_token: oauthRefreshToken } = await oauthService.requestToken(
const { oauthAccessToken, oauthRefreshToken, oauthId } = await oauthService.login(
authorizeCode,
this.REDIRECT_URI,
);

const { oauthId } = await oauthService.requestUserInfo(oauthAccessToken);

const refreshToken = await this.tokenService.signRefreshToken(oauthId, provider);
this.logger.debug('login - signRefreshToken', { refreshToken });

Expand All @@ -70,7 +67,7 @@ export class AuthService {
async signup({ oauthAccessToken, oauthRefreshToken, provider }: OauthData): Promise<AuthData> {
const oauthService = this.getOauthService(provider);

const { oauthId, oauthNickname, email } = await oauthService.requestUserInfo(oauthAccessToken);
const { oauthId, oauthNickname, email } = await oauthService.signup(oauthAccessToken);
const profileImageUrl = this.S3_PROFILE_IMAGE_PATH;

const refreshToken = await this.tokenService.signRefreshToken(oauthId, provider);
Expand Down Expand Up @@ -98,9 +95,7 @@ export class AuthService {
const { oauthAccessToken } = await this.usersService.findOne({ where: { id: userId } });
this.logger.debug('logout - oauthAccessToken', { oauthAccessToken });

if (provider === 'kakao') {
await (oauthService as KakaoService).requestTokenExpiration(oauthAccessToken);
}
await oauthService.logout(oauthAccessToken);
}

async reissueTokens({ oauthId, provider }: RefreshTokenPayload): Promise<AuthData> {
Expand All @@ -112,11 +107,11 @@ export class AuthService {
});

const [
{ access_token: newOauthAccessToken, refresh_token: newOauthRefreshToken },
{ oauthAccessToken: newOauthAccessToken, oauthRefreshToken: newOauthRefreshToken },
newAccessToken,
newRefreshToken,
] = await Promise.all([
oauthService.requestTokenRefresh(oauthRefreshToken),
oauthService.reissueTokens(oauthRefreshToken),
this.tokenService.signAccessToken(userId, provider),
this.tokenService.signRefreshToken(oauthId, provider),
]);
Expand All @@ -137,11 +132,7 @@ export class AuthService {

const { oauthAccessToken } = await this.usersService.findOne({ where: { id: userId } });

if (provider === 'kakao') {
await (oauthService as KakaoService).requestUnlink(oauthAccessToken);
} else {
await oauthService.requestTokenExpiration(oauthAccessToken);
}
await oauthService.deactivate(oauthAccessToken);

await this.deleteUserData(userId);
}
Expand Down
30 changes: 17 additions & 13 deletions backend/server/src/auth/oauth/__mocks__/oauth.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { RequestToken, RequestTokenRefresh, RequestUserInfo } from '../oauth.service.interface';
import { OauthLoginData, OauthReissueData, OauthSignupData } from '../oauth.service.base';

export const MockOauthService = {
requestToken: jest.fn().mockResolvedValue({
access_token: 'mock_oauth_access_token',
refresh_token: 'mock_oauth_refresh_token',
} as RequestToken),
login: jest.fn().mockResolvedValue({
oauthAccessToken: 'mock_oauth_access_token',
oauthRefreshToken: 'mock_oauth_refresh_token',
oauthId: '12345',
oauthNickname: 'mock_oauth_nickname',
email: 'mock_email@example.com',
profileImageUrl: 'mock_profile_image.jpg',
} as OauthLoginData),

requestUserInfo: jest.fn().mockResolvedValue({
signup: jest.fn().mockResolvedValue({
oauthId: '12345',
oauthNickname: 'mock_oauth_nickname',
email: 'mock_email@example.com',
profileImageUrl: 'mock_profile_image.jpg',
} as RequestUserInfo),
} as OauthSignupData),

requestTokenExpiration: jest.fn(),
logout: jest.fn(),

requestUnlink: jest.fn(),
reissueTokens: jest.fn().mockResolvedValue({
oauthAccessToken: 'new_mock_oauth_access_token',
oauthRefreshToken: 'new_mock_oauth_refresh_token',
} as OauthReissueData),

requestTokenRefresh: jest.fn().mockResolvedValue({
access_token: 'new_mock_oauth_access_token',
refresh_token: 'new_mock_oauth_refresh_token',
} as RequestTokenRefresh),
deactivate: jest.fn(),
};
77 changes: 59 additions & 18 deletions backend/server/src/auth/oauth/google.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { firstValueFrom } from 'rxjs';

import { OauthService, RequestToken, RequestTokenRefresh, RequestUserInfo } from './oauth.service.interface';
import {
OauthLoginData,
OauthReissueData,
OauthService,
OauthSignupData,
RequestTokenRefreshResponse,
RequestTokenResponse,
} from './oauth.service.base';

import { WinstonLoggerService } from '../../common/logger/winstonLogger.service';

interface TokenResponse {
interface TokenResponse extends RequestTokenResponse {
access_token: string;
expires_in: number;
refresh_token: string;
Expand All @@ -23,28 +30,67 @@ interface UserInfoResponse {
picture: string;
}

interface TokenRefreshResponse {
interface TokenRefreshResponse extends RequestTokenRefreshResponse {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}

@Injectable()
export class GoogleService implements OauthService {
export class GoogleService extends OauthService {
constructor(
private readonly configService: ConfigService,
private readonly httpService: HttpService,
private readonly logger: WinstonLoggerService,
) {}
readonly configService: ConfigService,
readonly httpService: HttpService,
readonly logger: WinstonLoggerService,
) {
super(configService, httpService, logger);
}

private readonly CLIENT_ID = this.configService.get<string>('GOOGLE_CLIENT_ID');
private readonly CLIENT_SECRET = this.configService.get<string>('GOOGLE_CLIENT_SECRET');
private readonly TOKEN_API = this.configService.get<string>('GOOGLE_TOKEN_API')!;
private readonly USER_INFO_API = this.configService.get<string>('GOOGLE_USER_INFO_API')!;
private readonly REVOKE_API = this.configService.get<string>('GOOGLE_REVOKE_API')!;

async requestToken(authorizeCode: string, redirectURI: string): Promise<RequestToken> {
async login(authorizeCode: string, redirectURI: string): Promise<OauthLoginData> {
const tokens = await this.requestToken(authorizeCode, redirectURI);
const userInfo = await this.requestUserInfo(tokens.access_token);

return {
oauthAccessToken: tokens.access_token,
oauthRefreshToken: tokens.refresh_token,
oauthId: userInfo.id,
oauthNickname: userInfo.name,
email: userInfo.email,
profileImageUrl: userInfo.picture,
};
}

async signup(oauthAccessToken: string): Promise<OauthSignupData> {
const userInfo = await this.requestUserInfo(oauthAccessToken);

return {
oauthId: userInfo.id,
oauthNickname: userInfo.name,
email: userInfo.email,
profileImageUrl: userInfo.picture,
};
}

async logout(_oauthAccessToken: string): Promise<void> {}

async reissueTokens(oauthRefreshToken: string): Promise<OauthReissueData> {
const tokens = await this.requestTokenRefresh(oauthRefreshToken);

return { oauthAccessToken: tokens.access_token, oauthRefreshToken: tokens.refresh_token };
}

async deactivate(oauthAccessToken: string): Promise<void> {
await this.requestTokenExpiration(oauthAccessToken);
}

private async requestToken(authorizeCode: string, redirectURI: string): Promise<TokenResponse> {
try {
const { data } = await firstValueFrom(
this.httpService.post<TokenResponse>(this.TOKEN_API, {
Expand All @@ -69,7 +115,7 @@ export class GoogleService implements OauthService {
}
}

async requestUserInfo(accessToken: string): Promise<RequestUserInfo> {
private async requestUserInfo(accessToken: string): Promise<UserInfoResponse> {
try {
const { data } = await firstValueFrom(
this.httpService.get<UserInfoResponse>(this.USER_INFO_API, {
Expand All @@ -81,12 +127,7 @@ export class GoogleService implements OauthService {

this.logger.log('requestUserInfo', { ...data });

return {
oauthId: data.id,
oauthNickname: data.name,
email: data.email,
profileImageUrl: data.picture,
};
return data;
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
this.logger.error('Google: 유저 정보 조회 요청이 실패했습니다', {
Expand All @@ -99,7 +140,7 @@ export class GoogleService implements OauthService {
}
}

async requestTokenExpiration(accessToken: string) {
private async requestTokenExpiration(accessToken: string): Promise<void> {
try {
await firstValueFrom(
this.httpService.post(
Expand Down Expand Up @@ -127,7 +168,7 @@ export class GoogleService implements OauthService {
}
}

async requestTokenRefresh(refreshToken: string): Promise<RequestTokenRefresh> {
private async requestTokenRefresh(refreshToken: string): Promise<TokenRefreshResponse> {
try {
const { data } = await firstValueFrom(
this.httpService.post<TokenRefreshResponse>(this.TOKEN_API, {
Expand Down
Loading

0 comments on commit ca5439c

Please sign in to comment.