Google SignInするSPAとGoサーバー間のセッション管理

書き出したら長くなってしまい4回分になってしまった日記です。

  1. ReactとGoogle Sign-In
  2. ReactRouterを使ってGoogleログイン前と後で画面を変える
  3. ReactRouterを使ってGoogleログイン前と後で画面を変える(続き)

今日は、Google SignInでログインした後、自分のサーバーサイドアプリケーションとのセッション管理について考えてみます。

IDトーク

GoogleSignInと自前サーバーサイドアプリケーションの連携は、ここに書いてある通りやれば良い。

https://developers.google.com/identity/sign-in/web/backend-auth

強調して書かれてるけど、ログイン後のコールバック関数で取得できるユーザー情報をサーバーサイドに渡してはいけない、って書いてある。IDトークンを渡しましょう。

下で取得してるuserIdをサーバーサイドに渡してはダメ。

  onSignIn = (googleUser) => {
    userId = googleUser.getBasicProfile().getId());
  }

代わりにIDトークンを渡す。

onSignIn = (googleUser) => {
    idToken = googleUser.getAuthResponse().id_token;
}

Googleのドキュメントに明記はされてないのだけど、OpenID ConnectのIDトークンと一緒だと思う。

IDトークンの検証

クライアントは、GoogleからもらったIDトークンをサーバーへ送信する。

サーバーアプリケーションは、受け取ったIDトークンの検証をする必要がある。検証方法も先ほどのGoogleのドキュメントに書いてある。自分でやる方法、Googleから提供されてるライブラリを使う方法、Web APIとして公開されてるtokeninfoエンドポイントを使う方法、3つある。

今回は、tokeninfoエンドポイントを使う。

セッション管理

検証がうまくいったらサーバーサイドアプリケーションで、セッションを作成する(初めてログインしてきた場合はユーザーデータの作成もする)。

セッションをどう管理するか?セッション情報をどこに保持するかと、クライアントサーバー間でどうやりとりするかの2つ考えることがある。

セッション情報をどこに保持するか?

  1. サーバー側のデータストアに格納して、そのキーとなる文字列をクライアントとの通信で使う
    • Pros
      • クライアントとやり取りするデータがキーのみなので小さい
    • Cons
      • セッション情報をデータストアに保持するので、アクセスが必要(データストアは、アプリケーションサーバーとは別のサーバーになることが多くサーバー間通信が発生)
      • アプリケーション内メモリに保持するのであれば、(通常複数ある)アプリケーションサーバー間で共有する工夫が必要
        • もしくは、ロードバランスを工夫して同じサーバーにアクセスがいくようにする
  2. JWTを使う
    • Pros
      • セッション情報をJWT自身に保持させることができるので、データストアから取り出す必要がない
        • と思ったけど、本当にデータストアへのアクセスはなくせるのか?例えばサーバー側からセッション切れにしたりしたい場合はどうするのだろう?
    • Cons
      • セッション情報が増えるとJWTのサイズが大きくなる

クライアントサーバー間でどうやりとりするか?

  1. Cookieを使う
    • Pros
      • securehttpOnly をつけておけば、XSS脆弱性があったとしてもJWTやセッションキーが外部に漏れない
    • Cons
      • CSRFの対策が必要
  2. レスポンスボディとリクエストヘッダーで通信して、クライアント側での保存にはlocalStrage ( or sessionStrage )を使う
    • Pros
      • 特になし?
    • Cons
      • XSS脆弱性があると、JWTやセッションキーが外部に漏れる

今回は、サーバーサイドのデータストアにセッション情報を格納、クライアントサーバー間はCookieでやりとりするようにする。

ここまでを実装

実装方針をまとめる。

  1. クライアントは、GoogleSignInに成功したらGoogleからもらったIDトークンをサーバーアプリケーションへ送る
  2. サーバーアプリケーションは、Googleトークンエンドポイントを使ってIDトークンの検証をする
  3. サーバーアプリケーションは、検証に成功したらセッション情報を作成しデータストアに保存する
  4. サーバーアプリケーションは、Set-Cookieヘッダーをセットしたレスポンスを返す
  5. クライアントは、以降サーバーアプリケーションにCookieを送信することで、認証が必要なWebAPIを使うことができる

サーバーサイド

