2020년 4월 20일 월요일

개방형 플랫폼 보안을 위한 OAuth기반 인증 개발 방법

이 글은 OAuth 기반 웹 서비스 인증 기능 개발 방법을 간단히 다룬다. 최근 건설 분야에서 개방형 플랫폼, 디지털 트윈 플랫폼 등의 요구가 많아지고 있다. 개방형 플랫폼을 개발하기 위해서는 API가 필수적이다. 하지만, 누구나 플랫폼에 접근해 필요한 정보를 얻거나 수정한다면 보안상 큰 문제가 된다. 보안 및 인증 문제를 해결하기 위한 방법이 오픈소스 기반 OAuth를 이용하는 것이다. 이 글은 다음 내용을 포함한다.
  • 구글, 페이스 북 등을 포함한 로그인 기능
  • API 접근 인증을 위한 토큰 획득 및 로그인 기능

이 글은 nodejs의 React, Strapi를 이용해 인증을 구현한다. API 사용 인증이나, 다른 인증 제공자(Facebook, GitHub, Google ...)를 이용한 로그인 방법을 다룬다.

프로젝트 개발
우선 Strapi API를 작성한다.

$ npm install strapi@alpha -g
$ strapi new my-app
$ cd my-app && strapi start
$ npm install create-react-app -g
$ create-react-app good-old-react-authentication-flow

이 결과 다음과 같은 폴더가 만들어진다.
/src
└─── containers // React components associated with a Route
 |    └─── App // The entry point of the application
 |    └─── AuthPage // Component handling all the auth views
 |    └─── ConnectPage // Handles the auth with a custom provider
 |    └─── HomePage // Can be accessed only if the user is logged in
 |    └─── NotFoundPage // 404 Component
 |    └─── PrivateRoute // HoC
 |
└─── components // Dummy components
 |
└─── utils
     └─── auth
     └─── request // Request helper using fetch

인증 뷰를 구현하려면 먼저 사용자가 특정 URL에 액세스 할 수 있는지 확인하는 HoC( Higher Order Component)를 만들어야 한다. 이를 위해, auth.js를 사용한다.

import React from 'react';  
import { Redirect, Route } from 'react-router-dom';

// Utils
import auth from '../../utils/auth';

const PrivateRoute = ({ component: Component, ...rest }) => (  
  <Route {...rest} render={props => (
    auth.getToken() !== null ? (
      <Component {...props} />
    ) : (
      <Redirect to={{
        pathname: 'auth/login',
        state: { from: props.location }
        }}
      />
    ):
  )} />
);

export default PrivateRoute;  

라우팅을 만들어 본다.
import React, { Component } from 'react';  
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

// Components
import AuthPage from '../../containers/AuthPage';  
import ConnectPage from '../../containers/ConnectPage';  
import HomePage from '../../containers/HomePage';  
import NotFoundPage from '../../containers/NotFoundPage';

// This component ios HoC that prevents the user from accessing a route if he's not logged in
import PrivateRoute from '../../containers/PrivateRoute';

// Design
import './styles.css';

