RailsアプリにLINEログインを実装する方法を、コードを1つずつ解説しながら紹介します。
この記事はプログラミング学習初心者が書いた記事になります。
はじめに
対象
- Rails アプリにLINE外部ログインを入れたい方
- Devise でメール/パスワード認証は作ったことがある方
環境
- Ruby 3.3 / Rails 8.1
- Devise(認証)
- 環境変数管理に
dotenv-railsを使用 - Renderにてデプロイ済み
前提条件
- Deviseでのメール/パスワード認証は導入済み
前提知識
そもそもLINEログイン(OAuth)とは?
パスワードを自分のアプリで預からず、「本人確認」だけをLINEに代行してもらう仕組み です。
ユーザーは LINE に対してだけパスワードを入力し、アプリは LINE から「本人である」という証明だけを受け取ります。アプリはパスワードを一切保持しないため、安全性が高く、ユーザーも簡単にログインできることから、多くのアプリで採用されています。
OAuthとは外部サービスのパスワードを預からず、安全に認可を行うための標準プロトコルのことで、現在はアクセストークンを使うOAuth 2.0が主流となっています。LINEログインは OpenID Connect にも対応しており、openid スコープを使うとIDトークンによる認証も可能です(本記事では使いません)。
全体の流れ
① ユーザーが「LINEでログイン」をクリック
↓
② アプリサーバーがstateと認可URLを生成し、LINE の認可画面へリダイレクト
↓
③ LINE上でユーザー認証
↓
④ LINE上でこのアプリサーバーへ情報提供してよいかユーザーに確認
↓
⑤ LINEがstate、認可コードをつけてコールバックURLへユーザーを送り返す
↓
⑥ アプリサーバーがstateを検証し、認可コードをトークンURLへ送ってアクセストークンを受け取る
↓
⑦ アプリサーバーがアクセストークンでユーザーのプロフィール(名前・アイコン)を取得
↓
⑧ その情報でアプリにユーザーを作成 or ログイン
この記事で実際にコードを書くのは主に①⑦⑧の部分です。
③④⑤はLINE上で行われます。
②⑥の通信は、後述する土台ライブラリ omniauth-oauth2 が担当してくれます。
stateとは、CSRF攻撃を防ぐために必要なランダムな文字列で「この認可レスポンスは、このブラウザが自分で開始したフローの続きかどうか」を判別するためのものです。
下準備
LINE Developers でチャネルを作る
コードを書く前に、LINE側で「アプリの登録」が必要です。
- LINE Developers にログイン
- プロバイダーを作成 →「LINEログイン」チャネルを作成
- Channel ID と Channel Secret を記録しておく
- 「LINEログイン設定」のコールバックURLに以下を登録
開発環境:https://gapy-unvenially-mabelle.ngrok-free.dev/users/auth/line/callback
本番環境:https://あなたのドメイン/users/auth/line/callback
今回の記事では開発環境にngrokを使用しており、ローカルで起動しているプロセスへインターネットを介して外部からアクセスできるようにしています。LINEログインはhttpでのエンドポイントを設定するとエラーになってしまいます。
コールバックURLとは
LINEでの認証が終わったあとの戻り先のURLのことです。
環境変数設定
.envは .gitignore に入れてGit管理外にします。
LINE_LOGIN_KEY='あなたのChannel ID'
LINE_LOGIN_SECRET='あなたのChannel Secret'
本番環境用の環境変数はデプロイ先のRenderに登録しています。
gem導入
Gemfile に以下を追加します。
# 認証の土台 - すでに導入済み前提
gem "devise"
# OAuth 2.0の共通処理を実装したOmniAuth用ライブラリ
gem "omniauth-oauth2"
# OmniAuth の CSRF(なりすまし)対策
gem "omniauth-rails_csrf_protection"
group :development, :test do
gem "dotenv-rails"
end
bundle install
LINEログイン機能を含んだgemであるomniauth-line はメンテナンスされていないため使用せず、土台の omniauth-oauth2 の上に独自ストラテジを実装します。
実装
LINE 以外の OmniAuth プロバイダーも今後追加する前提で実装しています。
userテーブルにカラム追加
rails generate migration AddColumnToUsers
users テーブルに、プロバイダー名と LINE の固有 ID を保存するカラムを追加し、その組み合わせにユニーク制約をかけます。
class AddColumnToUsers < ActiveRecord::Migration[8.1]
def change
add_column :users, :provider, :string
add_column :users, :uid, :string
add_index :users, [ :provider, :uid ], unique: true
end
end
rails db:migrate
ルーティング
devise_for :users が/users/auth/lineと/users/auth/line/callbackの経路を自動生成してくれます。
devise_for :users, controllers: {
omniauth_callbacks: "users/omniauth_callbacks"
}
Userモデル
外部ログインを有効化します。
devise :omniauthable, omniauth_providers: %i[line]
今後プロバイダーが増える可能性があったため、配列で指定しています。
イニシャライザにプロバイダ登録
require Rails.root.join("lib/omniauth/strategies/line")
Devise.setup do |config|
...
config.omniauth :line, ENV["LINE_LOGIN_KEY"], ENV["LINE_LOGIN_SECRET"], scope: "profile"
冒頭のrequireで、後述する自前ストラテジを明示的に読み込んでいます。
LINEログインボタン
<%- if devise_mapping.omniauthable? %>
<%= button_to omniauth_authorize_path(resource_name, :line),
method: :post,
data: { turbo: false },
... do %>
<%= image_tag "line_login_btn.png",
alt: "LINEでログイン",
... %>
<% end %>
<%- end %>
devise_mapping.omniauthable? とは
Devise が用意したメソッドで、このモデルは外部ログイン(OmniAuth)対応かを true / false で返します。
deviseのソースコード app/controllers/devise_controller.rb を見に行くと73-75行目に次のように定義されています。
def devise_mapping
@devise_mapping ||= request.env["devise.mapping"]
end
devise_mappingは現在のリクエストに対応するDeviseの設定情報を取り出すメソッドです。
中身は Devise::Mapping のインスタンスで、これはモデルごとの Devise 設定を保持するクラスです(Devise はモデルごとに異なる設定が可能。本アプリは devise_for :users の users のみ)。
つまり、devise_mapping.omniauthable?でこのインスタンスに対しOmniAuth機能が有効かどうかを問い合わせています。
app/models/user.rbにdevise :omniauthable, ...と書いてあれば true → ボタンが表示される。外部ログインが有効な時だけ表示される仕組みになっています。
button_toを使う理由
OAuthの認証フローはCSRF対策のためPOSTリクエストが推奨されています。button_to はデフォルトでPOSTを使用しますが、明示的に書くことでコードの意図を明確にしています。
omniauth_authorize_path(resource_name, :line) は第一引数にリソース名(:user)、第二引数にプロバイダー名を指定しパスを作るヘルパーメソッドです。内部で処理されuser_line_omniauth_authorize_pathというパスを作ります。
rails routesで確認すると
user_line_omniauth_authorize POST|GET /users/auth/line(.:format)
/users/auth/line というURLが生成されることがわかります。
独自ストラテジ
require "omniauth-oauth2"
require "json"
module OmniAuth
module Strategies
# OAuth2 の共通処理(トークン交換・state 検証等)は omniauth-oauth2 に委譲し、LINE 固有の差分のみここで定義
class Line < OmniAuth::Strategies::OAuth2
option :name, "line"
option :scope, "profile"
option :client_options, {
site: "https://api.line.me", # ベースURL
authorize_url: "https://access.line.me/oauth2/v2.1/authorize", # 認可URL
token_url: "/oauth2/v2.1/token" # トークン取得のURL
}
def callback_url
full_host + script_name + callback_path
end
uid { raw_info["userId"] }
info do
{
name: raw_info["displayName"]
}
end
# PROFILE 権限付きのアクセストークンで LINE プロフィールを取得する。
def raw_info
@raw_info ||= JSON.parse(access_token.get("v2/profile").body)
end
end
end
end
上から順に見ていきます。
require
require "omniauth-oauth2"
require "json"
omniauth-oauth2という土台ライブラリを読み込んでいます。
LINEからの返答はJSON形式で受け取るためjsonも読み込む必要があります。
オプション設定
option :name, "line"
option :scope, "profile"
このストラテジの名前と、LINEへプロフィール情報へのアクセスを要求するスコープのオプション設定を書いています。
client_options(プロバイダ固有のURL)
option :client_options, {
site: "https://api.line.me", # ベースURL
authorize_url: "https://access.line.me/oauth2/v2.1/authorize", # 認可画面URL
token_url: "/oauth2/v2.1/token" # トークン取得のURL
}
-
authorize_url認可URL
公式ドキュメント「ユーザーに認証と認可を要求する」を参照。
ユーザーがLINEログインボタンをクリックしたときに、必須のクエリパラメータを付けてリダイレクトする先。必須のパラメータをつける作業はomniauth-oauth2が行ってくれます。 -
token_urlトークンURL。アクセストークンを発行するURL
公式ドキュメント「アクセストークンを発行する」を参照。相対パスで書いているため、site と結合されてhttps://api.line.me/oauth2/v2.1/tokenになります。
callback_urlを オーバーライド
def callback_url
full_host + script_name + callback_path
end
オーバーライドする理由
土台となるomniauthのコード504行目を見に行くと
def callback_url
full_host + callback_path + query_string
end
となっており、redirect_uriとして送る時にもquery_string付きになってしまいます。認可リクエスト時は query_string が空なので、redirect_uri は
https://example.com/users/auth/line/callback
になります。一方、コールバック後にトークン交換を行うタイミングでは、
/users/auth/line/callback?code=xxx&state=yyy
にアクセスしているため、query_string が付与されます。その結果、
redirect_uri=https://example.com/users/auth/line/callback?code=...&state=...
が送られてしまいます。
LINEは redirect_uri が「認可リクエスト時」と「トークン交換時」で完全一致しないと弾かれるため、オーバーライドしてquery_stringを除く必要があります。
| 項目 | 意味 | 例 |
|---|---|---|
full_host |
プロトコル+ホスト名(必要ならポート番号も含む) | https://example.com |
script_name |
アプリのマウント接頭辞(通常は空文字列) |
""、"/app"
|
callback_path |
OmniAuthのコールバック先のパス | /users/auth/line/callback |
LINEのAPIレスポンスを共通の形式に変換
def raw_info
@raw_info ||= JSON.parse(access_token.get("v2/profile").body)
end
コールバックで戻ってきた時点で access_token は取得済みです。そこから .get("v2/profile") でプロフィール情報を取りに行き、レスポンスボディの JSON 文字列を Ruby のハッシュに変換しています。@raw_info ||= はメモ化で、2回目以降の呼び出しでは API を叩き直しません。
uid { raw_info["userId"] }
info do
{
name: raw_info["displayName"]
}
end
そのハッシュに格納された値のなかで不変であるuserIdをuidとしてユーザー識別子にしています。
infoはomniauth共通のスキーマ(今回はnameのみ)に正規化します。
独自ストラテジをZeitwerkの管理から外す
module Myapp
class Application < Rails::Application
config.autoload_lib(ignore: %w[assets tasks omniauth])
end
end
Rails 7.1 以降は config.autoload_lib により lib 配下が自動読み込みの対象になっています。
Zeitwerkとは「ファイル名からクラス名を判断してrequireしてくれるローダー」でRails6から標準で入っています。omniauthをZeitwerkの管理から外します。
Zeitwerkの管理から外す理由
railsはlib/omniauth/strategies/line.rb に対して Omniauth::Strategies::Line という定数を期待しますが、実際の定数は OmniAuth::Strategies::Line(Aが大文字)となり、Zeitwerk::NameError が発生します。
❯ dc exec web rails zeitwerk:check
Hold on, I am eager loading the application.
bin/rails aborted!
NameError: uninitialized constant Omniauth::Strategies::Line (NameError)
@mod.const_get(@cname, false)
^^^^^^^^^^
Tasks: TOP => zeitwerk:check
(See full trace by running task with --trace)
対策としてomniauthをZeitwerkの管理から外します。Rails8ではconfig/application.rbに config.autoload_lib(ignore: %w[assets tasks])が最初から書かれているので、omniauth を追記するだけです。
devise.rb で明示的にrequireしているので管理から外しても問題なく読み込まれます。
コールバックコントローラー
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def line
callback_for(:line) # LINE から戻ってきたときの入口
end
private
def callback_for(provider)
# 認証結果からユーザーを取得/作成(app/models/user.rb に定義)
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
sign_in user, event: :authentication
flash[:notice] = "#{provider_name(provider)}でログインしました"
redirect_to stored_location_for(user) || authenticated_root_path
else
session["devise.#{provider}_data"] = request.env["omniauth.auth"].except(:extra)
redirect_to new_user_session_path, alert: "ユーザー情報の取得に失敗しました。"
end
end
def provider_name(provider)
{
line: "LINE",
google_oauth2: "Google"
}.fetch(provider.to_sym, provider.to_s.titleize)
end
end
callback_for と provider_name はアクションではないので private 配下に置いています。
LINE専用の処理は書かず、共通処理として callback_for を呼び出しています。
if user.persisted?
sign_in user, event: :authentication # ログイン
flash[:notice] = "#{provider_name(provider)}でログインしました"
redirect_to stored_location_for(user) || authenticated_root_path # 元居た場所 or ダッシュボード
else
session["devise.#{provider}_data"] = request.env["omniauth.auth"].except(:extra)
redirect_to new_user_session_path, alert: "ユーザー情報の取得に失敗しました。"
end
ユーザーの保存可否を分岐しています。保存できていればログインして元のページ(なければダッシュボード)へ、失敗していれば認証情報をセッションに退避してログイン画面へ戻します。
ユーザー生成
def self.from_omniauth(auth)
user = find_or_initialize_by(provider: auth.provider, uid: auth.uid)
return user unless user.new_record?
user.email = auth.info.email.presence || "#{auth.uid}-#{auth.provider}@example.com"
user.name = auth.info.name.presence || default_name_for(auth.provider)
user.password = Devise.friendly_token[0, 20]
user.save
user
end
def self.default_name_for(provider)
{
"line" => "LINEユーザー"
}.fetch(provider, "ユーザー")
end
provider + uid の組み合わせでユーザーを識別し、2回目以降のログインでは既存ユーザーをそのまま返します。find_or_initialize_byは条件を指定して初めの1件を取得し1件もなければ新しいインスタンスを作るメソッドです。
初回作成時の各カラムは以下の通りにしています。
- メールアドレス:今回の設計ではLINEからメールアドレスを取得できないため、uidとプロバイダー名から仮アドレスを作成しています。
- 名前:取得失敗した場合に備えてダミー名にフォールバックしています。
- パスワード:外部ログインユーザーは使わないため、ランダム値を入れます。
実際のメールアドレスを取得したい場合は、scope に openid と email を追加したうえで、LINE Developers コンソールでメールアドレス取得権限の申請が必要です。取得は ID トークン経由になるため、本記事の構成とは別の実装が必要になります。
まとめ - 全体の流れ
ユーザーが「LINEでログイン」ボタンを押す
button_to から /users/auth/line に POST リクエストを送る。
↓
devise_for がリクエストを受け取り、OmniAuth::Strategies::Line を起動する
↓
omniauth-oauth2 が認可URL・state・redirect_uri などを生成し、LINEの認可画面 (https://access.line.me/...) へリダイレクトする
↓
ユーザーがLINEでログインし、アプリへの情報提供に同意する
↓
LINEが code と state を付与してコールバックURLへリダイレクトする
↓
omniauth-oauth2 が state を検証し、認可コード (code) を使って https://api.line.me/oauth2/v2.1/token にアクセストークンを要求する
このとき、本記事でオーバーライドした callback_url が redirect_uri として使用される。
↓
アクセストークンを利用して v2/profile API を呼び出し、userId や displayName を取得する
↓
OmniauthCallbacksController が User.from_omniauth を呼び出し、ユーザーを新規作成または既存ユーザーとしてログインさせる
開発環境で試してみてください。ログインできていれば成功です。
何か間違いがありましたらお気軽にコメント頂けますと幸いです。
参考文献