상세 컨텐츠

본문 제목

OAuth - github & Node.JS 구현

Computer Science/Security

by Yongari 2023. 1. 6. 22:07

본문

 

 

실습전에 github 설정을 해야하는데 필요한 코드는 다음 블로그 포스팅을 참고하면 된다. 

https://velog.io/@d-h-k/Oauth-%EC%B0%8D%EB%A8%B9%ED%95%B4%EB%B3%B4%EA%B8%B0-with-POSTMAN%EC%8B%A4%EC%8A%B5

 

OAuth 찍먹해보기 (with POSTMAN실습 )

Authorizing OAuth Apps 이거 따라한건데, 막연할꺼같아 POSTMAN으로 실습까지 곁들어 봄OAuth Application 등록하기client_id client_secret 발급받기code 발급받기access_token발급먼저 깃허브에 로그인해주세요우

velog.io

다음은 Github OAuth 와 노드JS를 연동하는 코드다. 

 

 

1. 위의 게시물을 보고 환경변수 설정을 합니다.

 

 

GITHUB_CLIENT_ID="설정"
GITHUB_CLIENT_SECRET="설정"

 

 

2. OAuth 단계별 실행

 

  1. 클라이언트에서 GITHUB_LOGIN_URL에 클라이언트 아이디를 파라미터로   Authorization code를 요청함 
  2. GITHUB에서 요청받은 정보를 기반으로 Authorization code를 클라이언트한테 부여함 
  3. 클라이언트에서 받아온 Authorization code를 서버의 /callback 엔드포인트로 전달함
  4. 서버의 /callback 엔드포인트에서 Authorization code를  바탕으로  githubApp에게 Access token 발급을 요청
  5. GITHUB OAuth에서 서버로 GITHUB Access token을 부여함 
  6. 서버의 /callback 엔드포인트에서 Access token을 클라이언트한테 전달함 
  7. 서버의 /images 엔드포인트에서 Access token이 정상일 경우 이미지를 정상적으로 표시

 

 

 

클라이언트

 

1. Login.js

import React, { Component } from "react";

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

    this.socialLoginHandler = this.socialLoginHandler.bind(this);
    // 참고: https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps

    this.GITHUB_LOGIN_URL =
      "https://github.com/login/oauth/authorize?client_id=클라이언트id";
  }

  //
  // Window Location Assign
  // location.assign() 함수는 새로운 페이지를 로드합니다.
  // 버튼 클릭 후  GITHUB_LOGIN_URL로 새 패이지를 로드한 뒤 권한요청에 수락한다면 
  // Authorization code와 함께 같은 url 뒤에 Authorization code를 파라미터로 리디렉션된다. 
  socialLoginHandler() {
    window.location.assign(this.GITHUB_LOGIN_URL);
  }

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

export default Login;

 

 

 

2. Mypage.js

import React, { Component } from "react";
import axios from "axios";

class Mypage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      images: [],

      // TODO: GitHub API 를 통해서 받아올 수 있는 정보들 중에서
      // 이름, login 아이디, repository 주소, public repositoty 개수를 포함한 다양한 정보들을 담아주세요.
    };
  }

  //리소스를 리퀘스트하는 함수

  async getGitHubUserInfo() {
    // TODO: GitHub API를 통해 사용자 정보를 받아오세요.
    // https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-the-authenticated-user
    const { accessToken } = this.props;

    if (!accessToken) {
      return;
    } else {
      console.log("accessToken", accessToken);
      const result = await axios({
        method: "get",
        url: "https://api.github.com/user",
        headers: {
          authorization: `Bearer ${accessToken}`,
        },
      });

      const { name, login, html_url, public_repos } = result.data;

      this.setState({
        name, //이름
        login, //나의 로그인 아이디
        html_url, //깃헙 주소
        public_repos, //깃헙 레퍼지토리
      });
    }
  }

  async getImages() {
    // TODO : 마찬가지로 액세스 토큰을 이용해 local resource server에서 이미지들을 받아와 주세요.
    // resource 서버에 GET /images 로 요청하세요.
    const { accessToken } = this.props;

    if (!accessToken) {
      return;
    } else {
      const result = await axios({
        method: "get",
        url: "http://localhost:8080/images",
        headers: {
          Accpet: "application/json",
          authorization: `Bearer ${accessToken}`,
        },
      });

      const { images } = result.data;
      this.setState({
        images,
      });
    }
  }

  componentDidMount() {
    this.getGitHubUserInfo();
    this.getImages();
  }

  render() {
    const { accessToken } = this.props;

    if (!accessToken) {
      return <div>로그인이 필요합니다</div>;
    }

    const { name, login, html_url, public_repos, images } = this.state;

    return (
      <div>
        <div className="mypageContainer">
          <h3>Mypage</h3>
          <hr />

          <div>
            안녕하세요.{" "}
            <span className="name" id="name">
              {name}
            </span>
            님! GitHub 로그인이 완료되었습니다.
          </div>
          <div>
            <div className="item">
              나의 로그인 아이디:
              <span id="login">{login}</span>
            </div>
            <div className="item">
              나의 GitHub 주소:
              <span id="html_url">{html_url}</span>
            </div>
            <div className="item">
              나의 public 레포지토리 개수:
              <span id="public_repos">{public_repos}</span>개
            </div>
          </div>

          <div id="images">
            {/* TODO: 여기에 img 태그를 이용해 resource server로 부터 받은 이미지를 출력하세요 */}
            {images.map((img) => (
              <img key={img.file} src={img.blob} />
            ))}
          </div>
        </div>
      </div>
    );
  }
}

