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