Project/[SAFU] 1st Project

9. [Client & Server] Social Login (Oct 30)

HJChung 2020. 11. 6. 18:10

 우리 서비스(SAFU)는 '깃헙으로 로그인하기(간편 로그인 기능)'을 제공한다. 

HJ님과 나를 아주 맘고생시켰던 깃헙 소셜로그인..ㅎㅎ 특히 client가 사용하는 주소(port:3000)와 server가 사용하는 주소(port:4000)이 달라서 리다이렉트 로직이 너무 헷갈려서 힘들었다. 

예전에 OAuth2와 소셜로그인에 대해서 블로그에 정리한 적이 있었지만 백지상태에서 처음부터 구현하려고 하니 결국 내가 아무것도 모르고 있는 상태였구나를 깨달았다.

그 포스팅과 이번에 구현해본 코드를 통해 여기에 내가 이해한 바를 다시 한번 자세히 정리해보기로 했다. 

1. OAuth2의 이해

libertegrace.tistory.com/entry/40-Authentication-OAuth-20

 

40. Authentication - OAuth 2.0

1. OAuth 소개 앞서 회원가입/로그인/로그아웃 등을 express와 session 개념을 사용해서 구현해보았다. 그러나 우리가 편하게 사용하고 있는 소셜로그인( 카카오 아이디로 로그인, 구글 아이디로 로그

libertegrace.tistory.com

여기에 정리해 놓았듯이 OAuth를 사용하면 '내가 사용하고 싶은 앱/웹'과 '로그인을 중개해주는 카카오, 구글 같은 소셜'간의 서비스를 안전하게 상호작용하며 사용할 수 있다.

 

어떻게??

'카카오, 구글 같은 소셜'이 '내가 사용하고 싶은 앱/웹'에게 '나'의 아이디와 비번을 그대로 제공하는 것이 아니라 AccessToken의 형태로 발급하여 전달한다.

Access Token의 장점이 뭐길래?

  • 반복된 말이지만, 일단 쌩판 동일한 아이디, 비번을 제공하는게 아니고
  • '카카오 구글 같은 소셜'의 모든 기능이 아니라 그 중 '내가 사용하고 싶은 웹/앱' 에 꼭 필요한 기능만 부분적으로 접근허용할 수 있다. 

==> 그러니까 한마디로 OAuth는 사용자가 사용하려는 웹사이트에 타 서비스의 동일한 아이디와 비밀번호를 제공하지 않고도 인증을 통해 (Authentication) 정보에 대한 접근 권한을 일정부분 부여(Authorization)할 수 있는 수단으로 사용되는, 접근 위임을 위한 개방형 표준이다. 

 

2. 역할 및 용어

OAuth를 통한 인증(Authentication)과 허가(Authorization)에 관여하는 역할을 아래의 3(4)가지로 구분 할 수 있다.

  •  Resource Owner  = 내 서비스를 사용할 사용자
  •  Resource Server = 리소스, 서비스 제공자 (구글, 깃헙, 페이스북) : 데이터를 가지고 있는 서버
    (Authorizationn Server = 인증과 관련된 처리를 전담하는 서버 ; 이지만 이를 Resource Server에 포함시켜서 정리해볼 것이다. )
  •  Client  = 서드 파티 서비스 (내 서비스, 내가 구현할 어플리케이션, 즉, SAFU)

3. OAuth와 소셜로그인

소셜 로그인은 OAuth를 통해 깃헙..등(Resource Server)에게 자신(Resource Owner)임을 인증(Authentication)하고

SAFU(Client)에게 자신의 정보를 사용할 수 있는 허가(Authorization)을 내어준다.

이렇게 허가가 나서 인증자의 정보를 받아 가입한 계정에 저장을 하면 연동이 된다.

 

이후 SAFU(Client)에 연동된 계정으로 로그인 시도 시(소셜 로그인 시도 시), 서비스 제공자로 부터 재확인이 완료되면, 이 과정이 바로 소셜 로그인으로 로그인이 완료 된 것이다.

 

 

※ 우리가 만들 서비스의 일반 회원가입 과정에서 필요한 정보는 (아이디로서 역할을 하는)이메일, 비밀번호, 깃헙아이디 이다. 

깃헙아이디는 이메일(즉, 아이디)를 찾을 때 필요하고, 이메일은 비밀번호를 찾을 때 해당 비밀번호를 보내주는 주소이기 때문에 사용한다. 

이러한 서비스의 백엔드 로직을 가지고 있기때문에 소셜 로그인을 한 이후 또 이메일을 추가요청하는 가입과정을 추가해야하나 라는 논의도 있었다. 그러나 결국 시간관계상(핑계ㅠ) 소셜로그인으로 가입한 사용자의 경우 깃헙에서 제공하는 API를 통해 정보를 받아 DB에 저장을 하는 식으로 회원 계정을 생성하고   아이디 찾기/비번 찾기/개인정보수정 에 접근을 못하게 하는 것으로 해주기로 하고 그냥 회원가입과 로그인이 한번에 되는 것으로 소셜로그인을 구현하였다. 

 

4. 소셜로그인 구현 과정 ☆

1) Github의 OAuth를 사용하기 위해서는 먼저 등록이 필요하다. 

