62일차 [인증/보안] OAuth

2021. 10. 27. 15:17
반응형

2021. 10. 22 금요일

1. Today's Key Points!🔑

  • OAuth 2.0

2. 정리해보자!🧹

OAuth 2.0

OAuth2.0은 인증을 위한 표준 프로토콜의 한 종류이다. 보안 된 리소스에 액세스하기 위해 클라이언트에게 권한을 제공하는 프로세스를 단순화하는 프로토콜 중 한 방법이다.

OAuth는 언제, 왜 쓸까?

유저 입장에서, 우리는 웹상에서 굉장히 많은 서비스를 이용하고 있고, 각각의 서비스들을 이용하기 위해서 회원가입 절차가 필요한 경우가 대부분이다. 그 서비스별로 ID와 Password를 다 기억하는 것은 매우 귀찮은 일이다. OAuth를 활용한다면 자주 사용하는 서비스의 ID와 Password만 기억해 놓고 해당 서비스들을 통해서 소셜 로그인을 할 수 있게된다. 뿐만 아니라 보안상의 이점도 있다. 검증되지 않은 App에서 OAuth를 사용해서 로그인한다면, 직접 유저의 민감한 정보가 App에 노출될 일이 없고 인증 권한에 대한 허가를 미리 유저에게 구해야 하기 때문에 더 안전하게 사용할 수 있다.

OAuth에서 꼭 알아야 할 용어

  • Resource Owner :  액세스 중인 리소스의 유저. 김코딩의 구글 계정을 이용하여 App에 로그인할 경우, 이때 Resource owner은 김코딩이 된다.
  • Client :  Resource owner를 대신하여 보호된 리소스에 액세스하는 응용프로그램. 클라이언트는 서버, 데스크탑, 모바일 또는 기타 장치에서 호스팅할 수 있다.
  • Resource server :  client의 요청을 수락하고 응답할 수 있는 서버.
  • Authorization server :  Resource server가 액세스 토큰을 발급받는 서버. 즉 클라이언트 및 리소스 소유자를 성공적으로 인증한 후 액세스 토큰을 발급하는 서버를 말한다.
  • Authorization grant :  클라이언트가 액세스 토큰을 얻을 때 사용하는 자격 증명의 유형.
  • Authorization code :  access token을 발급받기 전에 필요한 code. client ID로 이 code를 받아온 후, client secret과 code를 이용해 Access token 을 받아온다.
  • Access token :  보호된 리소스에 액세스하는 데 사용되는 credentials. Authorization code와 client secret을 이용해 받아온 이 Access token으로 이제 resource server에 접근을 할 수 있다.
  • Scope :  scope는 토큰의 권한을 정의한다. 주어진 액세스 토큰을 사용하여 액세스할 수 있는 리소스의 범위이다.

소셜 로그인 로직 플로우

소셜 로그인 로직 플로우

3. Sprint 과제 복기!🧐

github OAuth를 하기위해서는 github에 내 앱을 등록해야한다. https://github.com/settings/developers에서 등록하면 된다. 등록하는 과정은 생략하겠다. client_id, client_secret을 받고, env파일에 넣어둔다.

위에 정리해놓은 플로우대로 한번 진행해보자. 클라이언트에서 github 아이디로 로그인을 하려고 한다. 그럼 클라이언트에서는 github으로 로그인하기를 누르면 github로그인을 할 수 있는 곳으로 이동시켜주어야 한다.

//Login Component
import React, { Component } from "react";

class Login extends Component {
  constructor(props) {
    super(props);

    this.socialLoginHandler = this.socialLoginHandler.bind(this);

    // GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 한다. 적절한 URL을 입력해주어야 한다.
    // OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 한다.

    // 아까 발급받은 client_id를 파라미터로 넣어준다.
    this.GITHUB_LOGIN_URL =
      "https://github.com/login/oauth/authorize?client_id=088ba3fb0c476ab48f94";
  }

  socialLoginHandler() {
    window.location.assign(this.GITHUB_LOGIN_URL);
  }

  render() {
    return (
      <div className="loginContainer">
        OAuth 2.0으로 소셜 로그인을 구현해보세요.
        <img
          id="logo"
          alt="logo"
          src="https://image.flaticon.com/icons/png/512/25/25231.png"
        />
        <button onClick={this.socialLoginHandler} className="socialloginBtn">
          Github으로 로그인
        </button>
      </div>
    );
  }
}

export default Login;

github 로그인에 성공하면 authorizationCode를 내 앱에 발급해 줄 것이다. 그럼 이 코드를 서버에 전달해줘서 서버가 github 서버한데 access token을 달라고 요청할 것이다.

//서버에 code를 전달해주고 access token을 받아온다.

import React, { Component } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import Login from "./components/Login";
import Mypage from "./components/Mypage";
import axios from "axios";
class App extends Component {
  constructor() {
    super();
    this.state = {
      isLogin: false,
      accessToken: "",
    };
    this.getAccessToken = this.getAccessToken.bind(this);
  }

