-
Notifications
You must be signed in to change notification settings - Fork 2
[준섭] 1122(수) 개발기록 ‐ 깃헙 로그인 구현
일단 깃허브에 들어가서 개인 Settings
메뉴에 들어간다.
그 후 맨 밑 Developer settings
메뉴 선택
OAuth Apps
메뉴 선택, New OAuth App
선택
정보 입력
callback url은 /auth/github/callback
으로 하였다.
그러고 Register application
버튼을 눌러서 생성!
이 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
를 얻을 수 있다.
위에서 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 외에도 닉네임을 새로 설정하여 회원 가입을 완료하여야 한다.
깃헙 로그인을 처음 했다면 클라이언트에서 “닉네임을 정해주세요”와 같은 모달을 띄워서 닉네임을 받아오고, 그 후 회원가입을 완료시키기로 정했다.
어떻게 할 지 고민하던 중 다음과 같은 방법으로 처리하였다.
- 깃헙 로그인을 하고 받아온 유저 정보를 이용해 데이터베이스를 조회해 회원 가입이 된 유저인지 판단한다.
- 회원가입이 된 유저라면 로그인 → JWT 발급
- 회원가입이 되지 않은 유저라면 유저 정보 기억, 닉네임 결정 후 회원 가입 마무리
그래서 짠 코드는 다음과 같다.
// 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요청을 닉네임 정보를 담아 보내면 회원 가입이 완료된다!!
© 2023 debussysanjang
- 🐙 [가은] Three.js와의 설레는 첫만남
- 🐙 [가은] JS로 자전과 공전을 구현할 수 있다고?
- ⚽️ [준섭] NestJS 강의 정리본
- 🐧 [동민] R3F Material 간단 정리
- 👾 [재하] 만들면서 배우는 NestJS 기초
- 👾 [재하] GitHub Actions을 이용한 자동 배포
- ⚽️ [준섭] 테스트 코드 작성 이유
- ⚽️ [준섭] TypeScript의 type? interface?
- 🐙 [가은] 우리 팀이 Zustand를 쓰는 이유
- 👾 [재하] NestJS, TDD로 개발하기
- 👾 [재하] AWS와 NCP의 주요 서비스
- 🐰 [백범] Emotion 선택시 고려사항
- 🐧 [동민] Yarn berry로 모노레포 구성하기
- 🐧 [동민] Vite, 왜 쓰는거지?
- ⚽️ [준섭] 동시성 제어
- 👾 [재하] NestJS에 Swagger 적용하기
- 🐙 [가은] 너와의 추억을 우주의 별로 띄울게
- 🐧 [동민] React로 멋진 3D 은하 만들기(feat. R3F)
- ⚽️ [준섭] NGINX 설정
- 👾 [재하] Transaction (트랜잭션)
- 👾 [재하] SSH 보안: Key Forwarding, Tunneling, 포트 변경
- ⚽️ [준섭] MySQL의 검색 - LIKE, FULLTEXT SEARCH(전문검색)
- 👾 [재하] Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- 👾 [재하] NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
- 2주차(화) - git, monorepo, yarn berry, TDD
- 2주차(수) - TDD, e2e 테스트
- 2주차(목) - git merge, TDD
- 2주차(일) - NCP 배포환경 구성, MySQL, nginx, docker, docker-compose
- 3주차(화) - Redis, Multer 파일 업로드, Validation
- 3주차(수) - AES 암복호화, TypeORM Entity Relation
- 3주차(목) - NCP Object Storage, HTTPS, GitHub Actions
- 3주차(토) - Sharp(이미지 최적화)
- 3주차(일) - MongoDB
- 4주차(화) - 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- 4주차(수) - 코드 개선, 트랜잭션 제어
- 4주차(목) - 트랜잭션 제어
- 4주차(일) - docker 이미지 최적화
- 5주차(화) - 어드민 페이지(전체 글, 시스템 정보)
- 5주차(목) - 감정분석 API, e2e 테스트
- 5주차(토) - 유닛 테스트(+ mocking), e2e 테스트(+ 파일 첨부)
- 6주차(화) - ERD
- 2주차(화) - auth, board 모듈 생성 및 테스트 코드 환경 설정
- 2주차(목) - Board, Auth 테스트 코드 작성 및 API 완성
- 3주차(월) - Redis 연결 후 RedisRepository 작성
- 3주차(화) - SignUpUserDto에 ClassValidator 적용
- 3주차(화) - SignIn시 RefreshToken 발급 및 Redis에 저장
- 3주차(화) - 커스텀 AuthGuard 작성
- 3주차(수) - SignOut시 토큰 제거
- 3주차(수) - 깃헙 로그인 구현
- 3주차(토) - OAuth 코드 통합 및 재사용
- 4주차(수) - NestJS + TypeORM으로 MySQL 전문검색 구현
- 4주차(목) - NestJS Interceptor와 로거
- [전체] 10/12(목)
- [전체] 10/15(일)
- [전체] 10/30(월)
- [FE] 11/01(수)~11/03(금)
- [전체] 11/06(월)
- [전체] 11/07(화)
- [전체] 11/09(목)
- [전체] 11/11(토)
- [전체] 11/13(월)
- [BE] 11/14(화)
- [BE] 11/15(수)
- [FE] 11/16(목)
- [FE] 11/19(일)
- [BE] 11/19(일)
- [FE] 11/20(월)
- [BE] 11/20(월)
- [BE] 11/27(월)
- [FE] 12/04(월)
- [BE] 12/04(월)
- [FE] 12/09(금)
- [전체] 12/10(일)
- [FE] 12/11(월)
- [전체] 12/11(월)
- [전체] 12/12(화)