부트캠프/TIL

[백엔드 인증/보안] OAuth

하이고니 2023. 3. 9. 20:45

 

 

OAuth란?


우리가 웹이나 앱에서 흔히 볼 수 있는 소셜 로그인 인증 방식은 OAuth 2.0라는 기술을 바탕으로 구현된다.

 

직접 작성한 서버에서 인증 처리를 해주는 것과는 달리, OAuth는 인증을 중개해주는 메커니즘이다. 

이미 사용자 정보를 가지고 있는 웹 서비스(Naver, Kakao, Google 등)에서 사용자의 인증을 대신해주고, 접근 권한에 대한 토큰을 발급한 후 이를 이용해 내 서버에서 인증을 할 수 있도록 해준다.

 

 

유저 입장에서 각각의 서비스별 ID와 Password를 다 기억한다는 것은 굉장히 힘들고 귀찮은 일이다.

OAuth를 활용한다면 자주 사용하고 중요한 서비스들의 ID와 Password만 기억해놓고 해당 서비스들을 통해 외부 서비스에 로그인 할 수 있다.

 

뿐만 아니라 OAuth는 보안상의 이점도 가지고 있다. 검증되지 않은 앱에서 OAuth를 사용해 로그인하면, 유저의 민감한 정보가 직접 앱에 노출될 일이 없고 인증 권한에 대한 허가를 미리 유저에게 구해야 하기 때문에 훨씬 안전하다.

 

 

OAuth 작동 매커니즘


OAuth의 주체

 

Resource Owner (사용자)

  • 소셜 로그인을 하려는 사용자
  • Resource - 사용자의 이름, 전화번호 등

 

Resource Server & Authorization Server (사용중인 서비스)

  • Resource Server - 사용자의 정보를 이미 저장하고 있는 서버
  • Authorization Server - 이미 사용중인 서비스 의 서버 중에서 인증을 담당하는 서버

 

Application (새로운 서비스)

  • 사용자가 소셜 로그인을 활용해 이용하고자 하는 서비스

 

OAuth 인증 방식


 

Authorization Code Grant Type

 

 

 

Refresh Token Grant Type

 

 

 

 

 

Authorization Code Grant Type 구현

 

 

2. 로그인

 

import React from 'react';
import githubLogo from './../images/github.png';

export default function Login() {
  const CLIENT_ID = '깃허브에서제공받은클라이언트아이디';

  const loginRequestHandler = () => {
    // GitHub로부터 사용자 인증을 위해 GitHub로 이동. 
    // OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션.
  
    return window.location.assign(	// 해당 url로 이동
      `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}`
    );
  };

	...
}

 

3~5. 리디렉션 후 액세스 토큰 요청(엔드포인트 /callback에 post 요청)

 

function App() {
  const [isLogin, setIsLogin] = useState(false);
  const [accessToken, setAccessToken] = useState("");

  const getAccessToken = async (authorizationCode) => {
    // 받아온 Authorization Code로 다시 OAuth App에 요청해서 Access Token을 받을 수 있다.
    // Access Token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있다.
    // Authorization Code를 서버로 보내주고 서버에서 Access Token 요청을 하는 것이 적절하다.
    
    // 서버의 /callback 엔드포인트로 Authorization Code를 보내주고 Access Token을 받아온다.
    // Access Token을 받아온 후 state에 Access Token을 저장한다.
    
    const res = await axios.post("http://localhost:4000/callback", {
      authorizationCode,
    });
    
    setAccessToken(res.data.accessToken);
    setIsLogin(true);
  };
  
  useEffect(() => {
    // Authorization Server로부터 클라이언트로 리디렉션된 경우, 
    // Authorization Code가 함께 전달된다.
    // ex) http://localhost:3000/mypage?code=5e52fb85d6a1ed46a51f
    const url = new URL(window.location.href);
    const authorizationCode = url.searchParams.get("code");
    if (authorizationCode) {
      getAccessToken(authorizationCode);
    }
  }, []);
  
	...
}

export default App;

 

 

6. 로컬 서버에서 요청을 받은 후 github에 액세스 토큰 요청

7. github에서 로컬 서버로 액세스 토큰 전달(Authorization Code가 유효할 경우)

8. 액세스 토큰 제대로 받았을 경우, 클라이언트에 토큰 전달

 

module.exports = async (req, res) => {
  // req의 body로 authorization code가 들어온다. 
  // authorization code를 이용해 access token을 발급받기 위한 post 요청을 보낸다. 
  try {
    const result = await axios({	// 6번
      method: 'post',
      url: `https://github.com/login/oauth/access_token`,
      headers: {
        accept: 'application/json',
      },
      data: {
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
        code: req.body.authorizationCode,
      },
    });
    
    // 받아온 결과의 data에서 뽑아낸다는 것. 주의하숑
    const accessToken = result.data.access_token;
    

    return res.status(200).send({ accessToken });	// 8번
  } catch (err) {
    return res.status(401).send({ message: 'error' });
  }
};

 

 

9. 로그인 처리 & Mypage 이동

 

  const getAccessToken = async (authorizationCode) => {
		...
    setAccessToken(res.data.accessToken);
    setIsLogin(true);	// 9번
  };

 

10. /userinfo 엔드 포인트로 유저 정보 요청

 

  useEffect(() => {
    // /userinfo를 통해 사용자 정보를 받아온다.
    // prop으로 받은 Access Token을 이용해 /userinfo 엔드포인트로 요청을 보내야 한다.
    // githubUser, serverResource의 상태를 응답 데이터로 업데이트해야 한다.
    // isLoading 상태를 false로 업데이트해야 한다.
    
    axios.post("http://localhost:4000/userinfo", { accessToken })
         .then((res) => {
          setGithubUser(res.data.githubUserData)
          setServerResource(res.data.serverResource)
          setIsLoading(false)
         })
         .catch((err) => console.log(err))

  }, [accessToken]);

 

 

11. github에 토큰을 보내면서 유저 정보 요청

12. 토큰 유효하면 github에서 로컬 서버로 정보 전달

13. 로컬 서버에서 클라이언트로 정보 전달

 

module.exports = async (req, res) => {
  const { accessToken } = req.body;
  // 클라이언트에서 전달받은 access token를 이용해 사용자의 정보를 가져온다.
  // 이때 요청하는 서버는 Github의 Authorization 서버가 아닌 Resource Server.
  
  return axios
    .get('https://api.github.com/user', {	// 11번
      headers: {
        Authorization: `token ${accessToken}`,
      },
    })
    .then((res) => res.data)
    .then((githubUserData) => {
      res.send({ githubUserData, serverResource });	// 13번
    })
    .catch((e) => {
      res.sendStatus(403);
    });
};