Skip to content

[준섭] 1125(토) 개발 기록 ‐ OAuth 코드 통합 및 재사용

송준섭 edited this page Nov 25, 2023 · 1 revision

원래 나는 깃헙 로그인 개발 기록에서의 방법처럼 소셜 로그인 하나 하나 API를 따로 만드려고 하였다.

그러나 OAuth에서 AccessToken을 받아오는 방법이나, 유저 정보를 받아오는 방법이 서비스 별로 URL 정도만 다르고 비슷하기 때문에 그냥 통합해서 재사용 할 수 있지 않을까? 라는 생각이 들었다.

그래서 이 코드를 리팩토링하여 최대한 재사용하고자 하였다.

원래 코드

@Get('github/signin')
signInWithGithub(@Res({ passthrough: true }) res: Response) {
	res.redirect(
		`https://github.com/login/oauth/authorize?client_id=${process.env.OAUTH_GITHUB_CLIENT_ID}&scope=read:user%20user:email`,
	);
}

@Get('naver/signin')
signInWithGithub(@Res({ passthrough: true }) res: Response) {
	res.redirect(
		`https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${process.env.OAUTH_NAVER_CLIENT_ID}&redirect_uri=${process.env.OAUTH_NAVER_REDIRECT_URI}&state=STATE_STRING`,
	);
}

@Get('github/callback')
async oauthGithubCallback(
	@Query('code') authorizedCode: string,
	@Res({ passthrough: true }) res: Response,
) {
	const { username, accessToken, refreshToken } =
		await this.authService.oauthGithubCallback(authorizedCode);

	if (username) {
		res.cookie('GitHubUsername', username, {
			path: '/',
			httpOnly: true,
		});
		return { username };
	}

	res.cookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME, accessToken, {
		path: '/',
		httpOnly: true,
	});
	res.cookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
		path: '/',
		httpOnly: true,
	});

	return { accessToken, refreshToken };
}

@Get('naver/callback')
async oauthGithubCallback(
	@Query('code') authorizedCode: string,
	@Res({ passthrough: true }) res: Response,
) {
	const { username, accessToken, refreshToken } =
		await this.authService.oauthNaverCallback(authorizedCode);

	if (username) {
		res.cookie('NaverUsername', username, {
			path: '/',
			httpOnly: true,
		});
		return { username };
	}

	res.cookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME, accessToken, {
		path: '/',
		httpOnly: true,
	});
	res.cookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
		path: '/',
		httpOnly: true,
	});

	return { accessToken, refreshToken };
}

@Post('github/signup')
async signUpWithGithub(
	@Body('nickname') nickname: string,
	@Req() req,
	@Res({ passthrough: true }) res: Response,
) {
	let gitHubUsername;
	try {
		gitHubUsername = req.cookies.GitHubUsername;
	} catch (e) {
		throw new UnauthorizedException('잘못된 접근입니다.');
	}

	const savedUser = await this.authService.signUpWithGithub(
		nickname,
		gitHubUsername,
	);

	res.clearCookie('GitHubUsername', {
		path: '/',
		httpOnly: true,
	});

	return savedUser;
}

@Post('naver/signup')
async signUpWithNaver(
	@Body('nickname') nickname: string,
	@Req() req,
	@Res({ passthrough: true }) res: Response,
) {
	let naverUsername;
	try {
		naverUsername = req.cookies.NaverUsername;
	} catch (e) {
		throw new UnauthorizedException('잘못된 접근입니다.');
	}

	const savedUser = await this.authService.signUpWithNaver(
		nickname,
		gitHubUsername,
	);

	res.clearCookie('NaverUsername', {
		path: '/',
		httpOnly: true,
	});

	return savedUser;
}

원래 코드는 위처럼 각 리소스 서버마다 API를 따로 구현하려고 구상했었다.

그러나 어차피 코드도 다 비슷한데 이렇게 구분할 이유가 있나? 라는 생각이 들었다.

그래서 리팩토링을 진행하였다.

리팩토링 후 코드

@Get(':service/signin')
signInWithOAuth(
	@Param('service') service: string,
	@Res({ passthrough: true }) res: Response,
) {
	let redirectUrl: string;
	switch (service) {
		case 'github':
			redirectUrl = `https://github.com/login/oauth/authorize?client_id=${process.env.OAUTH_GITHUB_CLIENT_ID}&scope=read:user%20user:email`;
			break;
		case 'naver':
			redirectUrl = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${process.env.OAUTH_NAVER_CLIENT_ID}&redirect_uri=${process.env.OAUTH_NAVER_REDIRECT_URI}&state=STATE_STRING`;
			break;
		case 'google':
			redirectUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.OAUTH_GOOGLE_CLIENT_ID}&redirect_uri=${process.env.OAUTH_GOOGLE_REDIRECT_URI}&response_type=code&scope=email%20profile`;
			break;
		default:
			throw new NotFoundException('존재하지 않는 서비스입니다.');
	}
	res.redirect(redirectUrl);
}

@Get(':service/callback')
async oauthCallback(
	@Param('service') service: string,
	@Query('code') authorizedCode: string,
	@Query('state') state: string,
	@Res({ passthrough: true }) res: Response,
) {
	const { username, accessToken, refreshToken } =
		await this.authService.oauthCallback(service, authorizedCode, state);

	if (username) {
		res.cookie(`${service}Username`, username, {
			path: '/',
			httpOnly: true,
		});
		return { username };
	}

	res.cookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME, accessToken, {
		path: '/',
		httpOnly: true,
	});
	res.cookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME, refreshToken, {
		path: '/',
		httpOnly: true,
	});

	return { accessToken, refreshToken };
}

@Post(':service/signup')
async signUpWithOAuth(
	@Param('service') service: string,
	@Body('nickname') nickname: string,
	@Req() req,
	@Res({ passthrough: true }) res: Response,
) {
	let resourceServerUsername: string;
	try {
		resourceServerUsername = req.cookies[`${service}Username`];
	} catch (e) {
		throw new UnauthorizedException('잘못된 접근입니다.');
	}

	const savedUser = await this.authService.signUpWithOAuth(
		service,
		nickname,
		resourceServerUsername,
	);

	res.clearCookie(`${service}Username`, {
		path: '/',
		httpOnly: true,
	});

	return savedUser;
}

이렇게 하면 다음에 소셜 로그인 방식이 더 추가가 되더라도 API를 늘릴 필요가 없었다.

그래서 google 로그인까지 추가를 하는 것을 더 쉽게 할 수 있었다!

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally