🍣

【Rails7】LINEログインを公式ドキュメントに沿って実装する

2022/09/07に公開

Googleカレンダーの予定を家族や自分のLINEに通知するアプリ「トリコミ」を個人開発しています。

リモートワークやオンライン勉強会の予定を共有できるように作ったので、「何それちょっと気になる」と思った方は、よければ使ってみてください!!!

https://torikomi.fly.dev/

さて、この記事では、このアプリの中でも利用している「LINEログイン」の実装方法についてまとめます!

LINEログインとは

https://developers.line.biz/ja/docs/line-login/overview/

LINEアカウントを使ったソーシャルログインサービスで、自分のアプリにLINEアカウントの情報を使ってログインすることができます。

LINE Developersコンソールで開発者登録をすれば無料で使えます。

WebアプリではGoogleログインやTwitterログインほど使われている印象はありませんが、スマホアプリだったり、LINEのユーザー情報とアプリの情報を紐付けたい場合は便利ですね。

今回は、Rails7系でサンプルアプリを作成して、LINEログインを使えるようにしてみます!

環境

以下の環境で作成しています。

  • macOS 11.6.2
  • Ruby 3.1.2
  • Rails 7.0.3
  • PostgreSQL 14.4

下準備

まずrails newコマンドでアプリの雛形を作成します。後半の--css tailwindは、TailwindCSSというCSSをあてるためのコマンドなので、省略しても大丈夫です。見映えをよくしたいので今回は入れました。

またデータベースにPostgreSQLを指定していますが、これも各自の環境にあわせてお好きなものを。

$ rails new line_login_app --css tailwind --database=postgresql

line_login_appディレクトリに移動して、Userモデルを作成します。ユーザーの認証はLINEプラットフォームに任せるので、パスワード関連のカラムは作成しません。あとで説明しますが、LINEプラットフォームでの認証・認可が成功したあと、一意となるLINEのユーザーID(line_user_id)を取得できるので、これをUserの識別に使うことにします。

bundle exec rails g model User line_user_id:string

line_user_idカラムにnot null制約とunique制約をかけてデータベースを作成します。

db/migrate/XXXXXXX_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      # 追加
      t.string :line_user_id, null: false, index: { unique: true }

      t.timestamps
    end
  end
end
$ bundle exec rails db:create
$ bundle exec rails db:migrate

モデル側にも同じ制約をかけます。

app/models/user.rb
class User < ApplicationRecord
    # 追加
  validates :line_user_id, presence: true, uniqueness: true
end

最後にログイン前とログイン後を識別するためのページを適当に作成しておきます。

$ bundle exec rails g controller StaticPages before_login after_login
config/routes.rb
Rails.application.routes.draw do
  # 下2行を追加
  root 'static_pages#before_login'
  get '/after_login', to: 'static_pages#after_login'
end
app/views/static_pages/before_login.html.erb
<div>
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>
  <h1 class="font-bold text-4xl">ログイン前のページ</h1>
</div>
app/views/static_pages/after_login.html.erb
<div>
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>
  <h1 class="font-bold text-4xl">ログイン後のページ</h1>
</div>

LINE Developersコンソールでチャネルを作成する

https://developers.line.biz/ja/docs/line-login/integrate-line-login/#login-flow

WebアプリにLINEログインを組み込む手順は、こちらに詳しく書いてあります。

まずは、「チャネルを作成する」の項目に従って、LINEログインのチャネルを作成します。コールバックURLはあとで設定します。

コントローラを作成する

チャネルを作成したら、LINEログインを行うコントローラを作成します。名前はLineLoginApiとしましょう。

$ bundle exec rails g controller LineLoginApi

先ほどの公式ドキュメントの要件に従って、ユーザーをLINEプラットフォームにリダイレクトさせて認証・認可のプロセスを開始させるloginアクションと、認証・認可のプロセスが終了したあとに、その結果とともにユーザーがリダイレクトされてくるcallbackアクションを実装します。

最終的にはそのユーザーのLINEのユーザーIDが知りたいので、callbackアクションに戻ってきた認可コードからそのユーザーのアクセストークン(IDトークン)を取得するget_line_user_id_tokenメソッドと、そのIDトークンからプロフィール情報(ユーザーID)を取得するget_line_user_idメソッドも実装します。

