Skip to content

Authorization Code Grant 방식으로 Github OAuth 인증 구현

yejineee edited this page Nov 8, 2020 · 1 revision

OAuth란?

OAuth 2.0은 다양한 플랫폼 환경에서 권한 부여를 위한 산업 표준 프로토콜입니다.

(정의) 제 3의 앱이 자원의 소유자인 서비스 이용자를 대신하여 서비스를 요청할 수 있도록 자원 접근 권한을 위임하는 방법 출처 : 금융보안원 "OAuth 2.0 개요 및 보안 고려사항" 보안연구부-2015-030

Authorization Code Grant 방식이란?

  • 인증 코드로 access token 을 얻는 방식이다.

    → 보안에 더 효과적!

  • 웹과 모바일 앱에서 가장 많이 사용된다.

  • 코드를 교환하는 단계에서 access token은 항상 보호된 back channel에서 응용 프로그램과 OAuth 서버를 이동하기 때문에 공격자가 중간에서 가로챌 수 없다.

Authorization Code Grant 방식의 흐름

Authorization Code Grant 방식 구현

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b3259827-8ce0-4fd5-85f9-f457ec9ff277/Untitled.png

[1, 2, 3, 4 단계]

  • 프론트에서 깃허브 로그인 버튼 클릭 → 백엔드의 깃허브 로그인 API를 호출.
  • 백엔드에서 깃허브 인증 페이지로 리다이렉션 (passport-github2).
  • 사용자는 깃허브 계정 로그인을 진행

[5, 6, 7, 8 단계] - passport-github2 에서 내부적으로 진행하는 단계

router.get(
  '/github/callback',
  passport.authenticate('github', {
    session: false,
    failureRedirect: clientURL,
  }),
  githubVerifyCallbackController,
);
  • 로그인 성공시 GitHub OAuth 인증키를 받을 때 설정한 callbackURL로 Authorization Code가 전송됨.
  • Authorization Code로 인증 서버(GitHub)에 Access Token을 요청
  • Access Token을 받으면 토큰을 이용해서 유저 profile 정보를 가져오는 API를 호출해서 유저 정보를 받음.
const GitHubVerifyCallback = async (
  accessToken,
  refreshToken,
  profile,
  done,
) => {
  try {
    const [user] = await UserModel.findOrCreate({
      where: { uid: profile.id },
      defaults: {
        uid: profile.id,
        name: profile.username,
        profileLink: profile.profileUrl,
      },
    });
    return done(null, user);
  } catch (error) {
    return done(error, null);
  }
};

[JWT 토큰 발급 단계]

  • 로그인 시작점이 fetch API 방식이 아니고 페이지 이동이 일어나는 GET 요청이기 때문에 응답을 URL로 받는다. ⇒ 토큰이 URL에 노출됨

    • 노출되어도 상관이 없는 일회용 인증코드를 쿼리로 응답하고 해당 코드로 토큰을 요청하는 단계를 거침 (5, 6, 7, 8 단계를 모방한 방식)
  • 인증코드를 생성해서 inmemory 저장소에 <인증코드, 유저정보>를 저장하고 쿼리에 담아서 클라이언트(사용자)에게 전송.

    const codeMap = new Map(); // <인증코드, 유저정보>
    
    const githubVerifyCallbackController = (req, res) => {
      const authCode = uuidv4();
      codeMap.set(authCode, req.user);
      res.redirect(`${clientURL}/oauth/callback?code=${authCode}`);
    };
  • 쿼리에서 인증코드를 파싱하고 백엔드 서버에 JWT 토큰을 요청하는 빈 페이지 컴포넌트를 구현하여 콜백 페이지로 이용.

    const OAuthCallback = ({ location }) => {
      const history = useHistory();
      const getToken = async () => {
        const { code } = queryString.parse(location.search);
        const { token } = await oauthApi.getToken(code);
        localStorage.setItem('token', token);
      };
      useEffect(() => {
        getToken().then(() => history.push('/'));
      }, []);
    
      return <></>;
    };
  • /oauth/token 으로 API 요청을 받으면 인증 코드로 유저 정보를 꺼내 토큰을 생성하고 클라이언트에게 응답.

    router.post('oauth/token', (req, res) => {
      const {
        body: { code },
      } = req;
      const user = codeMap.get(code);
      codeMap.delete(code);
      const token = generateToken(JwtConfig)(user);
      return res.json({ token });
    });
  • 응답받은 토큰을 localStorage에 저장

    → 문제점: localStorage로 토큰에 접근할 수 있으므로 보안에 취약하다.

로그인 상태 체크

사용자가 로그인이 되어있는 상태가 아니라면 로그인 페이지로 이동시켜야 한다.

그렇게 하기 위해서는 로그인 요청을 제외하고, 서버에 api 요청을 할 때마다, 이 유저가 로그인이 되어있는 상태인지 확인해야 한다.

  1. 로컬 스토리지에 토큰 유무 검사

    const token = localStorage.getItem('{token_name}') ;
    if(token){
    	... 
    }
    1. 토큰이 존재하지 않으면, 로그인이 필요하므로 로그인 페이지로 이동시킨다.

      Redirect("/login")

    2. 토큰이 존재한다면, 그 토큰을 헤더에 담아서 서버에 api를 요청한다. 토큰을 담아서 사용자 정보 요청

      1. 유효한 토큰이라면, 서버는 토큰에 담긴 사용자 정보 반환한다.

        클라이언트 측에서는 응답받은 사용자 정보를 context에 저장하여 사용하게 된다.

        const login = async () => {
            const flow = {};
            try {
              const userInfo = await userAPI.getMyInfo();
              flow.action = userContextActions.setUserInfo(userInfo);
              flow.redirectURL = LOGIN_SUCCESS_URL;
            } catch (error) {
              flow.action = userContextActions.resetUserInfo();
              flow.redirectURL = LOGIN_FAILURE_URL;
            }
            userFlowAction({ ...flow });
          };
      2. 만약 유효하지 않은 토큰이라면, 401 응답이 오게 된다.

        이 유저는 로그인이 필요하므로, 로그인 페이지로 이동시킨다.

        Redirect("/login")

        instance.interceptors.response.use(
          (response) => {
            return response.data;
          },
          (error) => {
            if (error.response.status === 401) {
              localStorage.removeItem('token');
              window.location.href = '/login';
            }
            return Promise.reject(error);
          },
        );

axios interceptor

api 요청시마다 Authorization 요청 헤더에 토큰을 보내주어야 한다.

이 작업은 반복적이므로, axios의 interceptor 로 자동으로 요청 헤더에 토큰을 설정해준다.

  1. axios 인스턴스 생성.
const instance = axios.create({
  baseURL: process.env.API_BASE_URL,
});
  1. interceptor.request로 Authorization 헤더에 token을 저장한다.
instance.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  const configCopy = config;
  if (token) configCopy.headers.common.Authorization = `Bearer ${token}`;
  return configCopy;
});

Clone this wiki locally