우리가 사용할 Resource Server는 Github이므로, Github Developer settings 에서 우리 서비스를 등록한다. 

 

그러면 

  • Client ID: 우리 서비스(S*FU)인 Client를 식별 할 수 있는 ID
  • Client Secret: 우리 서비스(S*FU)인 Client를 식별 할 수 있는 PW (절대, 코드에 노출되면 안되는 정보, 보안 이슈 심각)
  • Homepage URL: SAFU application의 URL
  • Authorization callback URL: Resource Server가 권한을 부여하는 과정에서 Authorized Code를 전달해 줄 경로, (' Authorized rediret URIs로 Authorized Code 전달해줘~')로 사용자가 Github 로그인을 하면 redirect될 페이지의 주소이다. 

이 HomepageURL과 Authorization callback URL을 무엇으로 할 지가 OAuth 개념을 완벽히 이해하지 못할 때는 무척이나 헷갈렸다. 

이게 무슨 의미인지는 client와 server 코드를 보며 이해해보도록 하자. 

 

2) Client 코드

<div className="social-login-div">
      <a href={'http://localhost:4000/auth/github'}>Log in with Github</a>
</div>

0️⃣ 사용자가 Log in with Github 이라는 버튼을 누르면 href에 있는 링크로 가게 된다.

 

3) 사용자 인증 및 접근 권한 받기

1️⃣ 'http://localhost:4000/auth/github' 에 라우팅되어있는 서버 코드는 아래와 같다.

즉,

`https://github.com/login/oauth/authorize?client_id=${github.clientID}&redirect_uri=${Authorization callback URL}`

이러한 형식의 github주소로 리다이렉트 된다. 

const github = {
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
  redirectUri: 'http://localhost:4000/auth/github/callback',
};

module.exports = {
  get: (req, res) => {
    const githubAuthUrl =
      'https://github.com/login/oauth/authorize?client_id=' +
      github.clientID +
      '&redirect_uri=' +
      github.redirectUri;

    res.redirect(githubAuthUrl);
  },
};

 

2️⃣ 그러면 github(Resource Server)은 사용자(Resource Owner)가 github에 로그인 되어있는지를 확인하고 

=> 되어있지 않다면, 로그인을 하라는 화면을 보여준다. 

=> 되어 있다면,

      3️⃣ => github는 github.clientIDAuthorization callback URL(=redirectUri)이 위에서 등록된 정보와 같은지 확인한다. 

           =>  같지 않다면, 거기서 끝내버리고

          4️⃣ => 같다면 서비스 접근 허가를 허용하는지 확인하는 메세지를 전송한다. 

 

5️⃣ 사용자(Resource Owner)가 허용한다는 버튼을 누르면 Github는 이 사용자는 github.clientID 를 가진 서비스(Client) 에게 로그인을 허용함 이라고 저장한다.

 

이렇게 소셜 로그인은 OAuth를 통해 깃헙..등(Resource Server)에게 자신(Resource Owner)임을 인증(Authentication)하고 SAFU(Client)에게 자신의 정보를 사용할 수 있는 허가(Authorization)을 내어주는 과정이 끝났다. !!         

 

4) Resource Server이 승인하는 과정

위의 과정에서 사용자(Resource Owner)의 인증과 허가가 되었으니 깃헙..등(Resource Server)가 자신이 보유한 데이터에 SAFU(Client)가 접근하는 것을 승인을 해 줄 차례이다.

 

6️⃣ 5번의 과정에서 '허용'을 하면 Github는 Authorization Code를 응답의 header의 location에 Authorization callback URL?code=[Authorization Code] 를 주어서 여기로 리다이렉트 된다. 