  async getAccessToken(authorizationCode) {
    // 받아온 authorization code로 다시 OAuth App에 요청해서 access token을 받을 수 있다.
    // access token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있다.
    // authorization code를 서버로 보내주고 서버에서 access token 요청을 하는 것이 적절하다.
    // 서버의 /callback 엔드포인트로 authorization code를 보내주고 access token을 받아온다.
    // access token을 받아온 후
    //  - 로그인 상태를 true로 변경하고,
    //  - state에 access token을 저장하자

    let res = await axios.post("http://localhost:8080/callback", {
      authorizationCode,
    });

    this.setState({
      isLogin: true,
      accessToken: res.data.accessToken,
    });
  }

  componentDidMount() {
    const url = new URL(window.location.href);
    const authorizationCode = url.searchParams.get("code");
    if (authorizationCode) {
      // authorization server로부터 클라이언트로 리디렉션된 경우, authorization code가 함께 전달된다.
      // ex) http://localhost:3000/?code=5e52fb85d6a1ed46a51f
      this.getAccessToken(authorizationCode);
    }
  }

  render() {
    const { isLogin, accessToken } = this.state;
    return (
      <Router>
        <div className="App">
          {isLogin ? <Mypage accessToken={accessToken} /> : <Login />}
        </div>
      </Router>
    );
  }
}

export default App;

그럼 서버에 callback 요청이 들어오면 토큰을 받아오는 것을 구현해보자.

require("dotenv").config();

const clientID = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const axios = require("axios");

module.exports = (req, res) => {
  // req의 body로 authorization code가 들어온다. 클라이언트에서 요청을 보낼때 body에 담아서 보냈기 때문이다.
  const code = req.body.authorizationCode;
  // 이제 authorization code를 이용해 access token을 발급받기 위한 post 요청을 보낸다.
  axios
    .post(
      "https://github.com/login/oauth/access_token",
      { client_id: clientID, client_secret: clientSecret, code },
      { headers: { accept: "application/json" } }
    )
    .then((result) => {
      res.status(200).json({ accessToken: result.data.access_token });
    });
};

github에서 부여받은 client_id, client_secret, code를 담아서 post요청을 보내준다. headers에 accept:"application/json"을 해준 이유는 클라이언트가 서버에게 웬만하면 데이터 전송할 때 이러이러한 타입으로 가공해서 보내달라고 요청해서 우리가 읽을 수 있는 형태로 받기위해서 이다.

Content-Type과 다른점은 Content-Type에 적은 것은 우리가 보내는 타입이 이러이러한 것이다 라고 알려주는 것이고, accept는 이러이러한 타입으로 보내달라고 요청하는 것이라고 생각하면 된다.

이런 부분은 post요청을 할 때 중요하게 작용한다. get요청을 하면 어차피 URL 끝에 쿼리스트링으로 key=value이런 형식으로 날아가기 때문에 굳이 Content-Type 헤더가 필요없다. 서버 입장에서도 요청메시지의 method가 get이면 key=value 형식의 데이터라는 것을 유추할 수 있기 때문이다. 하지만 post나 put처럼 메시지 body에 json 형식의 데이터를 담아 보낼 때 Content-Type 값을 application/json으로 지정해서 보내야 서버에서도 어떤 형식의 데이터인지를 알 수 있을 것이다.

그럼 이제 access token을 받았으니 github에게 사용자 정보 get요청을 하러 가보자.

현재 access token은 상태로 저장이 되어있다. 그래서 Mypage 컴포넌트가 상태를 받아서 쓰면 된다.

//생략
class Mypage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      images: [],
      name: "",
      login: "",
      html_url: "",
      public_repos: 0,
    };
  }

  async getGitHubUserInfo() {
    // GitHub API를 통해 사용자 정보를 받아오자.
    let res = await axios.get("https://api.github.com/user", {
      headers: { Authorization: `token ${this.props.accessToken}` },
    });
    let { name, login, html_url, public_repos } = res.data;
    this.setState({ name, login, html_url, public_repos });
  }
  
  componentDidMount() {
    this.getGitHubUserInfo();
    this.getImages();
  }
  //생략

get 요청을 통해서 유저 정보들을 받아오고 받아온 정보들을 상태에 저장해서 클라이언트에 뿌려준다.

images는 우리 서버에서 받아온다. access token을 제대로 받아온것이 맞다면 images파일을 클라이언트로 보내줘야 한다.

const images = require("../resources/resources");

module.exports = (req, res) => {
  if (!req.headers.authorization) {
    return res
      .status(403)
      .json({ message: "no permission to access resources" });
  } else {
    return res.status(200).json({ images });
  }
};

클라이언트에서는 어떻게 요청을 하는지 한번 보자.

async getImages() {
    // TODO : 마찬가지로 액세스 토큰을 이용해 local resource server에서 이미지들을 받아와 주세요.
    // resource 서버에 GET /images 로 요청하세요.
    let res = await axios.get("http://localhost:8080/images", {
      headers: { Authorization: `token ${this.props.accessToken}` },
    });
    this.setState({ images: res.data.images });
  }
  
  componentDidMount() {
    this.getGitHubUserInfo();
    this.getImages();
  }

헤더에 발급받은 토큰을 담아서 get요청으로 images를 받아오고 이것을 상태에 저장시켜준다.

이것으로 간단한 github OAuth 구현이 끝이 났다. 추후에 google OAuth 구현을 해볼 것이다.

반응형
LIST

BELATED ARTICLES

more