iOSでPWAとして起動されたときにLocalStorageに保存したtokenを利用してオートログインさせる


# 背景
iOS(11.3時点)のPWAではCookieが起動ごとに初期化され、Session(Cookie)を利用したログイン状態の保持ができません。追加調査でLocalStorageについては初期化されないことがわかったため、LocalStorageを利用したオートログインの実現について実験しました

来週開催されるWWDCやiOS12で解消されるのを切に願ってます😭

## 過去の調査内容

- [【調査】WEBアプリをPWAとして起動した場合にブラウザのCookieが引き継がれるか確認する \- Qiita](https://qiita.com/NaokiIshimura/items/19073a45f994129c40b1)
- [【調査】WEBアプリをPWAとして起動した場合にブラウザのlocalStorageが引き継がれるか確認する \- Qiita](https://qiita.com/NaokiIshimura/items/742a2a236832456614c5)

# サンプルアプリ
調査のために作成したサンプルアプリは今後もiOS, Androidバージョンアップ時に利用する可能性がありそうなためHerokuで公開しておきました。（無料プランのためしばらくアクセスがなかった場合はページが表示されるまで少し時間がかかります）

https://pwa-autologin-test-app.herokuapp.com/ 

![pwa-autologin-test-app.png](https://qiita-image-store.s3.amazonaws.com/0/141934/406f454f-25d9-2fe6-8459-d3e1de10e5da.png)

Loginに利用するユーザ名／パスワードは以下をご利用ください。

- name : test
- password: hoge

# ソースコード

[NaokiIshimura/Qiita\-PWA\-AutoLogin](https://github.com/NaokiIshimura/Qiita-PWA-AutoLogin)

# アプリの説明

- ブラウザやAndroidでログインするとSession/Cookieにuser.id保存する。
- iOSでPWAとして起動した後にログインするとSession/Cookieにuser.id、LocalStorageにtokenを保存する。
- iOSでPWAとして起動した際にLocalStorageにtokenが保存されてるとオートログインを実行する

## ブラウザで利用する場合

PC/iOS/Androidブラウザからアクセスしてログインした場合は、tokenを利用したオートログインは不要なためSession(Cookie)のみ保存します。

1. ログインページへ遷移する（↓）
![normal01.png](https://qiita-image-store.s3.amazonaws.com/0/141934/90770fe4-c0f1-9c20-9eff-1f20c53b0006.png)

2. ログインフォームでName/Passwordを入力してログインする（↓）
![normal02.png](https://qiita-image-store.s3.amazonaws.com/0/141934/8e4ba2f7-4b88-fa46-f08d-cea503103872.png)

3. ログインに成功するとSessionとCookieにuser.idを保存する/tokenは保存しない（↓）
![normal03.png](https://qiita-image-store.s3.amazonaws.com/0/141934/68925590-eac5-f59f-3f07-c21a988b8631.png)

## iOSでPWAとして利用する場合
iOSでPWAとして利用する場合は専用のフォームが利用され、Session(Cookie)に加えてオートログインで利用するtokenも保存します。

1. ホームスクリーンからアプリを起動する（↓）
![ios01.png](https://qiita-image-store.s3.amazonaws.com/0/141934/49717fd4-8f12-1875-28fb-a0be9092e481.png)

2. 初回はiosPWA専用のログインフォームでName/Passwordを入力してログインする（↓）
![ios02.png](https://qiita-image-store.s3.amazonaws.com/0/141934/57c4efb8-e89b-ca93-81cc-834579409a06.png)

3. ログインに成功するとSessionとCookieにuser.idを保存し、LocalStorageにtokenを保存する（↓）
![ios03.png](https://qiita-image-store.s3.amazonaws.com/0/141934/3126a381-6411-3d82-81bf-3dfb01e0719f.png)

tokenが保存された状態でPWAとして起動すると、オートログインを実行します。

4. ホームスクリーンからアプリを起動する（↓）
![ios04.png](https://qiita-image-store.s3.amazonaws.com/0/141934/0d64d7a6-2d8a-6b96-8885-ba464f784e99.png)

5. LocalStorageに保存されたtokenを利用してオートログインが実行される（↓）
![ios05.png](https://qiita-image-store.s3.amazonaws.com/0/141934/56f25d6e-dcc5-4003-2e97-29e462067b4b.png)


## AndroidでPWAとして利用する場合
AndroidでPWAとして利用する場合もSession(Cookie)のみ保存します。

1. ホームスクリーンからアプリを起動する（↓）
![android01.png](https://qiita-image-store.s3.amazonaws.com/0/141934/0d14b38d-84ef-a325-925e-bc297447a52d.png)

2. ログインフォームでName/Passwordを入力してログインする（↓）
![android02.png](https://qiita-image-store.s3.amazonaws.com/0/141934/7a0609a0-3799-c023-7369-5493ccd582b1.png)

3. ログインに成功するとSessionとCookieにuser.idを保存する/tokenは保存しない（↓）
![android03.png](https://qiita-image-store.s3.amazonaws.com/0/141934/dfc3a1ff-94ad-2480-5599-de9bbd137baa.png)

---

# ログイン方法、個体識別番号(uuid)について

海外のフォーラムにおいてもiOSのPWAにおいてSession(Cookie)が保持されないことに対する解決策については議論されており、自分が調べた感じでは以下の解決案が提示されていました。

## 解決案１

LocalStorageに個体識別情報(uuid)やトークンなどを保存させて、PWA起動時のオートログインで利用する

## 解決案２

start_urlに個体識別番号(uuid)を埋め込んで、PWA起動時のオートログインで利用する

例：https://xxx.xxx.xxx.xxx/login?uuid=xxxxxxxxxx

## 解決案３

オフラインキャッシュさせるファイル(html, jsなど)に個体識別番号(uuid)を埋め込んで、、PWA起動時のオートログインで利用する

## 補足：Credential Management API

ログインをサポートする**Credential Management API**についてはiOSではまだサポートされていないため、利用することができません。

[Can I use\.\.\. Support tables for HTML5, CSS3, etc](https://caniuse.com/#search=Credential%20Management%20API)



## 所感

- 解決案１：一番オーソドックスで実装も簡単そう🧐
- 解決案２：簡単に書き換え可能なURL部分にuuidを埋め込むのは少し怖い😇
- 解決案３：一部のPWAアプリはこの方法をとってみたいだけど、キャッシュの挙動を理解してないと採用するのは怖い😇

「LocalStorageに個体識別情報(uuid)やトークンなどを保存させて、PWA起動時のオートログインで利用する」が一番カンタンそうなので試してみることにしました😊

---

# 実装

ログイン部分のみ抜粋して解説させていただきます。
サーバサイドはRailsを利用してます。
不明点がありましたらコメントしていただけると助かります。

## OS/起動契機の判定

以下の2つの条件がtrueの場合にログイン画面を「iOS PWA」用に切り替えてます。

- URLに「?launcher=true」が含まれている
- UserAgentから**iOS**であると判断できる

```ruby:app/controllers/sessions_controller.rb
    require 'browser'
    @pwa = true if (params['launcher'] == 'true' && browser.platform.ios? )
```

> UserAgentの判定には**browser**というgemを利用してます。


## ログイン時の動作

ログインフォームの挙動は「iOS PWA」と「それ以外」で分けてあり、「iOS PWA」の場合は`remote: true`となり、Ajax通信でログインを実行します。ログインに成功すると`create.js.erb`でLocalStorageにtokenを保存します。

「それ以外」の場合は`remote: false`となり、通常のHTTP通信でログインが実行され、LocalStorageにtokenを保存する処理が行われないようにしてあります。

ログイン成功後はSessionやCookieを利用してユーザ情報を管理したいので、SessionとCookieにuser.idを保存するのは共通の処理としてあります。

```erb:/views/sessions/new.html.erb
<%= form_for :session, url: sessions_path, remote: (@pwa == true ? true : false) do |f| %>
  <!-- 省略 -->
<% end%>
```

```ruby:app/controllers/sessions_controller.rb
def create
  # 省略
  session[:user_id] = user.id
  cookies[:user_id] = user.id
  # 省略
  respond_to do |format|
    format.html { redirect_to root_path }
    format.js { @token = user.token }
  end
  # 省略
end
```

```erb:app/views/sessions/create.js.erb
// LocalStorageにtokenを保存する
localStorage.setItem("token", "<%= @token %>");
// root_pathへ移動
window.location.href = '<%= root_path %>';
```

## ログアウト時の動作

ログアウト時の動作は共通で、**sessionリセット**、**Cookieからuser_id削除**、**LocalStorageからtoken削除**を実行します。

```ruby:app/controllers/sessions_controller.rb
def destroy
    # 省略
    reset_session
    cookies.delete :user_id
    # 省略
end
```

```erb:app/views/sessions/destroy.js.erb
// LocalStorageからtokenを削除する
localStorage.removeItem("token");
// root_pathへ移動
window.location.href = '<%= root_path %>';
```

## オートログイン

start_urlにはログイン画面へのパスとPWA起動を示すパラメータが指定されているので、iOS端末がPWAとして起動させると必ずログイン画面が表示されます。

- PWAとして起動された(URLに?launcher=trueが付与されている)
- UserAgentからiOSであると判断できる
- 未ログイン状態である
- LocalStorageにtokenが保存されている

という条件が揃うと以下のJavaScript内でAjax通信が実行され自動でログインを試みます。

```erb:app/views/sessions/new.html.erb
<!-- 省略 -->
<% if @pwa %>
  <script>
    // ログインしてない場合、
    <% if logged_in? == false %>
    $(function () {
      // autoLoginを実行させる
      autoLogin();
    });
    <% end %>

    function autoLogin() {

      console.log("[debug] auto login");

      // LocalStorageからtokenを取得
      var token = localStorage.getItem("token");

      // tokenが空じゃない場合、
      if (token != null) {
        console.log("[debug] Token exists");

        // POSTするjsonを作成
        var data = {
          "authentication":
            {
              token: token
            }
        };
        // Ajaxでtoken認証を実施
        $.ajax({
          type: "post",
          url: "<%= authentication_path %>",
          data: JSON.stringify(data),
          contentType: 'application/json',
          dataType: "json",

          // token認証を成功した場合、
          success: function (data) {
            console.log("[debug] auto login success");
            // root_pathへページ遷移
            window.location.href = '<%= root_path %>';
          },

          // token認証に失敗した場合、
          error: function (data) {
            console.log("[debug] auto login fail");
          }
        });

      } else {
        console.log("[debug] No token exists");
      }
    }
  </script>
<% end %>
```

---

# 実装後に思ったこと

個人的にはオートログインに利用する情報がtoken 1つだけだと心もとないので、AWSのCLIなどのように2種類のtoken(key)を利用したり、tokenに利用期限を設けたり、定期的にリフレッシュさせたりする必要があると感じた。

---

# 参考

## ユーザ認証

ユーザ認証の実装については以下の記事を参考にさせていただきました。

- [メールアドレスとパスワードによる基本的なユーザー認証 \- Qiita](https://qiita.com/nullabletypo/items/6d39617d969a1e8ca4e7)

## token認証

token認証の実装については以下の記事を参考にさせていただきました。

- [Rails5 APIで認証付きのWebAPIを作ってみる \- Qiita](https://qiita.com/ochiochi/items/966b884eb17045dfb929)
- [【Rails】RailsでAPIの簡単なトークン認証を実装する \- Qiita](https://qiita.com/Yarimizu14/items/c81a8cf1859f954b953e)