【Rails】なぜSorceryは、LINEログインからメールアドレスを取得できないか

# はじめに
### 本記事で分かること
- Sorceryを使っても、LINEログインからユーザーのメールアドレスを取得できない理由
- LINEログインのフロー
- LINEログインからメールアドレスを取得するために必要なこと

### 結論
LINEログインでユーザーのメールアドレスを取得するには、**IDトークンの検証が必要**。この**検証処理がSorceryに含まれていない**ので、LINEログインからメールアドレスを取得することができない。

:::note warn
現在はSorceryはメンテナンスが行われていないため、Sorceryの使用には注意してください
:::

### 対象
- Sorceryを使ってLINEログインを実装しようとされている方

### 環境
- Sorcery v0.17.0
- LINEログイン v2.1
- Ruby on Rails v7.1

### 目次
1. [LINEログインのフロー](https://qiita.com/drafts/e9909336028cd29e1895/edit#1-line%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E3%83%95%E3%83%AD%E3%83%BC)
    1-1. [LINEログインの全体像](https://qiita.com/drafts/e9909336028cd29e1895/edit#1-1-line%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E5%85%A8%E4%BD%93%E5%83%8F)
    1-2. [LINEログインからユーザーのメールアドレスを取得する方法](https://qiita.com/drafts/e9909336028cd29e1895/edit#1-2-line%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%8B%E3%82%89%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)
2. [SorceryはどのようにLINEログイン処理を行うか](https://qiita.com/drafts/e9909336028cd29e1895/edit#2-sorcery%E3%81%AF%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%ABline%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86%E3%82%92%E8%A1%8C%E3%81%86%E3%81%8B)
    2-1. [Sorceryの前提知識](https://qiita.com/drafts/e9909336028cd29e1895/edit#2-1-sorcery%E3%81%AE%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98)
    2-2. [Sorceryのソースコードを読んでみる](https://qiita.com/drafts/e9909336028cd29e1895/edit#2-2-sorcery%E3%81%AE%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%AA%AD%E3%82%93%E3%81%A7%E3%81%BF%E3%82%8B)
    2-3. [@user_hashの中身を見てみる](https://qiita.com/drafts/e9909336028cd29e1895/edit#2-3-user_hash%E3%81%AE%E4%B8%AD%E8%BA%AB%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%82%8B)
    2-4. [IDトークンを手動でLINEログインに送り、検証してみる](https://qiita.com/drafts/e9909336028cd29e1895/edit#2-4-id%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E6%89%8B%E5%8B%95%E3%81%A7line%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AB%E9%80%81%E3%82%8A%E6%A4%9C%E8%A8%BC%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)
3. [SorceryはIDトークンを検証してくれるのか？](https://qiita.com/drafts/e9909336028cd29e1895/edit#3-sorcery%E3%81%AFid%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%97%E3%81%A6%E3%81%8F%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B)
    [（補足） IDトークンを手動でLINEログインに送り、検証してみる](https://qiita.com/drafts/e9909336028cd29e1895/edit#%E8%A3%9C%E8%B6%B3-id%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E6%89%8B%E5%8B%95%E3%81%A7line%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AB%E9%80%81%E3%82%8A%E6%A4%9C%E8%A8%BC%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)
4. [まとめ](https://qiita.com/drafts/e9909336028cd29e1895/edit#%E3%81%BE%E3%81%A8%E3%82%81)
5. [参考資料](https://qiita.com/drafts/e9909336028cd29e1895/edit#%E5%8F%82%E8%80%83%E8%B3%87%E6%96%99) 

# 1. LINEログインのフロー
### 1-1. LINEログインの全体像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3695534/009b4fe0-67f3-dacb-e2fa-3fec18c1fb8c.png)
（転載：https://developers.line.biz/ja/docs/line-login/integrate-line-login/#login-flow）

上図に沿って、一つずつ解説します。
ここでのポイントは、**「手順１０（下方）でアクセストークンと共に、IDトークンもLINE PlatformからWebアプリに送られること」** です。
<br/>

1. 【A. ユーザー】LINEログインを開始する
2. 【B. Webアプリ】ユーザーをLINE Platformの認可URLにリダイレクトする
    - このURLパラメータには、`redirect_uri`と`state`を含める
        - `redirect_uri`：認証・認可完了後に、ユーザーがリダイレクトされるWebアプリのURL
        - `state`：CSRFを防ぐためのランダムな文字列
3. 【C. LINE Platform】LINEログイン画面をユーザーに表示する
4. 【A. ユーザー】認証する（LINEにログインする）
5. 【C. LINE Platform】認可画面をユーザーに表示する
6. 【A. ユーザー】認可する
7. 【C. LINE Platform】ユーザーを、`redirect_uri`にリダイレクトする。その際、下記2つの情報が`redirect_uri`のパラメータに付加される
    - `state`：手順２でLINE PlatformがWebアプリから受け取ったもの
    - `authorization code`：ユーザーが認可された際に作られるコード。Webアプリはこれを用いて、LINE Platformからアクセストークンを取得する。
8. 【B. Webアプリ】手順２でLINE Platformに送った`state`と、手順７でLINE Platformから受け取った`state`が同一であるか検証する
9. 【B. Webアプリ】LINE Platformに、`authorization code`を送り、アクセストークンをリクエストする
10. 【C. LINE Platform】`authorization code`を検証し、Webアプリにアクセストークンを送る
    - <font color="orange">**図には載っていませんが、この時にアクセストークンと共にIDトークンも送られます。**</font>
11. 【B. Webアプリ】アクセストークンを使い、LINE Platformにユーザー情報をリクエストする
12. 【C. LINE Platform】アクセストークンを検証し、Webアプリにユーザー情報を送る

### 1-2. LINEログインからユーザーのメールアドレスを取得する方法
まず、ユーザーのメールアドレスを取得するためには、以下の3つの準備が必要です。

1. メールアドレスの取得権限の申請
    申請方法は[こちら](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#applying-for-email-permission)から
    <br/>

2. ユーザーをリダイレクトする際（手順２）に、`scope`パラメーターに`email`を指定する
    指定する方法は[こちら](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#scopes)から
    <br/>

3. IDトークンの検証

https://developers.line.biz/ja/docs/line-login/verify-id-token/#get-an-id-token

先ほどのログインフローでは、アクセストークンをLINE Platformに送ると、ユーザー情報が取得できるとあります。しかし、ユーザーのメールアドレスは、これでは取得できません。**IDトークンの検証によってはじめて取得できる点に注意してください。**
<br/>


では、Sorceryが③IDトークンの検証を行ってくれるのか、見ていきましょう（①取得権限の申請、②`scope`パラメーターに`email`を指定はできている前提とします）。

# 2. SorceryはどのようにLINEログイン処理を行うか
### 2-1. Sorceryの前提知識
Sorceryのインストール方法など、基本的な使い方はこちらを参照してください

https://github.com/Sorcery/sorcery

Sorceryの外部認証機能（external）については、こちらをご覧ください

https://github.com/Sorcery/sorcery/wiki/External

### 2-2. Sorceryのソースコードを読んでみる
SorceryのExternalのソースコードを見てみます。
すると、アクセストークンを使い、`user`情報を取得するメソッドがあります。

```lib/sorcery/controller/submodules/external.rb
def sorcery_fetch_user_hash(provider_name)
  provider = sorcery_get_provider provider_name
  if @provider.nil? || @provider != provider
    @provider = provider
    @access_token = nil
    @user_hash = nil
  end

  @access_token ||= @provider.process_callback(params, session) # sends request to oauth agent to get the token
  @user_hash ||= @provider.get_user_hash(@access_token) # uses the token to send another request to the oauth agent requesting user info
  nil
end
```

しかし、先ほど確認した通り、メールアドレスの取得には、アクセストークンではなく**IDトークンの検証**が必要です。
`sorcery_fetch_user_hash(provider_name)`では、あくまでアクセストークンを使ったユーザー情報の取得を行っているにすぎません。

では、念のため、そこで取得された`@user_hash`の中身を見てみましょう。

### 2-3. `@user_hash`の中身を見てみる
前提として、開発中のRailsアプリに、Externalのドキュメント通りにログイン機能を実装しています。

また、`else`内の初めに`binding.pry`を設置し、処理を止めています。
```app/controllers/oauths_controller.rb
  def callback
    provider = params[:provider]
    if @user = login_from(provider)
      redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
    else
      begin
        binding.pry # ここでデバッグをします
        @user = create_from(provider)
        reset_session
        auto_login(@user)
        redirect_to root_path, :notice => "Logged in from #{provider.titleize}!"
      rescue
        redirect_to root_path, :alert => "Failed to login from #{provider.titleize}!"
      end
    end
  end
```
なお、デバッグした時点で、既に`login_from(provider)`が実行されています。そこには、`sorcery_fetch_user_hash`が含まれるので、これも実行済みです（下記ソースコードをご参照ください）。
```lib/sorcery/controller/submodules/external.rb
def login_from(provider_name, should_remember = false)
    sorcery_fetch_user_hash provider_name
    ...
```
では、コンソール上で`@user_hash`の中身を見てみます。
```bash
@user_hash
=> {:token=>
"hoge",
:refresh_token=>"fuga",
:expires_at=>1727252560,
:expires_in=>1727252560,
:user_info=>
   {"userId"=>"aaa",
    "displayName"=>"山田太郎",
    "pictureUrl"=>
     "https://profile.line-scdn.net/xxx"},
  :uid=>"aaa"}
```
※個人情報が含まれるため、一部値を変更しています。

やはり、`@user_hash`にはメールアドレスが入っていませんでした。

### 2-4. IDトークンを手動でLINEログインに送り、検証してみる
1-1. LINEログインの全体像にて、アクセストークンと共に、IDトークンも送られると説明しました。
そのIDトークンは受け取れているのでしょうか？
同じ個所で`binding.pry`で処理を止めたまま、`access_token`を確認してみます。

```bash
access_token
=> #<OAuth2::AccessToken:xxx
...
  @params=
   {"token_type"=>"Bearer",
    "scope"=>"profile openid",
    "id_token"=>　　　　　　　　 # ここにIDトークンがある
     "fuga",
    "token_url"=>"https://api.line.me/oauth2/v2.1/token"},
...
```
すると、IDトークンは、アクセストークンと共に取得できているのが分かります。

# 3. SorceryはIDトークンを検証してくれるのか？
では、Sorcery（External）は、そのIDトークンを検証する処理を含むのでしょうか。
**答えはNO**です（少なくとも私は、externalのソースコードを読み解いても、IDトークンを検証する処理は、見つかりませんでした・・・）

### （補足） IDトークンを手動でLINEログインに送り、検証してみる
なお、curlコマンドを使い、手動でIDトークンをLINE Platformに送り検証すると、メールアドレスが取得できることが確認できました。このことから、LINE Platform側には問題がないことが分かります。
```bash
# curlコマンドでIDトークンを検証してみる
curl -v -X POST 'https://api.line.me/oauth2/v2.1/verify' \
  -d 'id_token=fuga' \
  -d 'client_id=xxx'
```
```bash
# メールアドレスが返ってきた

{"iss":"https://access.line.me",(省略）,"name":"山田太郎","email":"taro@gmail.com"}

```

# 4. まとめ
1. LINEログインからメールアドレスを取得するには、IDトークンの検証が必要
2. SorceryにはIDトークンの検証処理は含まれていない
3. したがって、Sorceryを使うだけではLINEからユーザーのメールアドレスを取得できない。別途自分で処理を実装するなどして、工夫が必要

## 5. 参考資料
https://developers.line.biz/ja/docs/line-login/integrate-line-login/#login-flow

https://github.com/Sorcery/sorcery/wiki/External

https://speakerdeck.com/minako__ph/lineroguinnoroguinhurowoli-jie-siteshi-u