※ 우리는 Authorization callback URL로 http://localhost:4000/auth/github/callback 즉 서버의 라우팅 주소를 주었다. 

왜냐하면 소셜 로그인 후 서버에서 처리해 줘야 할 일(DB에 사용자 정보 저장, 세션 생성 등) 이 있기 때문이다!

 

그렇게 SAFU(Client)의 서버는 redirect URL 뒤에 params 형태로 담긴 authorization code를 알게 되는 것이다. 

 

7️⃣ [Server] server는 이 주소(자신의 주소)뒤에 code가 query로 붙어 있는 것을 가지고!

`https://github.com/login/oauth/access_token?client_id=${github.clientID}&client_secret=${github.clientSecret}&code=${code}`,

위 형식의 주소로 Resource Server에 직접 POST요청을 보내 Github에 자신이 바로 이 사용자가 Github에 접근을 허용한 바로 그 서비스라는 것을 알린다.

어떻게?

`https://github.com/login/oauth/access_token?client_id=${github.clientID}&client_secret=${github.clientSecret}&code=${code}`;이 형식의 주소에 그 code를 query형태로 동봉하여서! post 요청을 한다!(그러니까, safu의 server가 github server에게 post 요청하니까 이땐 마치 client가 되어버린 기분..)

이에 관련된 서버 코드는 아래와 같다.

app.get('/auth/github/callback', async (req, res) => {
  const { session, query } = req;
  const { code } = query;
  console.info('==== session ====requestCode====');
  console.log(session, code);
  try {
    const access_token = await axios({
      method: 'POST',
      url: `https://github.com/login/oauth/access_token?client_id=${github.clientID}&client_secret=${github.clientSecret}&code=${code}`,
      headers: {
        'content-type': 'application/json',
      },
    });

5) Access Token의 발급!

8️⃣ Github은 이 주소에 있는 github.clientID, github.clientSecret, code 가 자신이 5️⃣ 에서 저장한 정보와 맞는지 대조한다. (비동기)

.then((맞으면)=> {response응답에 access_token=5ccd906cccdd4bee5435ed36891ceaf87840b32f&scope=&token_type=bearer 형태로 데이터를 담아준다. (res.data를 콘솔에 찍어보면 됨.)})

console.log(`response.data:${access_token.data}`);
let access_token_split = access_token.data.split('&')[0].split('=')[1];
console.log(`access_token:${access_token_split}`);

6) 이제 Access Token을 활용해서 API를 통해 Resource Server의 리소스를 사용 할 수 있다 :))

9️⃣ 이제 safu의 server는 github의 access_token을 가졌으니, github의 API문서 형식에 따르기만 하면 어떠한 정보도 받아올 수 있다. https://api.github.com/user의 API 주소에 req의 header에 access_token을 담아서 get 요청!(그러니까, safu의 server가 github server에게 get 요청하니까 이땐 마치 client가 되어버린 기분..)

try {
        const userResponse = await axios({
          method: 'GET',
          url: 'https://api.github.com/user',
          headers: {
            Authorization: `token ${access_token_split}`,
          },
        });
        console.log('social login result:', userResponse.data);

그러면

이런 식의 사용자 정보를 GET 할 수 있다.! ㅎㅎ

 

🔟 이렇게 받은 정보를 소셜 로그인 후 서버에서 처리해 줘야 할 일인 정보를 받아 DB에 저장을 하는 식으로 회원 계정을 생성하고, 로그인이 되었으니 세션도 생성하는 작업을 진행한다. 

물론 이러한 비동기 처리가 정상적으로 모두 완료 되었을 때!

 

res.redirect(클라이언트 주소)로 세션을 보유한채 리다이렉트가 된다.! 그러면 사용자는 SAFU 서비스의 로그인 된 Main 페이지로 리다이렉팅 되겠다.

try {
        const userResponse = await axios({
          method: 'GET',
          url: 'https://api.github.com/user',
          headers: {
            Authorization: `token ${access_token_split}`,
          },
        });
        console.log('social login result:', userResponse.data);
        users
          .findOrCreate({
            where: { email: userResponse.data.id },
            defaults: {
              password: hash(userResponse.data.node_id),
              githubId: userResponse.data.login,
              kind_login: 'social',
            },
          })
          .then(([result]) => {
            session.userid = result.dataValues.id;
            console.log('session:', session);
            res.redirect('http://localhost:3000/');
          })
          .catch((err) => {
            console.log('err');
          });
      } catch (error) {
        res.status(500);
      }

 

 

지금까지의 0️⃣ ~ 🔟의 과정을 정리하면 아래와 같다... 

//1. [Client] client ------https://github.com/login/oauth/authorize?client_id=${github.clientID}&redirect_uri=${Authorization callback URL(=redirectUri)}--------> github
//2. [Github] github.clientID와 Authorization callback URL(=redirectUri)확인 -------> 혀용 여부를 물어보는 창을 띄움 -----> 허용 버튼 클릭시-----> Github는 이 유저는 github.clientID 를 가진 서비스에게 로그인을 허용함 이라고 저장 후 ~
//3. [Github] github가 -------Authorization callback URL(=redirectUri)?code=d9d5132cfcdaa242c0c8을 응답의 header넣어서 응답해줌-------------> 그러면 여기로 유저는 리다이렉트 됨. (결국 github을 통해 safu server의 Authorization callback URL(=redirectUri) 주소로 get 요청을 보낸 껏과 마찬가지)
//4-1. [Server] server는 이 주소(자신의 주소)뒤에 code가 query로 붙어 있는 것을 가지고! ---> github에 자신이 바로 이 유저가 github에 접근을 허용한 바로 그 서비스라는 것을 알린다.
// 어떻게? `https://github.com/login/oauth/access_token?client_id=${github.clientID}&client_secret=${github.clientSecret}&code=${code}`;이 형식의 주소에 그 code를 query형태로 동봉하여서! post 요청을 한다!(그러니까, safu의 server가 github server에게 post 요청하니까 이땐 마치 client가 되어버린 기분..)
// 비동기 1) [Github] 이 주소에 있는 github.clientID, github.clientSecret, code 가 자신이 2. 에서 저장한 정보와 맞는지 대조한다.
// .then((맞으면)=> {response응답에 access_token=5ccd906cccdd4bee5435ed36891ceaf87840b32f&scope=&token_type=bearer 형태로 데이터를 담아준다. (res.data를 콘솔에 찍어보면 됨.)})
// 4-2. [Server] 이제 safu의 server는 github의 access_token을 가졌으니, github의 API문서 형식에 따르기만 하면 어떠한 정보도 받아올 수 있다. https://api.github.com/user 여리고 req의 header에 access_token을 담아서 get 요청!(그러니까, safu의 server가 github server에게 post 요청하니까 이땐 마치 client가 되어버린 기분..)
// 비동기 2) 데이터 준비
// .then((데이터 응답오면)=> res.data 출력해보면 json 형태로 잘 나온다. )
// 4-3. [Server] 그럼 우리는 여기서 이 4-2의 결과로 받아온 데이터 중 필요한 것을 DB에 저장하고,
// 비동기 3) sequelize 비동기 처리
// .then((저장 잘 되었으면 )
// 4-4. [Server] isLogin=true가 포함된 getReviews API를 응답으로 주면서 res.redirect('[safu의 client 주소]/');)
// 4-5. 그러면 [Client]는 로그인 된 Main 페이지로 리다이렉팅 되겠다.

 

이것으로 우리 서비스의 소셜로그인은 정상 작동을 하게 되었다. 

SAFU 서비스의 Client와 Server 그리고 Github가 서로 막 통신을 하는 것이고 게다가 소셜 로그인이 완료되면 DB저장까지..

비동기 처리가 매우 많다. 

try,catch 문 .then 등의 구문도 주의깊게 쓰는 것이 중요하기도 했다.

 

물론 일반 회원가입을 할 때 githubID가 필수조건이면서 Ggithub 소셜로그인을 할 수 있다는 거 자체가 웃기기도 하고

여전히 DB에 저장되는 데이터가 이메일과 비밀번호가 아니라는 점 등의 허술한 것이 있지만 

끝까지 포기하지 않는 HJ님께 학습태도를 정말 많이 배웠으며 그에 힘입어 12시까지 함께 zoom으로 서로 방법을 고민해보면서도 피곤한줄 몰랐던 날들이 있었다.

 

정말 잊지 못할 교훈과 시간들이었다. 

 

reference

devhaks.github.io/2019/05/31/oauth2/

 

카카오 로그인 연동을 통한 OAuth2 이해하기

OAuth2 를 이해하기 위해 카카오 로그인 연동을 통해 설명합니다.

devhaks.github.io