Goでサーバーサイド書いた。サーバーサイドでやることはさっきのステップのうち2,3,4。データストアは map で代用。5555ポートで動かす。

HTML, JavaScriptも同じサーバーから配布する方法もあるけど、今回はCORSの設定をしてクライアントサーバー間で通信できるようにした。

コードは記事の最後に貼っておきます。

クライアントサイド

クライアントサイドの実装。上記の5ステップのうち、1,5がクライアントサイドでやること。

  1. ログインに成功したらGoogleからもらったIDトークンをサーバーサイドのエンドポイント( http://localhost:5555/login )へ送る
  2. サーバーアプリケーションにCookieを送信することで、認証が必要なサーバーサイドのエンドポイント( http://localhost:5555/my )を使ってプライベート情報を取得できる
@@ -54,7 +54,22 @@ class App extends Component {
   }
   onSignIn = (googleUser) => {
     console.log('ID: ' + googleUser.getBasicProfile().getId());
-    this.setState({authenticated: true})
+    fetch("http://localhost:5555/login", {
+      method: "POST",
+      mode: 'cors',
+      credentials: 'include',
+      headers: new Headers({ 'Content-Type': 'application/json' }),
+      body: JSON.stringify({ "IDToken": googleUser.getAuthResponse().id_token }),
+    })
+    .then(response => {
+      return fetch("http://localhost:5555/my", {
+        mode: 'cors',
+        credentials: 'include',
+      })
+    })
+    .then(response => {
+      response.json().then(data => console.log(data))
+      this.setState({authenticated: true})
+    })
   }
   onSignOut = () => {
     const auth2 = this.state.gapi.auth2.getAuthInstance();

ログアウト

この時点でセッションは2つある。

  1. 自分のアプリケーションのセッション
  2. Googleのセッション

Googleサインインは認証を肩代わりしてくれだけとみなして、セッション管理は自分のアプリケーションのセッションだけとした方が管理しやすそう。なので、IDトークンをサーバーサイドに送った以降は、Googleのことは気にしない。

ログアウトボタンの挙動を変える必要がある。

あと、gapiをAppのstateに持たせるのも止める。 そうすれば、1日目の日記で採用しないことにした React-Google-Loginも使える。

まずは、React-Google-Loginを導入して、Appのstateからgapiを消す。

$ npm install react-google-login
@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import { BrowserRouter as Router, Switch, Route, Link, Redirect } from 'react-router-dom';
+import { GoogleLogin } from 'react-google-login';
 
 const PrivateRoute = ({authenticated, render, ...rest}) => (
   authenticated ? (
@@ -22,34 +23,20 @@ const PrivateRoute = ({authenticated, render, ...rest}) => (
 class App extends Component {
   state = {
     authenticated: false,
-    gapi: null
+    initialized: null
   }
   componentDidMount() {
-    this.downloadGoogleScript(this.initSignInButton)
-  }
-  downloadGoogleScript = (callback) => {
-    const element = document.getElementsByTagName('script')[0];
-    const js = document.createElement('script');
-    js.id = 'google-platform';
-    js.src = '//apis.google.com/js/platform.js';
-    js.async = true;
-    js.defer = true;
-    element.parentNode.insertBefore(js, element);
-    js.onload = () => callback(window.gapi);
-  }
-  initSignInButton = (gapi) => {
-    gapi.load('auth2', () => {
-      gapi.auth2.init({client_id: "<CLIENT_ID>"})
-        .then(
-          (result) => {
-            if (result.isSignedIn.get()) {
-              this.setState({authenticated: true, gapi})
-            } else {
-              this.setState({authenticated: false, gapi})
-            }
-          },
-          (err) => console.error(err)
-        );
+    fetch("http://localhost:5555/my", {
+      mode: 'cors',
+      credentials: 'include',
+    })
+    .then(response => {
+      if (response.status === 200) {
+        response.json().then(data => console.log(data))
+        this.setState({ authenticated: true, initialized: true })
+      } else {
+        this.setState({ authenticated: false, initialized: true })
+      }
     })
   }
   onSignIn = (googleUser) => {
@@ -78,7 +65,7 @@ class App extends Component {
     this.setState({authenticated: false})
   }
   render() {
-    if (!this.state.gapi) {
+    if (!this.state.initialized) {
       return ('Loading...')
     }
     return (
@@ -88,7 +75,7 @@ class App extends Component {
             <Top {...props} authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
           }/>
           <Route path="/login" render={props =>
-            <Login {...props} onSignIn={this.onSignIn} gapi={this.state.gapi} authenticated={this.state.authenticated} />
+            <Login {...props} onSignIn={this.onSignIn} authenticated={this.state.authenticated} />
           }/>
           <PrivateRoute path="/private" authenticated={this.state.authenticated} render={props =>
             <div>プライベートなページ</div>
@@ -102,14 +89,6 @@ export default App;
 
 
 class Login extends Component {
-  componentDidMount() {
-    if (this.props.gapi) {
-      this.props.gapi.signin2.render('google-signin-button', {
-        'onsuccess': this.props.onSignIn,
-        'onfailure': (err) => console.error(err)
-      });
-    }
-  }
   render() {
     if (this.props.authenticated) {
       const { from } = this.props.location.state || { from: { pathname: "/" } };
@@ -118,7 +97,14 @@ class Login extends Component {
     return (
       <div>
         <h1>ようこそ!</h1>
-        <div id="google-signin-button"></div>
+        <div>
+          <GoogleLogin
+            clientId="<CLIENT_ID>"
+            buttonText="Login"
+            onSuccess={this.props.onSignIn}
+            onFailure={(err) => console.error(err)}
+          />
+        </div>
         <Link to='/'>Topへ</Link>
       </div>
     )

ログアウト時の処理を変えて、サーバーサイドアプリケーションのログアウトAPIを呼ぶようにする。

@@ -60,9 +60,16 @@ class App extends Component {
     })
   }
   onSignOut = () => {
-    const auth2 = this.state.gapi.auth2.getAuthInstance();
-    auth2.signOut().then(() => console.log('sign out'));
-    this.setState({authenticated: false})
+    fetch("http://localhost:5555/logout", {
+      method: "POST",
+      mode: 'cors',
+      credentials: 'include',
+      headers: new Headers({ 'Content-Type': 'application/json' }),
+    })
+    .finally(response => {
+      console.log(response)
+      this.setState({authenticated: false})
+    })
   }
   render() {
     if (!this.state.initialized) {

おしまい

これでできた。セッションタイムアウトや強制ログアウトはサーバーサイドのセッション情報を適切に消してあげれば良い。JWTを使う場合はJWT自身の有効期限でセッションタイムアウトは実現できるけど強制ログアウトさせたい場合に良い方法あるのかな、という点は分かっていない。

書いたコード。

Google SignIn + Server App (Go)

ReactRouterを使ってGoogleログイン前と後で画面を変える(続き)

前回前々回に引き続き、Google Sign-In をReactなSPAで使った時の日記です。

今回は、前回できなかった /login/private のルーティングを実装する。

  • /login
    • ログイン前: ログインページを表示 => ログインに成功すると / へ遷移
    • ログイン後: / へ遷移
  • /private
    • ログイン前: ログインページへ遷移 => ログインに成功するとアクセスしたパス( /private )へ遷移
    • ログイン後: プライベート画面を表示

前回終了時点のコード。

create-react-app でプロジェクトを生成した後、 src/App.js だけ編集してる。

/login

/login をやってみる。

  • /login
    • ログイン前状態: ログインページを表示 => ログインに成功すると / へ遷移
    • ログイン後状態: / へ遷移

現状だとログインに成功しても自動で / に遷移しない。ReactRouterの <Redirect /> を使うと、自動で遷移するようにできる。

React Routerのドキュメント を読むと、 <Redirect> を使ってナビゲーションしろ、と書いてある。

Login コンポーネントにも authenticated をpropsで渡して、true だったら <Redirect to='/login' /> をrederしてTopページに遷移するようにする。

--- a/src/App.js
+++ b/src/App.js
@@ -1,5 +1,5 @@
 import React, { Component } from 'react';
-import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
+import { BrowserRouter as Router, Switch, Route, Link, Redirect } from 'react-router-dom';
 
 class App extends Component {
   state = {
@@ -54,7 +54,7 @@ class App extends Component {
             <Top authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
           }/>
           <Route path="/login" render={props =>
-            <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/>
+            <Login onSignIn={this.onSignIn} gapi={this.state.gapi} authenticated={this.state.authenticated} />
           }/>
         </Switch>
       </Router>
@@ -74,6 +74,9 @@ class Login extends Component {
     }
   }
   render() {
+    if (this.props.authenticated) {
+      return (<Redirect to='/' />)
+    }
     return (
       <div>
         <h1>ようこそ!</h1>

できた。 <Redirect /> を使った遷移は、historyに対してpushするのではなくreplaceするみたいなので、ブラウザの戻るボタンを押されても大丈夫。

https://reacttraining.com/react-router/web/api/Redirect

この修正で、ログイン後状態にアクセスした場合も対応できた。

以下のルーティングができた。

  • /login
    • ログイン前: ログインページを表示 => ログインに成功すると / へ遷移
    • ログイン後: / へ遷移

未ログインだとアクセスできないページ

  • /private
    • ログイン前状態: ログインページへ遷移 => ログインに成功するとアクセスしたパス( /private )へ遷移
    • ログイン後状態: プライベート画面を表示

ログイン前状態だとアクセスできないページをやる。ログイン成功時のリダイレクト先を動的にして、最初にアクセスしたパスに遷移させるのが難しそう。

ReactRouterのドキュメントに "Redirects (Auth)" というサンプル があったので、それを参考に <PrivateRoute> というものを実装する。

--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,24 @@
 import React, { Component } from 'react';
 import { BrowserRouter as Router, Switch, Route, Link, Redirect } from 'react-router-dom';
 
+const PrivateRoute = ({authenticated, render, ...rest}) => (
+  authenticated ? (
+    <Route {...rest} render={render} />
+  ) : (
+    <Route
+      {...rest}
+      render={props =>
+        <Redirect
+          to={{
+            pathname: "/login",
+            state: {from: props.location}
+          }}
+        />
+      }
+    />
+  )
+);
+
 class App extends Component {
   state = {
     authenticated: false,
@@ -51,10 +69,13 @@ class App extends Component {
       <Router>
         <Switch>
           <Route exact path="/" render={props => 
-            <Top authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
+            <Top {...props} authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
           }/>
           <Route path="/login" render={props =>
-            <Login onSignIn={this.onSignIn} gapi={this.state.gapi} authenticated={this.state.authenticated} />
+            <Login {...props} onSignIn={this.onSignIn} gapi={this.state.gapi} authenticated={this.state.authenticated} />
+          }/>
+          <PrivateRoute path="/private" authenticated={this.state.authenticated} render={props =>
+            <div>プライベートなページ</div>
           }/>
         </Switch>
       </Router>

まず、 <PrivateRoute> というのを作って、 <Switch> の中のルーティングで使う。 <PrivateRoute>authenticated を渡し、ログイン済みかどうかで分岐。

ログイン済みである場合

ログイン済みであれば、 <PrivateRoute>render で渡された関数にルーティングする。今回の例だと <div>プライベートなページ</div> をrenderする。

<Routing>{...rest} と指定することで、 <PrivateRoute> に明記していないpropsをそのまま <Route> にも引き継いでいる)

ログイン済みではない場合

ログイン済みではない場合、 <Redirect> をrenderすることで /login へ遷移させる。この時に重要なのが、 state: {from: props.location} という指定。

<Route render={props =>propsroute props というものであり、 route propsの location でアクセスされたパスを取得できる。つまり、今回は /private という値が取れる。この値を <Redirect to= に指定するオブジェクトの state 経由で渡す( to: object)。

Redirect先のコンポーネントで渡された値( /private )を取得できる。

@@ -75,7 +96,8 @@ class Login extends Component {
   }
   render() {
     if (this.props.authenticated) {
-      return (<Redirect to='/' />)
+      const { from } = this.props.location.state || { from: { pathname: "/" } };
+      return (<Redirect to={from} />)
     }
     return (
       <div>

<Login>const { from } = this.props.location.state || { from: { pathname: "/" } }; として、渡された最初にアクセスしたパス( /private )を取得し、ログイン成功時にそのパスへ遷移させる。

これで以下のルーティングもできた。

  • /private
    • ログイン前: ログインページへ遷移 => ログインに成功するとアクセスしたパス( /private )へ遷移
    • ログイン後: プライベート画面を表示

おしまい

こんな感じでいいのかな。ベストプラクティスのようなものあったら知りたいのだけど見つけられなかった。次回はサーバーサイドアプリケーションとのセッション管理を考える。

こういうコードになりました。

Google SignIn + React + React Router v2 ( http://b ...

ReactRouterを使ってGoogleログイン前と後で画面を変える

前回に引き続きGoogle Sign-In をReactなSPAで使ってみた時の日記、2日目です。

今回はReactRouterを使って、ログイン前と後で画面を変えてみます。

ReactRouterを導入

ReactRouterを使う。バージョンは 4.2.2

$ npm install --save-prod react-router-dom

+ react-router-dom@4.2.2

とりあえず、以下のようにルーティングする。

  • / にアクセスするとトップページ
  • /login にアクセスするとログインページ

前回のコードを元に書き換えたのが以下。これを元に書いていく。

// src/App.js

import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';

class App extends Component {
  render() {
    return (
      <Router>
        <Switch>
          <Route exact path="/" component={Top}/>
          <Route path="/login" component={Login}/>
        </Switch>
      </Router>
    )
  }
}
export default App;


class Login extends Component {
  componentDidMount() {
    this.downloadGoogleScript(this.initSignInButton)
  }
  onSignIn = (googleUser) => {
    console.log('ID: ' + googleUser.getBasicProfile().getId());
  }
  downloadGoogleScript = (callback) => {
    const element = document.getElementsByTagName('script')[0];
    const js = document.createElement('script');
    js.id = 'google-platform';
    js.src = '//apis.google.com/js/platform.js';
    js.async = true;
    js.defer = true;
    element.parentNode.insertBefore(js, element);
    js.onload = () => callback(window.gapi);
  }
  initSignInButton = (gapi) => {
    gapi.load('auth2', () => {
      gapi.auth2.init({client_id: <CLIENT_ID>})
        .then(
          (result) => {
            gapi.signin2.render('google-signin-button', {
              'onsuccess': this.onSignIn,
              'onfailure': (err) => console.error(err)
            });
          },
          (err) => console.error(err)
        );
    })
  }
  render() {
    return (
      <div>
        <h1>ようこそ!</h1>
        <div id="google-signin-button"></div>
        <Link to='/'>Topへ</Link>
      </div>
    )
  }
}

class Top extends Component {
  render() {
    return (
      <div>
        <h1>Topページ</h1>
        <Link to='/login'>サインイン</Link>
      </div>
    )
  }
}

ルーティングの設計

/login へのアクセスをログイン前か後かで挙動を変える。それからログイン後でないとアクセスできないページとして /private を用意することにする。

  • /
    • ログイン前状態: ログイン前トップページを表示
    • ログイン後状態: ログイン後トップページを表示
  • /login
    • ログイン前状態: ログインページを表示 => ログインに成功すると / へ遷移
    • ログイン後状態: / へ遷移
  • /private
    • ログイン前状態: ログインページへ遷移 => ログインに成功するとアクセスしたパス( /private )へ遷移
    • ログイン後状態: プライベート画面を表示

トップページ

まずは、 / をやってみる。

  • /
    • ログイン前: ログイン前トップページを表示
    • ログイン後: ログイン後トップページを表示

ログイン済かどうかを保持するフラグを state に持たせる。ログイン後のcallbackである onSignInLogin コンポーネントから App へ移動して state を書き換えるようにする。

--- a/src/App.js
+++ b/src/App.js
@@ -2,13 +2,16 @@ import React, { Component } from 'react';
 import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
 
 class App extends Component {
+  state = {
+    authenticated: false
+  }
+  onSignIn = (googleUser) => {
+    console.log('ID: ' + googleUser.getBasicProfile().getId());
+    this.setState({authenticated: true})
+  }
   render() {
     return (
       <Router>
         <Switch>
           <Route exact path="/" component={Top}/>
-          <Route path="/login" component={Login}/>
+          <Route path="/login" render={props =>
+            <Login onSignIn={this.onSignIn} />
+          }/>
         </Switch>
       </Router>
@@ -22,9 +25,6 @@ class Login extends Component {
   componentDidMount() {
     this.downloadGoogleScript(this.initSignInButton)
   }
-  onSignIn = (googleUser) => {
-    console.log('ID: ' + googleUser.getBasicProfile().getId());
-  }
   downloadGoogleScript = (callback) => {
     const element = document.getElementsByTagName('script')[0];
     const js = document.createElement('script');
@@ -41,7 +41,7 @@ class Login extends Component {
         .then(
           (result) => {
             gapi.signin2.render('google-signin-button', {
-              'onsuccess': this.onSignIn,
+              'onsuccess': this.props.onSignIn,
               'onfailure': (err) => console.error(err)
             });
           },

認証済みかどうかを Top コンポーネントに渡して見出しの文字列を変える。

--- a/src/App.js
+++ b/src/App.js
@@ -13,7 +13,9 @@ class App extends Component {
     return (
       <Router>
         <Switch>
-          <Route exact path="/" component={Top}/>
+          <Route exact path="/" render={props => 
+            <Top authenticated={this.state.authenticated} />
+          }/>
           <Route path="/login" render={props =>
             <Login onSignIn={this.onSignIn} />
           }/>
@@ -66,9 +68,10 @@ class Login extends Component {
 
 class Top extends Component {
   render() {
+    const header = this.props.authenticated ? 'ログイン済み:Topページ' : 'ログイン前:Topページ';
     return (
       <div>
-        <h1>Topページ</h1>
+        <h1>{header}</h1>
         <Link to='/login'>サインイン</Link>
       </div>
     )

これで以下の流れができた。

  1. localhost:3000/ にアクセスすると「ログイン前:Topページ」と表示
  2. 「サインイン」リンクからログイン画面( /login )へ遷移
  3. Googleサインインでログイン
  4. 「Topへ」リンクからトップページ( / )へ遷移
  5. 「ログイン済み:Topページ」と表示

再アクセス時の挙動

ここで2つ程考えないといけないことが。

1つ目は、上のステップ5の後にブラウザをリロードしたりアドレスバーから直接アクセスした場合にログイン前状態になる、という点。これはログイン状態を state に保存しているだけだから。この挙動をどうするか。

2つ目は、リロードでログイン前状態になってしまった後、上のステップ3をやらなくてもログイン状態となる、という点。これはCookieが残るのでログイン画面( /login )に遷移しGoogleサインインボタンをレンダリングするだけで、以下の onsuccess コールバックが実行される。この挙動をどうするか。

gapi.signin2.render('google-signin-button', {
  'onsuccess': this.props.onSignIn,
  'onfailure': (err) => console.error(err)
});

セッション管理のCookieがあるから、Googleサインインがログイン成功のコールバックを実行するのは良い。だけど、ログインボタンをレンダリングする前にアプリケーション側で把握したい。それができれば上の2つとも対応できる。

アプリケーション側でログイン中かどうかを判定するには、ログイン状態を state だけでなくsessionStorageに保存すれば良いかなぁと最初は思った。有効期限にログイン時成功時に取得できる gapi.auth2.AuthResponse の有効期限を使って。

でも、Cookieの有効期限を使えば良いんじゃないかなぁと思いつつ、Googleのドキュメントを眺めてると、GoogleAuth.isSignedIn.get()というのを発見。これを使おう。

Googleサインインのgapi.auth2の初期化を Login コンポーネントでやってるけど、 App コンポーネントに移す。 Login コンポーネントではSignInボタンをレンダリングするときに gapi オブジェクトが必要なんだよな・・・。 props で渡すか。。

--- a/src/App.js
+++ b/src/App.js
@@ -3,7 +3,36 @@ import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
 
 class App extends Component {
   state = {
-    authenticated: false
+    authenticated: false,
+    gapi: null
+  }
+  componentDidMount() {
+    this.downloadGoogleScript(this.initSignInButton)
+  }
+  downloadGoogleScript = (callback) => {
+    const element = document.getElementsByTagName('script')[0];
+    const js = document.createElement('script');
+    js.id = 'google-platform';
+    js.src = '//apis.google.com/js/platform.js';
+    js.async = true;
+    js.defer = true;
+    element.parentNode.insertBefore(js, element);
+    js.onload = () => callback(window.gapi);
+  }
+  initSignInButton = (gapi) => {
+    gapi.load('auth2', () => {
+      gapi.auth2.init({client_id: <CLIENT_ID>})
+        .then(
+          (result) => {
+            if (result.isSignedIn.get()) {
+              this.setState({authenticated: true, gapi})
+            } else {
+              this.setState({authenticated: false, gapi})
+            }
+          },
+          (err) => console.error(err)
+        );
+    })
   }
   onSignIn = (googleUser) => {
     console.log('ID: ' + googleUser.getBasicProfile().getId());
@@ -17,7 +46,7 @@ class App extends Component {
             <Top authenticated={this.state.authenticated} />
           }/>
           <Route path="/login" render={props =>
-            <Login onSignIn={this.onSignIn} />
+            <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/>
           }/>
         </Switch>
       </Router>
@@ -29,31 +58,12 @@ export default App;
 
 class Login extends Component {
   componentDidMount() {
-    this.downloadGoogleScript(this.initSignInButton)
-  }
-  downloadGoogleScript = (callback) => {
-    const element = document.getElementsByTagName('script')[0];
-    const js = document.createElement('script');
-    js.id = 'google-platform';
-    js.src = '//apis.google.com/js/platform.js';
-    js.async = true;
-    js.defer = true;
-    element.parentNode.insertBefore(js, element);
-    js.onload = () => callback(window.gapi);
-  }
-  initSignInButton = (gapi) => {
-    gapi.load('auth2', () => {
-      gapi.auth2.init({client_id: <CLIENT_ID>})
-        .then(
-          (result) => {
-            gapi.signin2.render('google-signin-button', {
-              'onsuccess': this.props.onSignIn,
-              'onfailure': (err) => console.error(err)
-            });
-          },
-          (err) => console.error(err)
-        );
-    })
+    if (this.props.gapi) {
+      this.props.gapi.signin2.render('google-signin-button', {
+        'onsuccess': this.props.onSignIn,
+        'onfailure': (err) => console.error(err)
+      });
+    }
   }
   render() {
     return (

これで、ログイン後再アクセスした場合でも、「ログイン前:Topページ」ではなく「ログイン済み:Topページ」と表示されるようになった!

ローディング中表示

だけど、 gapi.auth2.init の処理が終わるまでの間「ログイン前:Topページ」と表示されてしまう。ローディング中の表示をしよう。

--- a/src/App.js
+++ b/src/App.js
@@ -39,6 +39,9 @@ class App extends Component {
     this.setState({authenticated: true})
   }
   render() {
+    if (!this.state.gapi) {
+      return ('Loading...')
+    }
     return (
       <Router>
         <Switch>

ログアウト

続いてログアウトボタンを追加。

--- a/src/App.js
+++ b/src/App.js
@@ -38,6 +38,11 @@ class App extends Component {
     console.log('ID: ' + googleUser.getBasicProfile().getId());
     this.setState({authenticated: true})
   }
+  onSignOut = () => {
+    const auth2 = this.state.gapi.auth2.getAuthInstance();
+    auth2.signOut().then(() => console.log('sign out'));
+    this.setState({authenticated: false})
+  }
   render() {
     if (!this.state.gapi) {
       return ('Loading...')
@@ -46,7 +51,7 @@ class App extends Component {
       <Router>
         <Switch>
           <Route exact path="/" render={props => 
-            <Top authenticated={this.state.authenticated} />
+            <Top authenticated={this.state.authenticated} onSignOut={this.onSignOut} />
           }/>
           <Route path="/login" render={props =>
             <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/>
@@ -81,12 +86,20 @@ class Login extends Component {
 
 class Top extends Component {
   render() {
-    const header = this.props.authenticated ? 'ログイン済み:Topページ' : 'Topページ';
-    return (
-      <div>
-        <h1>{header}</h1>
-        <Link to='/login'>サインイン</Link>
-      </div>
-    )
+    if (this.props.authenticated) {
+      return (
+        <div>
+          <h1>ログイン済み:Topページ</h1>
+          <button onClick={this.props.onSignOut}>ログアウト</button>
+        </div>
+      )
+    } else {
+      return (
+        <div>
+          <h1>ログイン前:Topページ</h1>
+          <Link to='/login'>サインイン</Link>
+        </div>
+      )
+    }
   }
 }

おしまい

今日はここまで。次回は、ログインページ( /login ) とログイン後じゃないとアクセスできないページ( /private )をやっていく。

ここまでの結果。