GitHub 소셜 로그인 프로세스 분석

henry·2025년 1월 21일

전체 흐름

1. 클라이언트의 로그인 요청:

  • 사용자가 /auth/github 엔드포인트를 호출합니다.
  • 서버는 사용자를 GitHub 로그인 페이지로 리다이렉트합니다.

2. 사용자가 GitHub에서 로그인:

  • GitHub에서 로그인하고 앱 권한 승인을 완료합니다.


3. Authorization Code 수신:

  • 권한 승인이 완료되면, GitHub는 Authorization Code를 포함하여 /auth/github/callback?code=abcd1234로 응답합니다.

4. Access Token 발급:

  • 서버는 수신한 Authorization Code를 사용해 GitHub에 요청을 보냅니다.
  • GitHub는 요청에 대한 응답으로 Access Token을 반환합니다.

5. 사용자 정보 조회:

  • 서버는 Access Token을 사용해 GitHub API를 호출하여 사용자의 프로필 정보와 이메일을 가져옵니다.

6. JWT 생성:

  • 조회한 사용자 정보를 기반으로 JWT를 생성합니다.
  • 이 JWT는 클라이언트가 인증 상태유지하는 데 사용됩니다.

7. 응답 반환:

  • 서버는 생성된 JWT와 사용자 정보를 클라이언트에 반환합니다.



1. 라우트 설정 (authRoutes.ts)


  • 소스 코드

    import express from 'express';
    import {
      redirectToGitHub,
      handleGitHubCallback,
    } from '../controllers/authController';
    
    const router = express.Router();
    
    router.get('/github', redirectToGitHub);
    router.get('/github/callback', handleGitHubCallback);
    
    export default router;

분석


1.1 express 모듈 및 컨트롤러 임포트


import express from 'express';
import { redirectToGitHub, handleGitHubCallback } from '../controllers/authController';
  • express
    • Node.js의 웹 프레임워크로, 라우팅 및 미들웨어 설정
  • redirectToGitHub, handleGitHubCallback
    • 컨트롤러 함수로, GitHub 로그인 리다이렉트, 콜백 처리를 각각 담당

1.2 라우트 설정

router.get('/github', redirectToGitHub);
router.get('/github/callback', handleGitHubCallback);
  • /github
    • 사용자가 GitHub로 리다이렉트되도록 설정.
  • /github/callback
    • GitHub가 인증 후 응답을 보낼 경로.


2. 컨트롤러


  • 소스 코드
import { Request, Response } from 'express';
import axios from 'axios';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import { BadRequestError } from '../errors/httpError';

dotenv.config();

const {
  GITHUB_CLIENT_ID,
  GITHUB_CLIENT_SECRET_KEY,
  GITHUB_CALLBACK_URL,
  JWT_SECRET_KEY,
} = process.env;

export const redirectToGitHub = (req: Request, res: Response): void => {
  const redirectURI = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${GITHUB_CALLBACK_URL}&scope=user:email`;
  res.redirect(redirectURI);
};

export const handleGitHubCallback = async (
  req: Request,
  res: Response
): Promise<void> => {
  const code = req.query.code as string;

  if (!code) {
    throw new BadRequestError();
  }

  try {
    const tokenResponse = await axios.post(
      'https://github.com/login/oauth/access_token',
      {
        client_id: GITHUB_CLIENT_ID,
        client_secret: GITHUB_CLIENT_SECRET_KEY,
        code,
      },
      { headers: { Accept: 'application/json' } }
    );

    const accessToken = tokenResponse.data.access_token;

    if (!accessToken) {
      throw new Error('액세스 토큰을 얻지 못했습니다');
    }

    const userResponse = await axios.get('https://api.github.com/user', {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    const userEmailResponse = await axios.get(
      'https://api.github.com/user/emails',
      {
        headers: { Authorization: `Bearer ${accessToken}` },
      }
    );

    const userInfo = {
      id: userResponse.data.id,
      username: userResponse.data.login,
      email: userEmailResponse.data.find((email: any) => email.primary)?.email,
    };

    if (!userInfo.email) {
      throw new Error('이메일을 찾을 수 없습니다');
    }

    const token = jwt.sign(userInfo, JWT_SECRET_KEY!, { expiresIn: '1h' });

    res.status(200).json({
      message: 'GitHub 로그인 성공',
      token,
      user: userInfo,
    });
  } catch (error) {
    const errorMessage =
      error instanceof Error ? error.message : 'Unknown error occurred';
    res.status(500).json({
      error: 'GitHub OAuth Failed',
      details: errorMessage,
    });
  }
};

분석


2.1 환경 변수 로드

dotenv.config();

const {
  GITHUB_CLIENT_ID,
  GITHUB_CLIENT_SECRET,
  GITHUB_CALLBACK_URL,
  JWT_SECRET_KEY,
} = process.env;
  • GITHUB_CLIENT_ID : GitHub OAuth 클라이언트 ID
  • GITHUB_CLIENT_SECRET_KEY : 시크릿 키.
  • GITHUB_CALLBACK_URL : GitHub 인증 후 응답을 받을 콜백 URL.
  • JWT_SECRET_KEY : JWT 토큰 서명에 사용할 암호 키.

2.2 리다이렉트 처리

  • 사용자가 깃허브 로그인을 할 수 있도록 로그인 페이지로 리다이렉트
export const redirectToGitHub = (req: Request, res: Response): void => {
  const redirectURI = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}
						&redirect_uri=${GITHUB_CALLBACK_URL}&scope=user:email`;
  res.redirect(redirectURI);
};
  • https://github.com/login/oauth/authorize : GitHub OAuth 엔드포인트.
  • client_id : GitHub OAuth 앱의 클라이언트 ID.
  • redirect_uri : 인증 후 응답을 받을 서버 측 콜백 URL.
  • scope=user:email : 사용자의 이메일 접근 권한을 요청.
  • res.redirect(redirectURI) : 클라이언트를 GitHub OAuth 인증 페이지로 리다이렉트.




3. GitHub 콜백(handleGitHubCallback)


분석


3.1 요청 파라미터 추출

  • 깃허브 로그인 요청 파라미터
    • http://localhost:5000/auth/github/callback?code=d20917d34b6684f443a4
const code = req.query.code as string;

if (!code) {
  res.status(400).json({ error: 'Authorization code is missing' });
  return;
}
  • req.query.code
    • GitHub가 전달한 Authorization Code.
  • 검증
    • Authorization Code가 없을 경우, 400 에러를 반환합니다.

3.2 Access Token 요청

const tokenResponse = await axios.post(
  'https://github.com/login/oauth/access_token',
  {
    client_id: GITHUB_CLIENT_ID,
    client_secret: GITHUB_CLIENT_SECRET,
    code,
  },
  { headers: { Accept: 'application/json' } }
);
  • GitHub OAuth 엔드포인트

    • https://github.com/login/oauth/access_token
      • Access Token을 발급하기 위해 요청하는 URL
  • 전송 데이터

    • client_id

      • GitHub 앱의 클라이언트 ID.
    • client_secret_key

      • GitHub 앱의 시크릿 키.
    • code

      • GitHub가 전달한 Authorization Code.
    • headers: { Accept: 'application/json' }

      • HTTP 요청 헤더를 설정하는 부분

      • GitHub OAuth API에 요청할 때 응답 데이터 형식지정하는 역할

        • 헤더를 설정하지 않은 경우:

          POST https://github.com/login/oauth/access_token
          • application/x-www-form-urlencoded 형식의 응답
          access_token=abcdef12345&scope=user&token_type=bearer
        • 헤더를 설정한 경우

          POST https://github.com/login/oauth/access_token
          Accept: application/json
          • 요청 헤더가 JSON 응답을 요구하면, GitHub는 아래와 같이 JSON 형식으로 반환:
          {
            "access_token": "abcdef12345",
            "scope": "user",
            "token_type": "bearer ..."
          }
          						```
  • 응답 객체

    • tokenResponse.data.access_token
      • 발급받은 Access Token.

3.3 사용자 정보 요청

const userResponse = await axios.get('https://api.github.com/user', {
  headers: { Authorization: `Bearer ${accessToken}` },
});
  • https://api.github.com/user
    • GitHub 사용자 정보 API.
  • Authorization 헤더
    • Bearer ${accessToken}
      • 발급받은 Access Token을 통해 인증.

3.4 사용자 이메일 정보 요청

const userEmailResponse = await axios.get(
  'https://api.github.com/user/emails',
  {
    headers: { Authorization: `Bearer ${accessToken}` },
  }
);
  • https://api.github.com/user/emails
    • 사용자의 이메일 정보 API.
  • primary 필드
    • email.primary가 true인 이메일이 기본 이메일

3.5 사용자 정보 추출

const userInfo = {
  id: userResponse.data.id,
  username: userResponse.data.login,
  email: userEmailResponse.data.find((email: any) => email.primary)?.email,
};

if (!userInfo.email) {
  throw new Error('Primary email not found');
}
  • 사용자 정보:
    • id : GitHub 사용자 고유 ID.
    • username : 사용자명.
    • email : 기본 이메일.
  • 검증:
    • 기본 이메일이 없을 경우, 에러를 발생

3.6 JWT 생성

const token = jwt.sign(userInfo, JWT_SECRET_KEY!, { expiresIn: '1h' });
  • jwt.sign(payload, secret, options):
    • payload : JWT에 저장할 사용자 정보 (userInfo).
    • secret : JWT 서명 비밀 키.
    • options
      • expiresIn: '1h': JWT 유효기간 1시간.

3.7 응답 반환

res.status(200).json({
  message: 'GitHub 로그인 성공',
  token,
  user: userInfo,
});
  • 응답 데이터:
    • message: 성공 메시지.
    • token: 발급된 JWT.
    • user: 사용자 정보 (id, username, email).

3.8 에러 처리

} catch (error) {
  console.error('GitHub OAuth Error:', error);
  res.status(500).json({ error: 'GitHub OAuth Failed' });
}
  • 에러 발생 시
    • 로그로 에러를 기록하고, 500 상태 코드를 반환합니다.


0개의 댓글