Skip to content

[준섭] 1122(수) 개발기록 ‐ 깃헙 로그인 구현

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

GitHub OAuth App 생성

일단 깃허브에 들어가서 개인 Settings 메뉴에 들어간다.

image

그 후 맨 밑 Developer settings 메뉴 선택

image

OAuth Apps 메뉴 선택, New OAuth App 선택

image 정보 입력

image

callback url은 /auth/github/callback으로 하였다.

그러고 Register application 버튼을 눌러서 생성!

image

이 App에 들어가면 Client ID, Client secrets이 있다.

아직 Client secrets을 생성하지 않았다면 생성하고 기억해두자!

깃헙 로그인 페이지로 리다이렉트

이제 가장 먼저 할 일은 사용자를 깃험 로그인 페이지로 이동시키는 것이다.

// auth/auth.controller.ts
@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`,
	);
}

클라이언트에서 /auth/github/signin으로 요청을 보내면 깃헙 로그인 페이지로 리다이렉트 하도록 구현하였다.

쿼리 스트링으로 위에서 기억해둔 Client ID를 넘겨주고, 리소스 서버에서 사용할 기능을 scope에 담아서 보내준다.

이제 사용자가 깃헙 로그인 페이지에서 로그인을 하고 권한을 허가하면 우리는 Authorized Code를 얻을 수 있다.

Authorized Code를 이용해 깃헙 AccesToken, 유저 정보 받아오기

위에서 OAuth Apps를 생성할 때 설정한 callback url이 있다.

깃헙 로그인을 완료하면 Authorized Code를 쿼리 스트링에 담아 우리 서버에서 구현한 callback url로 GET 요청이 들어온다.

우리는 이 Authorized Code를 이용하는 기능을 구현하면 된다.

// auth/auth.controller.ts
@Get('github/callback')
async oauthGithubCallback(@Query('code') code: string) {
	await this.authService.oauthGithubCallback(code);
}

/auth/github/callback에 쿼리 스트링의 Authorized Code를 받아와 처리하는 기능을 구현하였다.

이 code를 바로 서비스에 넘겨주고 서비스에서 로직을 처리하고자 하였다.

그 전에 먼저 Authorized Code를 통해 깃헙 AccessToken을 받아오고, 그 토큰을 이용해 유저 정보를 가져오는 메소드를 구현하였다.

// utils/auth.util.ts

// AccessToken을 가져오는 메소드
export async function getGitHubAccessToken(authorizedCode: string) {
	const accessTokenResponse = await fetch(  // AccessToken을 받아오기 위해 깃헙에 fetch
		'https://github.com/login/oauth/access_token',
		{
			method: 'POST',  // POST 요청
			headers: {
				Accept: 'application/json',
				'Content-Type': 'application/json',
			},
			// OAuth Apps를 생성할 때 기억한 Client ID, Client SECRETS와 Authorized Code를 넘겨준다.
			body: JSON.stringify({  
				client_id: process.env.OAUTH_GITHUB_CLIENT_ID,
				client_secret: process.env.OAUTH_GITHUB_CLIENT_SECRETS,
				code: authorizedCode,
			}),
		},
	);

	if (!accessTokenResponse.ok) {  // 받아온 응답이 200 OK가 아니라면 에러 발생
		throw new InternalServerErrorException(
			'GitHub으로부터 accessToken을 받아오지 못했습니다.',
		);
	}

	const accessTokenData = await accessTokenResponse.json();
	return accessTokenData.access_token;  // 응답에서 AccessToken을 리턴
}

// 유저 정보를 가져오는 메소드
export async function getGithubUserData(accessToken: string) {
	const userResponse = await fetch('https://api.github.com/user', {  // 유저 정보를 받아오기 위해 깃헙에 fetch
		method: 'GET',  // GET 요청
		headers: {
			Authorization: `Bearer ${accessToken}`,  // Authorization Bearer 토큰으로 AccessToken을 넘겨준다.
		},
	});

	if (!userResponse.ok) {  // 받아온 응답이 200OK가 아니라면
		throw new InternalServerErrorException(
			'GitHub으로부터 유저 정보를 받아오지 못했습니다.',
		);
	}

	const userData = await userResponse.json();
	return {  // 응답에서 깃헙 닉네임을 리턴 (깃헙 닉네임을 username 으로 데이터베이스에 저장할 예정)
		username: userData.login,
	};
}

위 메소드들을 AuthService에서 사용하였다.

// auth/auth.service.ts
async oauthGithubCallback(code: string) {
	if (!code) {  // 만약 code가 넘어오지 않았다면 에러
		throw new BadRequestException('Authorized Code가 존재하지 않습니다.');
	}

	const accessToken = await getGitHubAccessToken(code);
	const githubUser = await getGithubUserData(accessToken);
	console.log(githubUser);
}

이렇게 진행을 해보면

{ "username": "~~~" }

출력이 잘 되었다!

유저 정보 받아온 후 처리

유저 정보를 받아온 후 어떻게 처리할 지 생각해보았다.

우리 서비스에서는 로그인 아이디로 필요한 username 외에도 nickname을 새로 받아온다.

즉, 깃헙 유저 정보로 받아온 username 외에도 닉네임을 새로 설정하여 회원 가입을 완료하여야 한다.

깃헙 로그인을 처음 했다면 클라이언트에서 “닉네임을 정해주세요”와 같은 모달을 띄워서 닉네임을 받아오고, 그 후 회원가입을 완료시키기로 정했다.

어떻게 할 지 고민하던 중 다음과 같은 방법으로 처리하였다.

  1. 깃헙 로그인을 하고 받아온 유저 정보를 이용해 데이터베이스를 조회해 회원 가입이 된 유저인지 판단한다.
  2. 회원가입이 된 유저라면 로그인 → JWT 발급
  3. 회원가입이 되지 않은 유저라면 유저 정보 기억, 닉네임 결정 후 회원 가입 마무리

그래서 짠 코드는 다음과 같다.

// auth/auth.service.ts
async oauthGithubCallback(authorizedCode: string) {
		if (!authorizedCode) {
			throw new BadRequestException('Authorized Code가 존재하지 않습니다.');
		}

		const gitHubAccessToken = await getGitHubAccessToken(authorizedCode);
		const gitHubUser = await getGitHubUserData(gitHubAccessToken);

		// GitHubUsername으로 데이터베이스 조회
		const user = await this.authRepository.findOneBy({  
			username: gitHubUser.username,
		});

		if (!user) {  // 만약 조회되는 유저가 없다면(회원가입을 완료하지 않은 유저라면)
			// Redis에 GitHubUsername을 키로하여 AccessToken 저장
			this.redisRepository.set(gitHubUser.username, gitHubAccessToken);
			return {  // username 정보 반환
				username: gitHubUser.username,
				accessToken: null,
				refreshToken: null,
			};
		}

		// 만약 조회되는 유저가 있다면(이미 가입을 완료한 깃헙 로그인 유저라면) 토큰 발급
		const [accessToken, refreshToken] = await Promise.all([
			createJwt(user, JwtEnum.ACCESS_TOKEN_TYPE, this.jwtService),
			createJwt(user, JwtEnum.REFRESH_TOKEN_TYPE, this.jwtService),
		]);

		this.redisRepository.set(user.username, refreshToken);

		return {  // 토큰 정보 반환
			username: null,
			accessToken,
			refreshToken,
		};
	}
}
// auth/auth.controller.ts
@Get('github/callback')
async oauthGithubCallback(
	@Query('code') authorizedCode: string,
	@Res({ passthrough: true }) res: Response,
) {
	const { username, accessToken, refreshToken } =  // Service 로직 결과 받아옴
		await this.authService.oauthGithubCallback(authorizedCode);
	if (username) {  // username 값이 있다면 (회원가입을 완료하지 않은 유저라면)
		res.cookie('GitHubUsername', username, {  // 쿠키에 GitHubUsername 저장
			path: '/',
			httpOnly: true,
		});
		return { username };
	}

	// username 값이 없다면 (이미 회원 가입을 완료한 유저라면) 쿠키에 JWT 저장
	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 };
}

위와 같이 회원 가입을 하지 않은 유저라면 쿠키에 GitHubUsername을 저장하도록 하였다.

이는 닉네임 설정 후 회원가입 요청을 보낼 때 사용할 예정이다.

그 후 새로운 메소드를 구현하였다.

// auth/auth.service.ts
async signUpWithGithub(nickname: string, GitHubUsername: any) {
	let gitHubUserData;
	try {  
		// GitHubUsername으로 Redis에서 GitHub AccessToken 가져옴
		const gitHubAccessToken = await this.redisRepository.get(GitHubUsername);
		// 토큰을 이용해 깃헙으로 다시 유저 정보 조회 요청
		gitHubUserData = await getGitHubUserData(gitHubAccessToken);
	} catch (e) {  // AccessToken이 잘못되어 유저 정보를 받아오지 못했다면 에러
		throw new UnauthorizedException('잘못된 접근입니다.');
	}
	
	// 새로 조회한 username과 쿠키에 저장되었던 username이 다르다면 에러
	if (gitHubUserData.username !== GitHubUsername) {
		throw new UnauthorizedException('잘못된 접근입니다.');
	}

	// 유저 정보가 일치한다면 Redis에 저장했던 AccessToken 정보 제거
	this.redisRepository.del(GitHubUsername);

	// 데이터베이스에 저장할 정보 생성
	const newUser = this.authRepository.create({
		username: GitHubUsername,
		password: uuid(),  // 비밀번호는 uuid
		nickname,  // 클라이언트에서 설정해 받아온 닉네임
	});

	const savedUser: User = await this.authRepository.save(newUser);  // 데이터베이스에 저장
	savedUser.password = undefined;
	// 패스워드 가리고 반환
	return savedUser;
}
// auth/auth.controller.ts
@Post('github/signup')
async signUpWithGithub(
	@Body('nickname') nickname: string,
	@Req() req,
	@Res({ passthrough: true }) res: Response,
) {
	let gitHubUsername;
	try {  // 만약 쿠키에 username 정보가 없다면 에러
		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;
}

이제 첫 깃헙 로그인 후 닉네임 설정을 완료하고 /auth/github/signup 으로 POST요청을 닉네임 정보를 담아 보내면 회원 가입이 완료된다!!

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally