안녕하세요! 오늘은 Nest.js에서 Github OAuth 구현하기를 주제로 글을 작성 해보도록 하겠습니다. 요즘에 Nest.js에 흥미를 느끼기도 해서 Nest.js 관련 글도 많이 올라갈 예정입니다.
OAuth는 흔히 여러사이트에서 페이스북, 카카오톡, 깃허브, 네이버로 로그인 하기 기능을 제공하는 방식으로서, 별도의 회원가입 없이 로그인을 제공하는 플랫폼의 아이디만 있으면 서비스를 이용 할 수 있습니다.
외부 서비스에서도 인증을 가능하게 하고 그 서비스의 API를 이용하게 해주는 것, 이것을 바로 OAuth라고 합니다. 아래의 사진은 OAuth의 원리를 보여주는 사진입니다.
(사진 출처: Showerbugs님 블로그)
위의 사진에서 가장 중요한 부분이 있다면, 인증토큰 전달 단계입니다. Application API에서 발급된 인증토큰은, 해당 유저의 정보를 조회하는데 쓰이며, API 서버로 전달해야 하는 값입니다.
이렇게 기본적인 OAuth의 원리를 알아보았고, 메인주제인 깃허브 OAuth를 구현하도록 하겠습니다.
Github Profile -> Developer Setting 탭으로 먼저 들어갑니다.
왼쪽의 탭에서 OAuth Apps 탭을 누르면, 아래의 화면을 볼 수 있습니다.
위의 화면에서 오른쪽에 있는 New OAuth App 버튼을 누르면 아래의 입력창들이 보입니다.
해당 입력창들은 아래의 양식이 있습니다.
Application name: 개발할 서비스의 이름
Homepage URL: 개발 서비스의 URL (리액트 환경에서는 locahost:3000 으로 하시면 됩니다.
Application description: 개발할 서비스에 대한 간단 설명
Authorization callback URL: OAuth를 진행하고나서, query string으로 보내지는 code를 받을 주소 (예: http://localhost:3000/github-login?code=12345678)
입력창들을 양식에 맞춰서 적은 다음, Register application 버튼을 눌러서 OAuth app을 생성해줍니다. 생성하고 나서 아래의 화면을 볼 수 있습니다.
가장 처음에 Generate a new client secret 버튼을 눌러서 Client secret 코드를 얻어줍시다.
총 두가지가 발급이 되는데, Client ID와 Client secrets가 발급이 됩니다. 이 두가지는 나중에 OAuth를 진행할 깃허브 링크로 이동하는데에 필수적인 요소들이며, 고유한값이므로 노출되지 않는것이 좋습니다.
이외에도 Homepage URL 및 Authorization callback URL들도 추후에 쓰이므로 중요합니다.
OAuth를 진행하기 위한 깃허브 세팅은 끝이났으며, 이제 Nest.js를 열어서 본격적으로 코드를 작성해보도록 하겠습니다.
Nest.js 프로젝트 세팅이 어느정도 되어있다고 가정을 하고 Controller와 Service부분 코드만 작성해주도록 하겠습니다.
코드를 작성하기 전, 필요한 node modules들을 설치해주도록 하겠습니다.
yarn add class-validator axios
그 다음으로 저는 user.dto.ts 파일에서 dto를 정의해주고 나서 user.serivce.ts 서비스 파일을 만들어서 코드를 먼저 작성해주겠습니다. 코드 각각에 주석을 달아 설명해놓았습니다.
// user.dto.ts
import { IsString } from 'class-validator';
export class GithubCodeDto {
@IsString()
readonly code: string;
}
import { Injectable } from '@nestjs/common';
import { CLIENT_ID, CLIENT_SECRET } from 'config/config.json';
import axios, { AxiosResponse } from 'axios';
// 깃허브에서 발급받은 CLIENT_ID와 CLIENT_SECRET을 config 파일에 저장하여 불러옵니다.
export interface IGithubUserTypes {
githubId: string;
avatar: string;
name: string;
description: string;
location: string;
}
@Injectable()
export default class UserService {
public async getGithubInfo(githubCodeDto: GithubCodeDto): Promise<IGithubUserTypes> {
const { code } = githubCodeDto;
// 웹에서 query string으로 받은 code를 서버로 넘겨 받습니다.
const getTokenUrl: string = 'https://github.com/login/oauth/access_token';
// 깃허브 access token을 얻기위한 요청 API 주소
const request = {
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
};
// Body에는 Client ID, Client Secret, 웹에서 query string으로 받은 code를 넣어서 전달해주어야 합니다.
const response: AxiosResponse = await axios.post(getTokenUrl, request, {
headers: {
accept: 'application/json', // json으로 반환을 요청합니다.
},
});
if (response.data.error) {
// 에러 발생시
throw new HttpError(401, '깃허브 인증을 실패했습니다.');
}
const { access_token } = response.data;
// 요청이 성공한다면, access_token 키값의 토큰을 깃허브에서 넘겨줍니다.
const getUserUrl: string = 'https://api.github.com/user';
// 깃허브 유저 조회 API 주소
const { data } = await axios.get(getUserUrl, {
headers: {
Authorization: `token ${access_token}`,
},
// 헤더에는 `token ${access_token} 형식으로 넣어주어야 합니다.`
});
const { login, avatar_url, name, bio, company } = data;
// 깃허브 유저 조회 API에서 받은 데이터들을 골라서 처리해줍니다.
const githubInfo: IGithubUserTypes = {
githubId: login,
avatar: avatar_url,
name,
description: bio,
location: company,
};
return githubInfo;
}
}
그 다음, user.controller.ts 컨트롤러 파일을 만들어서 request mapping을 해주도록 하겠습니다. 저는 아래와 같이 코드를 작성했어요.
import { Controller, Post, Body } from '@nestjs/common';
import UserService from './user.service';
import { GithubCodeDto } from './user.service';
@Controller('user')
export default class UserController {
constructor(
private readonly userService: UserService,
) {}
@Post('/github-info')
public async getGithubInfo(@Body() githubCodeDto: GithubCodeDto) {
const user = await this.userService.getGithubInfo(githubCodeDto);
return {
status: 200,
message: '깃허브 유저 정보를 조회하였습니다.',
data: {
user,
},
};
}
}
지금까지 Nest.js를 코딩해오면서 body에 정의되어있는 code 객체에 대해서 아직까지 아무런 흔적조차 안보였었는데요, 2번글의 OAuth app 정보에 작성했던 Homepage URL과 Authorization callback URL에 리액트 프로젝트 경로인, localhost:3000을 적어주었으니 리액트 프로젝트를 열어서 해당 URL로 테스트를 해보겠습니다.
간단하게 create-react-app oauth-test
처럼 리액트 프로젝트를 만들어줍시다.
아 그리고, 테스트를 진행할때도 위에서 발급받았던 Client ID를 가지고 있어야 합니다. 간단하게 Auth.tsx라는 컴포넌트를 만들어주고 나서 아래의 코드를 입력해줍니다.
import React, { useCallback } from 'react';
const Auth = (): JSX.Element => {
const CLIENT_ID: string = 'a9306dbf03e77c1cd343';
const REDIRECT_URL: string = 'http://localhost:3000/sign';
const onClick = useCallback((): void => {
const url: string = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URL}`;
// OAuth app을 등록할때 작성했던 redirect url과 발급받은 CLIENT_ID를 바탕으로 URL을 생성합니다.
window.open(url);
// 해당 URL로 이동하기
}, []);
return (
<button onClick={onClick}>
클릭클릭1
</button>
);
};
export default Auth;
그리고 리액트 라우터 경로에 callback URL인 /github-login 경로에 등록된 컴포넌트가 있어야 하므로, 임의의 컴포넌트도 하나 만들어줍시다. 저는 Token 이라고 네이밍을 했습니다.
import React from 'react';
const Token = (): JSX.Element => {
return (
<>
Hello!
</>
);
};
export default Token;
마지막으로 App.tsx 파일에서 리액트 라우터 설정을 해주도록 하겠습니다.
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Auth from './components/Auth';
import Token from './components/Token';
const App = (): JSX.Element => {
return (
<Switch>
<Route exact path='/' component={Auth} />
<Route exact path='/github-login' component={Token} />
</Switch>
);
}
export default App;
이제 yarn start를 입력하고 나서 리액트 프로젝트를 실행해보세요! 가장 처음에는 빈 화면에 클릭클릭1 이라는 버튼이 보일겁니다. 그 버튼을 클릭해주세요! 위에서 조합한 URL로 이동을 하게됩니다.
이런식으로 자신의 깃허브 아이디 / 이메일과 비밀번호를 입력받는 로그인 창이 뜨게됩니다. 모두 입력하고 나서 로그인을 진행해주세요.
로그인을 모두 끝내면 아래의 화면이 보일겁니다. 아까전에 만들었던 OAuth app에 대한 정보들과 인증 승인 버튼이 있습니다. Authorize 버튼을 눌러주세요!
OAuth app을 등록할때 설정해두었던 redirect URL을 통해 리다이렉트 되며, 해당 URL에는 code라는 쿼리 스트링이 생성됩니다. 해당 code를 postman 테스팅 도구를 이용하여 방금 만든 Nest.js 서버로 요청을 보내보도록 하겠습니다.
Nest.js 서버를 실행해주세요!
자기 자신서버의 URL경로를 주소에 입력을 해주고, 메소드 방식은 POST로 보냅니다.
body에는 방금 웹에서 쿼리스트링 code로 받은 문자열을 넣어줍니다.
해당 code는 친구의 깃허브 계정을 빌려서 발급받았습니다.
웹에서 받은 code의 빠른 만료시간 때문에 401 오류를 반환할 수도 있습니다.
이제 Send 버튼을 눌러서 테스트를 해보도록 하겠습니다! 혹여나 401 오류가 뜨시는 분은 다시 code를 웹에서 발급받아서 넣어주시면 해결됩니다.
3번글 Nest 서버를 짤때, 추출한 데이터들인 githubId, avatar, name, description, location 등등, 올바르게 응답됨을 확인할 수 있었습니다.
추후에 서버에서는 해당 깃허브 유저 데이터들을 바탕으로 로그인 및 회원가입을 데이터베이스에 맞게 로직을 설계하여 OAuth를 구현 할 수 있습니다.
이번에 깃허브를 이용한 OAuth를 처음으로 진행해보았는데, 이전에 잠깐 다뤄봤던 Google OAuth2와 유사하다는 느낌이 들어서 어렵다고 느끼진 않았습니다. 그래서 앞으로 프로젝트를 진행할때도 OAuth 방식을 이용하여 로그인 / 회원가입을 진행하는게 사용자 입장에서 굉장히 편할것 같아서 많은 도입 예정입니다.
이상으로 글을 마치도록 하겠습니다. 궁금한점이 생기시면 댓글로 남겨주세요! 긴 글 읽어주셔서 감사합니다!! 😀😁