OAuth 코드 통합 및 재사용

송준섭 Junseop Song·2023년 11월 25일
0

원래 나는 소셜 로그인 하나 하나 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 로그인까지 추가를 하는 것을 더 쉽게 할 수 있었다!

0개의 댓글

관련 채용 정보