React Routerで認証を制御する方法

React Routerで認証を制御する方法です。試行錯誤して良さそうな実装方法を発見したのでご紹介します。

アプリに認証があると、画面ごとに、

1. 認証済みユーザのみアクセスを許可したいページ
    * アカウント設定など
2. 認証していないユーザのみアクセスを許可したいページ
    * ログイン・新規登録など
3. 認証に関係なくアクセスを許可したいページ
    * Qiita投稿のようなパブリックなコンテンツなど

の設定が必要になる場合があります。

React Routerのサンプルをいくつか見たところ幾つか認証を制御する方法が示されていました。

1. `onEnter`を使う方法
*  AuthenticatedComponent

## `onEnter`を使う方法

[本家のサンプル](https://github.com/rackt/react-router/blob/master/examples/auth-flow/app.js#L118-L132)で例示された実装です。現在βのreact-router 1.0.0が必要になります。

```xml
function requireAuth(nextState, transition) {
  if (!auth.loggedIn())
    transition.to('/login', null, { nextPathname: nextState.location.pathname });
}

React.render((
  <Router history={history}>
    <Route path="/" component={App}>
      <Route path="login" component={Login}/>
      <Route path="logout" component={Logout}/>
      <Route path="about" component={About}/>
      <Route path="dashboard" component={Dashboard} onEnter={requireAuth}/>
    </Route>
  </Router>
), document.getElementById('example'));
```

僕が試したところ、Flux実装によっては`requireAuth`関数とストアをうまくリンクできないという課題がありました。


## AuthenticatedComponent

AuthenticatedComponentは認証制御をするコンポーネントを作り、認証が必要なコンポーネントをwrapする実装パターンです。

```js:AutenticatedComponent.jsx
import React from 'react';
import LoginStore from '../stores/LoginStore';

export default (ComposedComponent) => {
  return class AuthenticatedComponent extends React.Component {

    static willTransitionTo(transition) {
      if (!LoginStore.isLoggedIn()) {
        transition.replaceWith('/login');
      }
    }

    ...

    render() {
      return (
      <ComposedComponent {...this.props} />
      );
    }
  }
};
```

クラスを定義するときに、コンポーネントのクラスを`AuthenticatedComponent`で囲むようにします。

```js:AccountSetting.jsx
import React from 'react';
import AuthenticatedComponent from './AuthenticatedComponent';

export default AuthenticatedComponent(class AccountSetting extends React.Component {
  render() {
    return <div>...</div>;
  }
});
```

`AuthenticatedComponent`はご覧のとおり、認証が必要なコンポーネントすべてに認証コードを記述しないといけないため、新しいページを作った場合、認証コードを記述し忘れるリスクがあります。

このパターンの詳細は[Adding authentication to your React Flux app](https://auth0.com/blog/2015/04/09/adding-authentication-to-your-react-flux-app/)を御覧ください。


## 認証コンポーネントをルーティングに組み込む方法

以上で見たとおり、`onEnter`はFlux実装を選ぶし、AuthenticatedComponenentは記述忘れがありそうでした。大抵のFlux実装とインテグできて、記述忘れも少ない方法が必要ほしくなります。

できれば、ルーティング設定で認証の制御を宣言したいと思いました。認証コンポーネントをルーティングに組み込む方法です。例えば下のようにです。

```xml
<Route component={App}>
  <Route component={UserOnly}>
    <Route path="/" component={Dashboard}/>
    <Route path="/settings" component={Settings}/>
  </Route>
  <Route component={GuestOnly}>
    <Route path="/signup" component={SignUp}/>
    <Route path="/login" component={Login}/>
  </Route>
</Route>
```

こうしておけば、ルーティング設定を見れば認証制御が一目瞭然で、認証まわりのコードを冗長的に書く必要もありません。

上のコードで示した`UserOnly`や`GuestOnly`は例によってReact Componentの一種です。`UserOnly`の実装サンプルが下のようになります。Flux実装のひとつの[Redux](https://github.com/gaearon/redux)を使ったものになっています。`@connect`の部分はReduxでストアと繋ぎこむ宣言なので、お使いのFlux実装に読み替えてください。


```js:UserOnly.jsx
import React from "react";

import { connect } from "redux/react";

@connect(state => ({
  session: state.session
}))
export default class UserOnly {
  static contextTypes = {
    router: React.PropTypes.object.isRequired
  }

  componentWillMount() {
    this.guestWillTransfer(this.props, this.context.router);
  }

  componentWillUpdate(nextProps) {
    this.guestWillTransfer(nextProps, this.context.router);
  }

  guestWillTransfer(props, router) {
    const { loggedIn } = props.session;
    if (loggedIn === false) {
      router.replaceWith("/login");
    }
  }

  render() {
    return (
      <div>{this.props.children}</div>
    );
  }
}
```


UserOnlyなどはコンポーネントなのでストアの状態(つまりログイン状態)が変化すると、`componentWillMount`や`componentWillUpdate`が呼ばれます。ここでフックして、認証できなかった場合、リダイレクトしているわけです。

ついでにGuestOnlyの実装です。

```js:GuestOnly.jsx
import React from "react";

import { connect } from "redux/react";

@connect(state => ({
  session: state.session
}))
export default class GuestOnly {
  static contextTypes = {
    router: React.PropTypes.object.isRequired
  }

  componentWillMount() {
    this.userWillTransfer(this.props, this.context.router);
  }

  componentWillUpdate(nextProps) {
    this.userWillTransfer(nextProps, this.context.router);
  }

  userWillTransfer(props, router) {
    const { loggedIn } = props.session;
    if (loggedIn === true) {
      router.replaceWith("/");
    }
  }

  render() {
    return (
      <div>{this.props.children}</div>
    );
  }
}
```

以上が、認証コンポーネントをルーティングに組み込んで、認証を制御する方法でした。