class App extends Component {  
  render() {
    return (
      <Router>
        <div className="App">
          <Switch>
            {/* A user can't go to the HomePage if is not authenticated */}
            <PrivateRoute path="/" component={HomePage} exact />
            <Route path="/auth/:authType/:id?" component={AuthPage} />
            <Route exact path="/connect/:provider" component={ConnectPage} />
            <Route path="" component={NotFoundPage} />
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;  

이제 인증 뷰를 만든다. 먼저 forms.json 인증보기 양식을 만든다. 이 구조는 다음 JSON과 같다.

{
  "views": {    
    "login": [
      {
        "customBootstrapClass": "col-md-12",
        "label": "Username",
        "name": "identifier",
        "type": "text",
        "placeholder": "johndoe@gmail.com"
      },
      {
        "customBootstrapClass": "col-md-12",
        "label": "Password",
        "name": "password",
        "type": "password"
      },
      {
        "customBootstrapClass": "col-md-6",
        "label": "Remember me",
        "name": "rememberMe",
        "type": "checkbox"
      }
    ]
  },
  "data": {
    "login": {
      "identifier": "",
      "password": "",
      "rememberMe": false
    }
  }
}

auth/login, auth/register는 다음 같은 수명주기를 사용한다.

componentDidMount() {  
  // Generate the form with a function to avoid code duplication
  // in other lifecycles
  this.generateForm(this.props);
}
componentWillReceiveProps(nextProps) {  
  // Since we use the same container for all the auth views we need to update
  // the UI on location change
  if (nextProps.location.match.params.authType !== this.props.location.match.params.authType) {
    this.generateForm(nextProps);
  }
}

양식을 만들려면 forms.json파일에서 검색한 데이터를 매핑하면 된다.
handleChange = ({ target }) => this.setState({ value: { ...this.state.value, [target.name]: target.value } });

render() {  
  const inputs = get(forms, ['views', this.props.match.params.authType, []);

  return (
    <div>
      <form onSubmit={this.handleSubmit}>
        {inputs.map((input, key) => (
          <Input
            autoFocus={key === 0}
            key={input.name}
            name={input.name}
            onChange={this.handleChange}
            type={input.type}
            value={get(this.state.value, [input.name], '')}
          />
        ))}
        <Button type="submit" />
      </form>
    </div>
  );
}

이제 사용자 인증에 필요한 모든 보기를 작성해야 한다.

API 호출을 수행하려면 request 헬퍼를 사용한다.

handleSubmit = (e) => {  
  e.preventDefault();
  const body = this.state.value;
  const requestURL = 'http://localhost:1337/auth/local';

  request(requestURL, { method: 'POST', body: this.state.value})
    .then((response) => {
      auth.setToken(response.jwt, body.rememberMe);
      auth.setUserInfo(response.user, body.rememberMe);
      this.redirectUser();
    }).catch((err) => {
      console.log(err);
    });
}

redirectUser = () => {  
  this.props.history.push('/');
}

API에서 응답을 받으면 필요한 정보를 localStorageor 에 저장하고 사용자를 HomePage로 리디렉션한다.

인증 공급자 사용
사용자가 Facebook, GitHub 또는 Google을 선택하더라도 Strapi를 사용하여 사용자를 인증하는 것은 쉽다. 이 예에서는 Facebook을 사용하는 방법을 보여준다.

흐름은 다음과 같다.

  1. 사용자는 Facebook으로 로그인을 클릭한다
  2. 앱을 승인 할 수 있도록 다른 페이지로 리디렉션한다.
  3. 승인되면 Facebook은 URL의 코드를 사용하여 사용자를 앱으로 리디렉션한다.

componentDidMount에서 ConnectPage컨테이너 응답에 따라 API를 호출하고 사용자를 리디렉션한다.

componentDidMount() {  
  const { match: {params: { provider }}, location: { search } } = this.props;
  const requestURL = `http://localhost:1337/auth/${provider}/callback${search}`;

 request(requestURL, { method: 'GET' })
   .then((response) => {
      auth.setToken(response.jwt, true);
      auth.setUserInfo(response.user, true);
      this.redirectUser('/');
   }).catch(err => {
      console.log(err.response.payload)
      this.redirectUser('/auth/login');
   });
}

redirectUser = (path) => {  
  this.props.history.push(path);
}

SocialLink 컴포넌트 구현은 다음과 같다.

/**
*
* SocialLink
*
*/

import React from 'react';  
import PropTypes from 'prop-types';

import Button from '../../components/Button'

function SocialLink({ provider }) {  
  return (
    <a href={`http://localhost:1337/connect/${provider}`} className="link">
      <Button type="button" social={provider}>
        <i className={`fab fa-${provider}`} />
        {provider}
      </Button>
    </a>
  );
}

SocialLink.propTypes = {  
  provider: PropTypes.string.isRequired,
};

export default SocialLink;  

이제 AuthPage를 추가한다.
render() {  
  const providers = ['facebook', 'github', 'google', 'twitter']; // To remove a provider from the list just delete it from this array...

  return (
     <div>
       {providers.map(provider => <SocialLink provider={provider} key={provider} />)}
       {/* Some other code */}
     </div>
  );
}

레퍼런스


댓글 없음:

댓글 쓰기