React Nativeの認証フローを考える

React Nativeのアプリ開発やReactのSPA開発において認証フローは、クオリティを左右する重要な要素であることは間違いない。

実際、検索結果には、認証フローに言及した記事や質問が投稿されていて関心の高さが伺える。その内容としては、ルーター設計における認証フローに関するものが多くを占めている印象だ。

そしてReact Nativeのルーター設計には、React Navigationやreact-native-router-fluxが使われるが、いずれもルーター・ナビゲーションライブラリであって認証ロジックを提供していない。すなわちルーターで良しなに対応するか、自作の認証ロジックをこしらえる必要がある。

このエントリーでは、React Nativeの認証フロー について考えてみたい。

そもそも…

そもそもReact Navigationの公式ドキュメントに、認証フローに言及したページがある。これはナビゲーションロジックを駆使して認証コンポーネントとプライベートコンポーネントを振り分けるというものだ。

まったくもって理にかなう認証フローだと思う。

その内容で十分だと思うならば、そちらを参考にするのがよいだろう。ただし認証ロジックを提供するものではないことは理解しなければならない。

認証フローのデモンストレーション

AppScreen.js

AppNavigator定数がメインナビゲーションに該当する。認証後に表示されるコンポーネントだ。AppNavigatorをcreateAppContainer(...)にセットした上で、withAuthenticator(...)にセットする。withAuthenticatorは、高階コンポーネントになっていて条件によって別コンポーネントを返す。

import React from 'react';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import HomeScreen from './HomeScreen';
import OtherScreen from './OtherScreen';
import withAuthenticator from '../components/withAuthenticator';

const AppNavigator = createStackNavigator({
  Home: HomeScreen,
  Other: OtherScreen
});

export default withAuthenticator(createAppContainer(AppNavigator));

withAuthenticator.js

まずconstructor(props) {...}で認証トークンを取得済みか否かを確認する。その結果で認証コンポーネントかプライベートコンポーネントに振り分ける。

認証トークン取得済みであればprops.authenticate(true);を実行して、this.props.isAuthenticatedの状態を更新する。これによってif (isAuthenticated) return <Component />;のコンポーネントが返される。すなわちこれは、AppScreen.jsで定義したcreateAppContainer(AppNavigator)を参照する。

認証トークン未取得であればprops.signIn();を実行して、this.props.authStageの状態を更新する。これによってswitch (authStage) {...}case types.SIGN_IN:が該当する。したがってサインインコンポーネントが返される。

import React from 'react';
import { connect } from 'react-redux';
import { types, actions } from '../modules/authentication';
import ForgotPassword from '../screens/ForgotPassword';
import SignInScreen from '../screens/SignInScreen';
import SignUpScreen from '../screens/SignUpScreen';

export default function withAuthenticator (Component) {
  class AuthenticatedComponent extends React.Component {

    constructor (props) {
      super(props);

      const authToken = await AsyncStorage.getItem('authToken');
      if (authToken) {
        props.authenticate(true);
      } else {
        props.signIn();
      }
    }

    render () {
      const { authStage, isAuthenticated } = this.props;

      if (isAuthenticated) return <Component />;

      switch (authStage) {
        case types.FORGOT_PASSWORD:
          return <ForgotPassword />;
        case types.SIGN_UP:
          return <SignUpScreen />;
        case types.SIGN_IN:
          return <SignInScreen />;
        default:
          return <SignInScreen />;
      }
    }
  }

  const mapDispatchToProps = dispatch => ({
    forgotPassword: () => dispatch(actions.forgotPassword()),
    signUp: () => dispatch(actions.signUp()),
    signIn: () => dispatch(actions.signIn()),
    authenticate: bool => dispatch(actions.authenticate(bool)),
  });

  const mapStateToProps = state => ({
    isAuthenticated: state.authenticationReducer.isAuthenticated,
    authStage: state.authenticationReducer.authStage,
  });

  return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent);
}

authentication.js

Reduxのaction-creatorとaction-type、reducerをまとめている。いわゆるDucksというデザインパターンだ。

stateにはisAuthenticatedauthStageを定義している。この状態を更新するためにaction-creatorのforgotPasswordsignUpsignInauthenticateがある。

forgotPasswordsignUpsignInは、authStageの状態を担当し、action-typeの定数値に更新する。

authenticateは、isAuthenticatedの状態を担当し、真偽値を受け取って更新する。

export const types = {
  FORGOT_PASSWORD: 'FORGOT_PASSWORD',
  SIGN_UP: 'SIGN_UP',
  SIGN_IN: 'SIGN_IN',
  AUTHENTICATED: 'AUTHENTICATED',
};

const initialState = {
  isAuthenticated: false,
  authStage: types.SIGN_IN,
};

export default function reducer (state = initialState, action = {}) {
  switch (action.type) {
    case types.FORGOT_PASSWORD:
      return Object.assign({}, state, {
        ...state,
        authStage: types.FORGOT_PASSWORD,
      });
    case types.SIGN_UP:
      return Object.assign({}, state, {
        ...state,
        authStage: types.SIGN_UP,
      });
    case types.SIGN_IN:
      return Object.assign({}, state, {
        ...state,
        authStage: types.SIGN_IN,
      });
    case types.AUTHENTICATED:
      return Object.assign({}, state, {
        ...state,
        isAuthenticated: action.payload,
      });
    default:
      return state;
  }
}

export const actions = {
  forgotPassword: () => ({ type: types.FORGOT_PASSWORD }),
  signUp: () => ({ type: types.SIGN_UP }),
  signIn: () => ({ type: types.SIGN_IN }),
  authenticate: bool => ({ type: types.AUTHENTICATED, payload: bool }),
};

まとめ

React Nativeの認証フロー に関する内容だった。

この内容ではReact Navigationを使っているが、react-native-router-fluxであっても再現可能だ。AppScreen.jsのwithAuthenticator(...)にreact-native-router-fluxのルーターコンポーネントをセットすればよい。

ただこの認証フローにも悩みどころがある。

React Navigationかreact-native-router-fluxに関わらず、認証コンポーネントはナビゲーション管轄外になっていることで、ヘッダーが表示されない。認証画面などにもヘッダーが必要ならば、NativeBaseなどのUIコンポーネントでヘッダーUIを挿入したり、各コンポーネントもナビゲーション化しなければならない。

UIコンポーネントを使う手段は、ヘッダーUIに差異が発生するから却下だろう。ナビゲーション化は、せっかく振り分けたのに、どこかモヤモヤする。

素直にReact Navigationの認証フローに倣ったほうがよいのかもしれない。

このエントリーが、あなたのクリエイティビティを刺激するものであると期待したい。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です