セキュアなLIFFアプリをRailsで作成する

# 概要

LIFFアプリでクライアントと自分のサーバ間で安全に認証するにはアクセストークンやIDトークンをサーバに送って認証する必要があります。
今回はLIFFのアクセストークンとRailsのdevise_auth_token Gemを利用することで安全な認証を行います。
今回のコードは[GitHub](https://github.com/Arahabica/liff-rails-sample)に置いています。

ちなみに、RailsではなくFirebaseを利用したバージョンの記事は[こちら](https://qiita.com/Arahabica/items/8f5c7472ded92128535f)になります。

# 使用ライブラリ

### Devise
Railsのデファクトの認証ライブラリ(gem)
認証関連は自作する部分はできるだけ減らして、デファクトを活用

### devise_auth_token
Deviseを利用しつつ、APIによる認証を可能にするライブラリ(gem)

# LIFFアプリのセキュリティについて

LIFFを使うと簡単にユーザ情報を取得できますが、そのユーザ情報をサーバに送ってそのまま信頼してしまうと、なりすましやその他の攻撃に対して脆弱になってしまいます。
![notsecure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30931/b90d0271-1ef9-ef03-53c8-1bf48115fdf3.png)

↑([LINE公式ページ](https://developers.line.biz/ja/docs/line-login/secure-login-process/#using-access-tokens)より引用)

安全のためにアクセストークンを使いましょう。



# 大まかな処理の流れ

1. LIFFでアクセストークンを取得
2. アクセストークンをサーバに送信
3. サーバでLINEの認証処理
4. userIdに対応するトークン作成(devise_auth_token)
5. クライアントからサーバにアクセスする時にトークンをヘッダにつける


これは下記のLINE公式に載っているシーケンス図にのっとった流れになっています。


![secure2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30931/fb2d247e-a2b1-71ac-0e8b-19cd3bd0f890.png)

ただ、「Create new user」の後にdevise_token_authのトークンを生成する処理を追加しています。

# Devise、devise_auth_tokenの導入

まずはdevise_auth_tokenを使える状態に持っていきます。

### Deviseとdevise_token_authのインストール

Gemfileに下記のgemを追加します。

```rb:Gemfile
gem 'jbuilder'
gem 'devise'
gem 'devise_token_auth'
```

gemをインストールします。

```sh
$ bundle install
```

### Deviseとdevise_token_authの初期化

下記のコマンドでDeviseの設定ファイルやUserテーブルのマイグレーションファイルが生成されます。

```sh
$ rails g devise:install
$ rails g devise_token_auth:install User auth
```

マイグレーションを実行してUserテーブルを作成します。

```sh
$ rails db:migrate
```

devise_auth_tokenはデフォルトだと、リクエスト毎にtokenが更新されてしまうので、configを修正します。

```rb:config/initializers/devise_token_auth.rb
# By default the authorization headers will change after each request. The
# client is responsible for keeping track of the changing tokens. Change
# this to false to prevent the Authorization header from changing after
# each request.
config.change_headers_on_each_request = false # <= コメントアウトを外す

# By default, users will need to re-authenticate after 2 weeks. This setting
# determines how long tokens will remain valid after they are issued.
config.token_lifespan = 2.weeks # <= コメントアウトを外す

# コメントアウトを外す
config.headers_names = {:'access-token' => 'access-token',
                        :'client' => 'client',
                        :'expiry' => 'expiry',
                        :'uid' => 'uid',
                        :'token-type' => 'token-type' }
```

これでdevise_token_authによる認証APIが動く状態になりました。
動作確認をしていきましょう。


### 認証APIの確認

まず、名前、メールアドレス、パスワードの項目だけのユーザをDBに登録します。

```sh
$ rails c
> User.create!(name: 'テストユーザー', email: 'example@example.com', password: 'password')
```

テストサーバを起動します。

```sh
$ rails s
```

以下のcurlコマンドで認証APIを叩きます。

```sh
$ curl -D - localhost:3000/auth/sign_in -X POST \
  -d '{"email":"example@example.com", "password":"password"}' \
  -H "content-type:application/json"
```

認証に成功すれば、結果は下記のようになるはずです。(見やすいように少し整形してます。)

```sh
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
access-token: q1wL0eAS3IwKGcs5-8vEyA
token-type: Bearer
client: sd74van0pd3Sxs4O-fowvQ
expiry: 1641540499
uid: example@example.com
ETag: W/"12ac3053b26f91ca234280ac13a0790c"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 707fe01b-d25a-4167-b0f2-95e009c9271a
X-Runtime: 0.403161
Vary: Origin
Transfer-Encoding: chunked

{
  "data": {
    "id": 1,
    "email": "example@example.com",
    "provider": "email",
    "uid": "example@example.com",
    "allow_password_change": false,
    "name": "テストユーザー"
  }
}
```

返ってきたヘッダの`access-token`, `client`, `expiry`, `uid` をリクエストのヘッダにつければ、認証状態でAPIにアクセスすることができます。

### トークン検証APIの確認

devise_auth_tokenはトークンの検証用のAPIもあるので試してみましょう。
`YOUR_ACCESS_TOKEN`, `YOUR_CLIENT`, `YOUR_EXPIRY`, `YOUR_UID` には先ほど取得したものに置き換えてください。


```sh
$ curl localhost:3000/auth/validate_token -D - \
-H "access-token:YOUR_ACCESS_TOKEN" \
-H "client:YOUR_CLIENT" \
-H "expiry:YOUR_EXPIRY" \
-H "uid:YOUR_UID" \
-H "content-type:application/json"
```

うまくいけば、下記のような結果になるはずです。

```sh
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
access-token: q1wL0eAS3IwKGcs5-8vEyA
token-type: Bearer
client: sd74van0pd3Sxs4O-fowvQ
expiry: 1641540499
uid: test-user+1@gmail.com
ETag: W/"f3e45c8f2942619bd67981aead0bc740"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0b9e57df-1f3b-4597-9c0f-01a6b3f904be
X-Runtime: 0.086486
Vary: Origin
Transfer-Encoding: chunked

{
  "success": true,
  "data": {
    "id": 1,
    "provider": "email",
    "uid": "example@example.com",
    "allow_password_change":false,
    "name":"テストユーザー"
  }
}
```

### 独自APIで認証機能が使えることを確認

独自に作成したAPIでもdevise_token_authの認証機能が使えることを確認しましょう。

まずは、route.rbに下記を追加してください。

```rb:route.rb
    get '/hello', to: 'hello#index'
```

下記コマンドでHelloコントローラを作成しましょう。

```
rails g controller hello
```

コントローラを下記のように入力してください。

```rb:app/controller/hello_controller.rb
class HelloController < ApplicationController
  before_action :authenticate_user!
  def index
    message = "こんにちは、#{current_user.name}さん"
    render json: { message: message }, status: :ok
  end
end
```

`before_action :authenticate_user!`を冒頭に書くことでこのコントローラへのアクセスは認証を必須にしています。

まずはアクセストークンなしでリクエストしてみます。

```sh
$ curl localhost:3000/hello -D - \
-H "content-type:application/json"
```

結果は下記のように401エラーが返ってきます。

```sh
HTTP/1.1 401 Unauthorized
...

{"errors":["You need to sign in or sign up before continuing."]}
```

次にアクセストークンありでリクエストしてみます。

```sh
$ curl localhost:3000/hello \
-H "access-token:YOUR_ACCESS_TOKEN" \
-H "client:YOUR_CLIENT" \
-H "expiry:YOUR_EXPIRY" \
-H "uid:YOUR_UID" \
-H "content-type:application/json"
```

下記のようなメッセージが表示されれば成功です。

```sh
HTTP/1.1 200 OK
...

{"message":"こんにちは、テストユーザーさん"}
```

# devise_token_authをLINEのアクセストークンに対応させる

今のままでは普通のEmail認証になってしまっているので、これをLINEに対応させていきます。


devise_token_authのコントローラをオーバーライドする形で対応していきましょう。
devise_token_authのオーバーライドする方法の詳細は[こちら](https://github.com/lynndylanhurley/devise_token_auth/blob/master/docs/usage/overrides.md)を参照してください。

まずは、下記のように`route.rb` を編集します。

```rb:config/route.rb
    mount_devise_token_auth_for 'User', at: 'auth', controllers: {
      registrations: 'line_token_auth/registrations',
      sessions: 'line_token_auth/sessions'
    }
```

`registrations`と`sessions`ではオーバーライドしたコントローラで処理するように修正しています。

### Deviseの設定の修正

`config/initializers/devise.rb`を編集してDeviseの設定を修正します。
デフォルトでは認証キーがemailに設定されているので、これをuidに修正します。

```diff
-   #config.authentication_keys = [:email]
+   config.authentication_keys = [:uid]
```

### ApplicationControllerの修正

`application_controller.rb`を下記のように書き換えましょう。
Devise関連の処理でパラメータとしてアクセストークンを受け入れるように修正しています。

```rb:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  before_action :authenticate_user!, unless: :devise_controller?
  before_action :configure_permitted_parameters, if: :devise_controller?
  skip_before_action :verify_authenticity_token, if: :devise_controller? # skip CSRF check if API

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:uid, :access_token])
    devise_parameter_sanitizer.permit(:sign_in, keys: [:uid, :access_token])
  end
end
```

### LineTokenAuth::RegistrationsControllerの作成

`LineTokenAuth::RegistrationsController`(認証用コントローラ)を作成していきます。

まずは下記コマンドでコントローラを作成してください。

```sh
$ rails g controller LineTokenAuth::registrations
```

次に、内容を下記のように入力してください。

```rb:app/controllers/line_token_auth/registrations_controller.rb
module LineTokenAuth
  # DeviseTokenAuthのコントローラを継承している
  class RegistrationsController < DeviseTokenAuth::RegistrationsController
    include LineTokenAuth::Concerns::LineAuthenticator

    def create
      build_resource

      unless @resource.present?
        raise DeviseTokenAuth::Errors::NoResourceDefinedError,
              "#{self.class.name} #build_resource does not define @resource,"\
              ' execution stopped.'
      end

      # if whitelist is set, validate redirect_url against whitelist
      return render_create_error_redirect_url_not_allowed if blacklisted_redirect_url?(@redirect_url)

      auth_result = authenticate(@resource[:uid], sign_up_params[:access_token])
      if auth_result[:error]
        return render_error(auth_result[:error][:code], auth_result[:error][:message])
      end
      @resource.name = auth_result[:profile][:name]
      @resource.image = auth_result[:profile][:image]
      if @resource.save
        yield @resource if block_given?

        if active_for_authentication?
          # email auth has been bypassed, authenticate user
          @token = @resource.create_token
          @resource.save!
          update_auth_header
        end

          render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end
    end
    protected

    def build_resource
      @resource            = resource_class.new(uid: sign_up_params[:uid])
      @resource.provider   = provider
    end

    private

    def provider
      'line'
    end
  end
end
```

DeviseTokenAuthのコントローラを継承して、`create`, `build`, `provider` メソッドをオーバーライドしています。

それぞれのメソッドを簡単にみていきます。

#### LineTokenAuth::RegistrationsController#create

`create` メソッドが長いですが、DeviseTokenAuthのコントローラとの差分は下記となっており、追加したのは5行だけになります。

```diff:create
    def create
      build_resource

      unless @resource.present?
        raise DeviseTokenAuth::Errors::NoResourceDefinedError,
              "#{self.class.name} #build_resource does not define @resource,"\
              ' execution stopped.'
      end
-
-     # give redirect value from params priority
-     @redirect_url = params.fetch(
-       :confirm_success_url,
-       DeviseTokenAuth.default_confirm_success_url
-     )
-
-     # success redirect url is required
-     if confirmable_enabled? && !@redirect_url
-       return render_create_error_missing_confirm_success_url
-     end

       # if whitelist is set, validate redirect_url against whitelist
       return render_create_error_redirect_url_not_allowed if blacklisted_redirect_url?(@redirect_url)


+      auth_result = authenticate(@resource[:uid], sign_up_params[:access_token])
+      if auth_result[:error]
+        return render_error(auth_result[:error][:code], auth_result[:error][:message])
-      # override email confirmation, must be sent manually from ctrl
-      callback_name = defined?(ActiveRecord) && resource_class < ActiveRecord::Base ? :commit : :create
-      resource_class.set_callback(callback_name, :after, :send_on_create_confirmation_instructions)
-      resource_class.skip_callback(callback_name, :after, :send_on_create_confirmation_instructions)
-
-      if @resource.respond_to? :skip_confirmation_notification!
-        # Fix duplicate e-mails by disabling Devise confirmation e-mail
-        @resource.skip_confirmation_notification!
       end
+      @resource.name = auth_result[:profile][:name]
+      @resource.image = auth_result[:profile][:image]
-
-       if @resource.save
-         yield @resource if block_given?
- 
-         unless @resource.confirmed?
-         # user will require email authentication
-         @resource.send_confirmation_instructions({
-           client_config: params[:config_name],
-           redirect_url: @redirect_url
-         })
-       end

        if active_for_authentication?
          # ここでトークンを生成している
          @token = @resource.create_token
          update_auth_header
        end

        render_create_success
      else
        clean_up_passwords @resource
        render_create_error
      end
    end
```


`authenticate` メソッドでIDとトークンを渡し、認証の結果が返ってきています。
`authenticate` メソッドは冒頭の`LineTokenAuth::Concerns::LineAuthenticator`の中で定義されています。

また、`@resource.create_token`の部分でトークンを生成していることが確認できます。


#### LineTokenAuth::RegistrationsController#build_resource

`build_resource`はUserモデルを生成しています。
もともとはemailベースなので関連するものを削除し、`uid`のみを使用するように修正しています。

```diff
    def build_resource
+      @resource            = resource_class.new(uid: sign_up_params[:uid])
-      @resource            = resource_class.new(sign_up_params)
      @resource.provider   = provider
-
-      # honor devise configuration for case_insensitive_keys
-      if resource_class.case_insensitive_keys.include?(:email)
-        @resource.email = sign_up_params[:email].try(:downcase)
-      else
-        @resource.email = sign_up_params[:email]
-      end
    end
```

#### LineTokenAuth::RegistrationsController#provider
デフォルトはemailなのでproviderを`line`に修正しています。

```
def provider
  'line'
end
```

### LineTokenAuth::Concerns::LineAuthenticatorの作成

先程の`create`メソッドでも使用した`LineTokenAuth::Concerns::LineAuthenticator`を作成していきます。
このConcernに具体的なLINEの認証処理を書いています。

まずは下記コマンドでConcernファイルを作成してください。

```
$ mkdir app/controllers/line_token_auth/concerns
$ touch app/controllers/line_token_auth/concerns/line_authenticator.rb
```

内容は下記のようになります。

```rb:app/controllers/line_token_auth/concerns/line_authenticator.rb
require 'net/http'
require 'uri'

module LineTokenAuth::Concerns::LineAuthenticator
  extend ActiveSupport::Concern

  protected

  def authenticate(uid, access_token)
    verify_result = verify_line_token(access_token)
    if verify_result[:code] != 200
      return fail_authenticate(verify_result[:code], verify_result[:body]["error_description"])
    end
    if verify_result[:body]["client_id"] != line_channel_id
      return fail_authenticate(401, 'LINE Channel ID is not matched.')
    end
    if verify_result[:body]["expires_in"] <= 0
      return fail_authenticate(401, 'LINE access token is expired')
    end
    profile_result = get_profile_by_line_token(access_token)
    if profile_result[:code] != 200
      return fail_authenticate(profile_result[:code], profile_result[:body][:error_description])
    end
    if profile_result[:body]["userId"] != uid
      return fail_authenticate(401, 'uid is not matched.')
    end
    success_authenticate({
      uid: uid,
      name: profile_result[:body]["displayName"],
      image: profile_result[:body]["pictureUrl"]
    })
  end
  private
  def line_channel_id
    @line_channel_id ||=  ENV["LINE_CHANNEL_ID"]
  end
  def verify_line_token(access_token)
    uri = URI.parse("https://api.line.me/oauth2/v2.1/verify")
    uri.query = URI.encode_www_form(access_token: access_token)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req = Net::HTTP::Get.new uri.request_uri
    res = http.request req
    {
      code: res.code.to_i,
      body: JSON.parse(res.body)
    }
  end
  def get_profile_by_line_token(access_token)
    uri = URI.parse("https://api.line.me/v2/profile")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req = Net::HTTP::Get.new uri.request_uri
    req[:Authorization] = "Bearer #{access_token}"
    res = http.request req
    {
      code: res.code.to_i,
      body: JSON.parse(res.body)
    }
  end
  def fail_authenticate(code, message)
    { error: { code: code, message: message }, profile: nil }
  end
  def success_authenticate(profile)
    { error: nil, profile: profile }
  end
end
```

`authenticate`メソッドでLINEログインAPIのアクセストークンの有効性を検証するAPIとユーザープロフィールを取得するAPIを使っていることが確認できると思います。

また、環境変数`LINE_CHANNEL_ID`を使用しているので自分のLINEチャネルのチャネルIDを設定するする必要があります。



### LineTokenAuth::SessionsControllerの作成

`LineTokenAuth::SessionsController`(セッションコントローラ)を作成していきます。

まずは下記コマンドでコントローラを作成してください。

```sh
$ rails g controller LineTokenAuth::sessions
```

次に、内容を下記のように入力してください。

```rb:app/controllers/line_token_auth/sessions_controller.rb
module LineTokenAuth
  class SessionsController < DeviseTokenAuth::SessionsController
    include LineTokenAuth::Concerns::LineAuthenticator

    def create
      # Check
      field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
      @resource = nil
      if field
        q_value = get_case_insensitive_field_from_resource_params(field)
        @resource = find_resource(field, q_value)
      end
      
      if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        auth_result = authenticate(@resource[field], resource_params[:access_token])
        if auth_result[:error]
          return render_error(auth_result[:error][:code], auth_result[:error][:message])
        end
        
        @token = @resource.create_token
        @resource.save
      
        sign_in(:user, @resource, store: false, bypass: false)
      
        yield @resource if block_given?
      
        render_create_success
      elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        if @resource.respond_to?(:locked_at) && @resource.locked_at
          render_create_error_account_locked
        else
          render_create_error_not_confirmed
        end
      else
        render_create_error_bad_credentials
      end
    end
    def valid_params?(key, val)
      resource_params[:access_token] && key && val
    end
    def provider
      'line'
    end
  end
end
```

#### LineTokenAuth::SessionsController#create

`create`メソッドの差分は下記です。

```diff
    def create
      # Check
      field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
      @resource = nil
      if field
        q_value = get_case_insensitive_field_from_resource_params(field)
        @resource = find_resource(field, q_value)
      end

      if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
+       auth_result = authenticate(@resource[field], + resource_params[:access_token])
+       if auth_result[:error]
+         return render_error(auth_result[:error][:code], auth_result[:error][:message])
-       valid_password = @resource.valid_password?(resource_params[:password])
-       if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
-         return render_create_error_bad_credentials
        end
-
-       create_and_assign_token
+       @token = @resource.create_token
+       @resource.save

        sign_in(:user, @resource, store: false, bypass: false)

        yield @resource if block_given?

        render_create_success
      elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        if @resource.respond_to?(:locked_at) && @resource.locked_at
          render_create_error_account_locked
        else
          render_create_error_not_confirmed
        end
      else
        render_create_error_bad_credentials
      end
    end

```

RegistrationsControllerと同様、Email認証の部分をconcernsで定義したLINE認証の処理に差し替えていることがわかると思います。

### LineTokenAuth::SessionsController#valid_params?

差分は下記です。

```diff:
    def valid_params?(key, val)
+      resource_params[:access_token] && key && val
-      resource_params[:password] && key && val
    end

```

パラメータチェックでアクセストークンを必須にしています。

### LineTokenAuth::SessionsController#provider
RegistrationsControllerと同様です。
デフォルトはemailなのでproviderを`line`に修正しています。

```
def provider
  'line'
end
```

####　サーバサイドは完了🎉

以上でdevise_auth_tokenのオーバーライド処理は完了です🎉

この状態でLINEのアクセストークンで認証し、認証状態を保持することができるようになっているはずです。



#　フロント側(LIFFアプリ)の実装

次に肝心のLIFFアプリを作っていきます。
LIFFアプリの管理画面での細かい設定は省略します。

まずはHTMLです。本当に必要最低限のものにしてあります。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hello</title>
</head>
<body>
	<h1>Loading...</h1>
	<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
	<script src="./app.js"></script>
</body>
</html>
```
CDNからLIFFのSDKを呼んで、自分のapp.jsを呼んでるだけですね。


次にjavascriptです。こちらも説明のため最低限の素のjsになっています。

```js:app.js
const store = {
    userId: "",
    client: "",
    accessToken: "",
    expiry: 0
};
const LIFF_ID = "YOUR_LIFF_ID";

window.onload = async function() {
    await liff.init({ liffId: LIFF_ID });
    // LINEアプリ外で開いた時はログイン処理をする
    if (!liff.isLoggedIn()) {
        liff.login();
    }
    const { userId } = liff.getContext();
    store.userId = userId;
    console.log(LIFF_ID, liff.getContext());
    const accessToken = await liff.getAccessToken();
    await signIn(store.userId, accessToken);
    if (!store.accessToken) {
        console.log(accessToken);
        await signUp(store.userId, accessToken);
    }
    const message = await getHelloMessage();
    console.log(message);
    document.getElementsByTagName('h1')[0].innerText = message;
};
async function signIn(userId, accessToken) {
    const res = await fetch('/auth/sign_in', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'  
        },
        body: JSON.stringify({
            uid: userId,
            access_token: accessToken
        })
    });
    if (res.status === 200) {
        store.client = res.headers.get('client');
        store.accessToken = res.headers.get('access-token');
        return;
    }
    if (res.status !== 401 && res.status !== 404) {
        const data = await res.json();
        console.error(data);
        throw new Error(`Status Code: ${res.status}.`)
    }
}
async function signUp(userId, accessToken) {
    const res = await fetch('/auth', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            uid: userId,
            access_token: accessToken
        })
    });
    if (res.status !== 200) {
        const data = await res.json();
        console.error(data);
        throw new Error(`Status Code: ${res.status}.`);
    }
    // 認証に成功したときにアクセストークンなどを保存
    store.client = res.headers.get('client');
    store.accessToken = res.headers.get('access-token');
    store.expiry = res.headers.get('expiry');
}
async function getHelloMessage() {
    // 保存したアクセストークンを使用し認証必須のAPIを呼んでいる
    const res = await fetch('/hello', {
        headers: {
            'Content-Type': 'application/json',
            ...getAuthHeaders()
        }
    });
    if (res.status !== 200) {
        const data = await res.json();
        console.error(data);
        throw new Error(`Status Code: ${res.status}.`);
    }
    const data = await res.json();
    return data.message;
}
// 保存しているアクセストークンなどからリクエストヘッダを生成
function getAuthHeaders() {
    return {
        uid: store.userId,
        client: store.client,
        expiry: store.expiry,
        'access-token': store.accessToken
    }
}
```

`/auth/sign_in` APIにユーザIDとLINEのアクセストークンを渡し、取得したdevise_auth_tokenのアクセストークンを保存していることが確認できると思います。
(2種類のアクセストークンが出てきてややこしいですね。。。)

その後、認証必須の`/hello` APIをアクセストークン(devise_auth_token)を使って呼び出し、取得したメッセージを画面に表示しています。

「こんにちは、"LINE名"さん」というメッセージが表示されたら成功です。

# おわりに
ちょっとややこしいですが、一度この基盤を整えてしまえば、あとは普通のRailsのWeb開発になります。
ベースはDeviseになっているので、後からLIFFの認証以外を使いたくなっても柔軟に対応することができるはずです。

# 参考記事
https://qiita.com/kk-icare/items/c4cd040824c7d6b434d6