【Rails7】LINEログインを公式ドキュメントに沿って実装する
Googleカレンダーの予定を家族や自分のLINEに通知するアプリ「トリコミ」を個人開発しています。
リモートワークやオンライン勉強会の予定を共有できるように作ったので、「何それちょっと気になる」と思った方は、よければ使ってみてください!!!
さて、この記事では、このアプリの中でも利用している「LINEログイン」の実装方法についてまとめます!
LINEログインとは
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制約をかけてデータベースを作成します。
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
モデル側にも同じ制約をかけます。
class User < ApplicationRecord
    # 追加
  validates :line_user_id, presence: true, uniqueness: true
end
最後にログイン前とログイン後を識別するためのページを適当に作成しておきます。
$ bundle exec rails g controller StaticPages before_login after_login
Rails.application.routes.draw do
  # 下2行を追加
  root 'static_pages#before_login'
  get '/after_login', to: 'static_pages#after_login'
end
<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>
<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コンソールでチャネルを作成する
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アプリ側で何かすることはありません。
具体的なコードは下記となります。
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コンソールから取得できます。
ルーティングも設定します。
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を使いたいので、インストールします。
gem 'typhoeus'
$ bundle install
また、ログイン前のページに、LineLoginApiのloginアクションへ遷移させるボタンを実装します。
<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を表示させるようにします。
<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で外部公開します。
まず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エラーが発生します。)
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ログインを実装するとこのようになりました。
なお、本番環境で運用するにあたっては、こちらのセキュリティチェックリストが非常に参考になりました。
LINEのユーザーID以外にも、プロフィール画像や表示名も取得することができるので、用途にあわせてアレンジしてみてください。



Discussion