公式ドキュメントの図を引用して説明させてもらうと、


https://developers.line.biz/ja/docs/line-login/integrate-line-login/#login-flow より

  • Access the LINE Login authorization URL with 'redirect uri' and 'state' のリクエストを送るのがlogin
  • Access 'redirect uri' with 'state' and authorization code のリクエストを受け取るのがcallback
  • Request access token のリクエストを送って Access token のレスポンスを受け取るのがget_line_user_id_token
  • Request user profile information のリクエストを送って User profile information のレスポンスを受け取るのがget_line_user_id

という割り振りです。

それ以外の矢印は、LINEプラットフォームとユーザーが直接やりとりする部分なので、Webアプリ側で何かすることはありません。

具体的なコードは下記となります。

app/controllers/line_login_api_controller.rb
class LineLoginApiController < ApplicationController
  require 'json'
  require 'typhoeus'
  require 'securerandom'

  def login

    # CSRF対策用の固有な英数字の文字列
    # ログインセッションごとにWebアプリでランダムに生成する
    session[:state] = SecureRandom.urlsafe_base64

    # ユーザーに認証と認可を要求する
    # https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request

    base_authorization_url = 'https://access.line.me/oauth2/v2.1/authorize'
    response_type = 'code'
    client_id = 'LINEログインチャネルのチャネルID' #本番環境では環境変数などに保管する
    redirect_uri = CGI.escape(line_login_api_callback_url)
    state = session[:state]
    scope = 'profile%20openid' #ユーザーに付与を依頼する権限

    authorization_url = "#{base_authorization_url}?response_type=#{response_type}&client_id=#{client_id}&redirect_uri=#{redirect_uri}&state=#{state}&scope=#{scope}"

    redirect_to authorization_url, allow_other_host: true
  end

  def callback

    # CSRF対策のトークンが一致する場合のみ、ログイン処理を続ける
    if params[:state] == session[:state]

      line_user_id = get_line_user_id(params[:code])
      user = User.find_or_initialize_by(line_user_id: line_user_id)

      if user.save
        session[:user_id] = user.id
        redirect_to after_login_path, notice: 'ログインしました'
      else
        redirect_to root_path, notice: 'ログインに失敗しました'
      end

    else
      redirect_to root_path, notice: '不正なアクセスです'
    end

  end

  private

  def get_line_user_id(code)

    # ユーザーのIDトークンからプロフィール情報(ユーザーID)を取得する
    # https://developers.line.biz/ja/docs/line-login/verify-id-token/

    line_user_id_token = get_line_user_id_token(code)

    if line_user_id_token.present?

      url = 'https://api.line.me/oauth2/v2.1/verify'
      options = {
        body: {
          id_token: line_user_id_token,
          client_id: 'LINEログインチャネルのチャネルID' # 本番環境では環境変数などに保管
        }
      }

      response = Typhoeus::Request.post(url, options)

      if response.code == 200
        JSON.parse(response.body)['sub']
      else
        nil
      end
    
    else
      nil
    end

  end

  def get_line_user_id_token(code)

    # ユーザーのアクセストークン(IDトークン)を取得する
    # https://developers.line.biz/ja/reference/line-login/#issue-access-token

    url = 'https://api.line.me/oauth2/v2.1/token'
    redirect_uri = line_login_api_callback_url

    options = {
      headers: {
        'Content-Type' => 'application/x-www-form-urlencoded'
      },
      body: {
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: redirect_uri,
        client_id: 'LINEログインチャネルのチャネルID', # 本番環境では環境変数などに保管
        client_secret: 'LINEログインチャネルのチャネルシークレット' # 本番環境では環境変数などに保管
      }
    }
    response = Typhoeus::Request.post(url, options)

    if response.code == 200
      JSON.parse(response.body)['id_token'] # ユーザー情報を含むJSONウェブトークン(JWT)
    else
      nil
    end
  end

end

LINEログインチャネルのチャネルID、LINEログインチャネルのチャネルシークレットと書いてある部分は、ご自分のID、シークレットに変更してください。LINE Developersコンソールから取得できます。

ルーティングも設定します。

config/routes.rb
Rails.application.routes.draw do
  # 下2行を追加
  get 'line_login_api/login', to: 'line_login_api#login'
  get 'line_login_api/callback', to: 'line_login_api#callback'
end

順番が前後しましたが、get_line_user_id_token get_line_user_idメソッド内のHTTP通信でtyphoeusというgemを使いたいので、インストールします。

https://github.com/typhoeus/typhoeus

Gemfile
gem 'typhoeus'
$ bundle install

また、ログイン前のページに、LineLoginApiloginアクションへ遷移させるボタンを実装します。

app/views/static_pages/before_login.html.erb
<div>
  <!-- 略 -->
  <!-- 下記を追加 -->
  <%= link_to "LINEでログイン", line_login_api_login_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium" %>
</div>

ついでに、ログイン後のページにも、動作確認のため、ログイン中のユーザーのIDを表示させるようにします。

app/views/static_pages/after_login.html.erb
<div>
  <!-- 略 -->
  <!-- 下記2行を追加 -->
  <% current_user = User.find(session[:user_id]) %>
  <%= "現在ログイン中のユーザーのID:#{current_user.id}" %>
</div>

これで、Webアプリ側の実装は終了です!

ngrokを使ってローカル環境をhttpsで公開する

最後に、LINE Developersコンソールで、LINEログインチャネルにコールバックURLを登録して終わりとしたいのですが、なんとLINEログインのコールバックURLはhttpsしか受け付けていません!

つまり、コールバックURLにhttp://localhost:3000などのURLが使えないわけです。

そこでちょっと面倒なのですが、ngrokというサービスを使ってlocalhost環境をhttpsで外部公開します。

https://ngrok.com/

まずngrokのサイトでユーザー登録を行い、バイナリファイルをダウンロードします。

サイトの手順通り、zipファイルを解凍し、PCとngrokのアカウントを紐づけます。

$ unzip ~/Downloads/ngrok-v3-stable-darwin-amd64.zip
$ ./ngrok config add-authtoken xxxxxxxxxxxxxxxxxxxxxxxx(あなたのauthtoken)

下記のコマンドで、ngrokを実行します。

$ ./ngrok http 3000

このような画面が出ればOKです。https://(乱数).jp.ngrok.ioの部分はngrokを起動するたびに変わります。

次に、development環境で接続先として許可するホスト名に.jp.ngrok.ioを追加します。(これを追加しないと、Blocked hostエラーが発生します。)

https://weseek.co.jp/tech/680/#1_Railsapplicationconfighosts_Host

config/environments/development.rb
Rails.application.configure do
  # 追加
  config.hosts << '.jp.ngrok.io'
end

これで、http://localhost:3000に、https://(乱数).jp.ngrok.ioでアクセスできるようになりました。

ngrokを実行したまま、./bin/devコマンドを実行し、ngrokの実行画面に表示されているhttps://(乱数).jp.ngrok.ioにアクセスしてみましょう。

無事、localhost環境に外部からhttpsで接続できるようになりました!

最後に、LINE DevelopersコンソールのLINEログインチャネルの設定画面で、コールバックURLをhttps://(乱数).jp.ngrok.io/line_login_api/callbackと設定します。

また、「権限設定」からLINEログインのテストに使いたいLINEアカウントを登録するか、ステータスを「開発中」から「公開」に変更して、どのLINEアカウントでもLINEログインのテストができるようにします。

Webアプリに戻って、LINEログインを試してみましょう。

LINEの認証画面が表示され、ログイン後のページにリダイレクトされたら成功です!

感想

認証系のgemを使わず公式ドキュメント通りにRailsでLINEログインを実装するとこのようになりました。

https://developers.line.biz/ja/docs/line-login/security-checklist/#check-authorization-request

なお、本番環境で運用するにあたっては、こちらのセキュリティチェックリストが非常に参考になりました。

LINEのユーザーID以外にも、プロフィール画像や表示名も取得することができるので、用途にあわせてアレンジしてみてください。

Discussion