Rails (with sorcery)+ React でOAuth認証を実装してみた



sorceryでRails +SPA構成のOauthサンプルがあまりなさそうなので
参考になれば。
<br>

## 環境
**backend**

- rails 6系

**front**

- react:16.12
- react-router: v5系


以下、sorceryのwikiに沿いつつ、SPA用でカスタムしたところを中心に記載
[sorcery wiki](https://github.com/Sorcery/sorcery/wiki/External)

今回はgithub と連携してみる

<br>

## 開発の概要

**Backend(Rails)**

- oauthからのcallbackを受け取り、ユーザー作成及びログイン処理

**Front(React)**

- githubAPIへQueryString形式でパラメーターをもたせてアクセス。
- 認証後callBackURLにリダイレクトし、Backendに取得したパラメーターを送信

<br>

### Backend側開発
---

#### external module関連のセットアップ
```bash
# external moduleのインストール
bundle exec rails g sorcery:install external --only-submodules

# migration
bundle exec rails db:migrate

# 認証用モデル作成
bundle exec rails g model Authentication --migration=false
```

<br/>
<br/>

#### sorceryのgithub認証関連設定変更
**認証用情報の取得**
github側の認証設定は以下から行う
https://github.com/settings/developers


```ruby:initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:external] #:external追加
Rails.application.config.sorcery.configure do |config|
　...
  config.github.key = "your github key"
  config.github.secret = "your github secret"
  config.github.callback_url = ""
  config.github.user_info_mapping = {:email => "email" }
  config.github.scope = "user:email"
end
```

<br/><br/>

#### Oauth用controllerの作成

```ruby:oauths_controller

class OauthsController < ApplicationController
# Frontで取得したToken情報をもとにユーザー認証をするMethod
  def callback
    provider = params[:provider]
    # loginできた場合はここで200を返す
    if @user = login_from(provider)
      render json: { status: 'OK' }
    else
      begin
       # loginできない場合は送られてきた情報をもとにユーザー作成
        @user = create_from(provider)

        reset_session
        auto_login(@user)
        render json: { status: 'OK'　}
      rescue
        render json: { status: 'NG' }, status: 400
      end
    end
  end
end

```

<br/><br/>

#### ルーティングの設定
- wiki記載の内容と異なり、oauth用tokenが送信されてくるAPIのみでOK

```ruby:config/routes.rb
Rails.application.routes.draw do 
  ...
  post "oauth/callback" => "oauths#callback"
end
```
<br/>
<br/>


### Front側開発
---

#### 関連するルーティングの定義
```tsx:router.jsx
const AppRouter = () => (
  <Router>
    <Switch>
      <Route
        path="/callback/:provider/"
        component={ExternalAuthCallback}
      />
      <Route path="/sign_in" component={SignIn} />
      <Route component={NotFound} />
    </Switch>
  </Router>
);

export default AppRouter;
```

<br/><br/>

#### サインインページ
- サインインページの1機能としてGithub認証があるイメージ
- 通常のRailsのOauthと異なり、callbackされるURLはReactで構成されたSPAのURL(=>"/callback/:provider/")が叩かれることに注意

```tsx:SignIn.jsx

// CONST.GITHUB.REDIRECT_URL = "http://localhost:3001/callback/github/"

const GITHUB_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CONST.GITHUB.APP_ID}&redirect_url=${CONST.GITHUB.REDIRECT_URL}&scope=user:email`;

// ただ queryStringの付与したURLのリンクを踏ませるだけ
const signInForm = () => (
      <div className={styles.submitBox}>
        <Button href={GITHUB_AUTH_URL}>GITHUBで認証</Button>
      </div>
);

export default signInForm;
```

<br/><br/>

#### CallbackURLコンポーネント
- callback時に叩かれるURLで利用するコンポーネント
- URLパラメーターでprovider名（今回は"github")を取得
- callbackURLのquesyStringに付与された認証情報（code=XXXX）を取得
- ReactからバックエンドAPIへPostする

```tsx:oAuthCallback/index.jsx
import React, { useState } from "react";
import queryString from "query-string";
import { useHistory } from "react-router";
import { useParams, useLocation } from "react-router-dom";
import { api } from "../../../modules/user";
import Circular from "../../atoms/circular";

const BEFORE = "BEFORE";
const DOING = "DOING";

const ExternalAuth = () => {
  const location = useLocation();
  const history = useHistory();
  const { code = "" } = queryString.parse(location.search);
  const { provider = "" } = useParams();
  const [requestStatus, setRequestStatus] = useState(BEFORE);

  const request = () => {
    setRequestStatus(DOING);
    api.sendExternalAuthRequest({ code, provider }).then(isSuccess => {
      if (isSuccess) {
        history.push("/member/dashboard"); // login後ページ
      } else {
        history.push(PAGE_PATH.AUTH_SIGN_IN); //認証失敗した場合
      }
    });
  };
　   
  if (requestStatus === BEFORE) {
    request();
  }
  return (
    <div className={styles.container}>
      <Circular />
    </div>
  );
};

export default ExternalAuth;

```

<br/>

**Rails APIへのリクエスト**

```typescript:api.js
export const sendExternalAuthRequest = async ({
  code,
  provider
}) => {
  const requester = requestManager.get();
  return requester
    .post(
      "/oauth/callback",
      {
        code,
        provider
      },
    )
    .then(() => true)
    .catch(() => false);
};

```