export default Mypage;

 

 

 

3. App.js

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,
      // TODO:
      accessToken: "",
    };
    this.getAccessToken = this.getAccessToken.bind(this);
  }

  async getAccessToken(authorizationCode) {
    // 받아온 authorization code로 다시 OAuth App에 요청해서 access token을 받을 수 있습니다.
    // access token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있습니다.
    // authorization code를 서버로 보내주고 서버에서 access token 요청을 하는 것이 적절합니다.

    // TODO: 서버의 /callback 엔드포인트로 authorization code를 보내주고 access token을 받아옵니다.
    // access token을 받아온 후
    //  - 로그인 상태를 true로 변경하고,
    //  - state에 access token을 저장하세요
    await axios
      .post("http://localhost:8080/callback", { authorizationCode })
      .then((data) => {
        this.setState({
          isLogin: true,
          accessToken: data.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=$code
      this.getAccessToken(authorizationCode);
    }
  }

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

export default App;

 

 

 

백엔드 

 

4. callback.js

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가 들어옵니다. console.log를 통해 서버의 터미널창에서 확인해보세요!
  //인증(authentication)은 자신이 누구라고 주장하는 사람을 확인하는 절차이다. 권한부여(authorization)는 가고 싶은 곳으로 가도록 혹은 원하는 정보를 얻도록 허용하는 과정이다.
  console.log(req);

  // TODO : 이제 authorization code를 이용해 access token을 발급받기 위한 post 요청을 보냅니다. 다음 링크를 참고하세요.
  // https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps#2-users-are-redirected-back-to-your-site-by-github
  // 2) AccessToken
  //
  // 클라이언트에서 받은 Authorization Code를 Authorization Server로 보내고
  // Server는 이를 이용해 AccessToken을 발급 받는다.

  console.log("req.body.authorizationCode", req.body.authorizationCode);
  axios({
    method: "post",
    url: `https://github.com/login/oauth/access_token`,
    headers: {
      accept: "application/json",
    },
    data: {
      //oauth 앱등록시 받은 클라이언트 아이디, 클라이언트 시크릿 그리고 클라이언트에서 받은
      //req.body.authorizationCode를 가지고 요청을 한다.
      client_id: clientID,
      client_secret: clientSecret,
      code: req.body.authorizationCode,
    },
  })
    .then((response) => {
      accessToken = response.data.access_token;
      res.status(200).json({ accessToken: accessToken });
    })
    .catch((e) => {
      res.status(404);
    });
};

 

5. images.js

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

module.exports = (req, res) => {
  // TODO : Mypage로부터 access token을 제대로 받아온 것이 맞다면, resource server의 images를 클라이언트로 보내주세요.

  console.log(req.headers);
  if (!req.headers.authorization) {
    res.status(403).send({ message: "no permission to access resources" });
    return;
  } else {
    res.status(200).send({ images });
  }
};

 

출처 : 코드스테이츠

관련글 더보기