【Laravel11】LINEログインの実装

# はじめに
最近ではWEBでもモバイルアプリでもメアド（ID）ログインの他にGoogleやLINEなどソーシャルメディア経由のログイン方法は当たり前になってきています。ログインが必要なアプリケーションにおいてはこれらを実装するかどうかでユーザー数も変わってくるのではと考えています。今回はLaravel11でのLINEログインの実装を紹介いたします。

## ソーシャルメディアログインでのメリット・デメリット
前段としてソーシャルメディアログインを実装するメリット・デメリットを押さえておきます。「そんなのいいから早く実装方法教えて！」って方は読み飛ばしていただいて構いません。
### メリット
* メールアドレスやパスワードの入力が不要のため、ユーザーを会員登録まで持っていきやすい。（今回の実装方法は初回ログイン時はDBにユーザーデータが入るため実質新規登録となります）ユーザーは画面の案内にしたがってボタンをポチポチしてけば完了するため、UX向上が期待できる。
* ユーザーはパスワードの管理をしなくて済む。
* 各ユーザーデータにメアド・パスワードが存在しないため、これらが盗まれる心配がなくなる。
* LINE公式アカウントと連携させることで、ログイン時に友達追加の勧誘ができる。
### デメリット
* 実装工数が発生する
* ログイン履歴が連携元のタイムラインなどに記載されてしまう可能性がある

デメリットの２つめについてはアプリ側で利用者規約でどのような情報を取得し、アプリ内のどこに使用するかを明記する必要があります。ユーザーはこれに同意することによってログインを実現できるようにさせればよいかと。

# 実装してみよう
では早速実装に移っていきましょう。実装手順は以下のような感じで行きます
1. LINEログイン用のチャネルを作成
1. 作成したチャネルにコールバックURIを設定
1. LINEログイン用のルーティング設定
1. コントローラー作成

### LINEログイン用のチャネルを作成
[こちら](https://developers.line.biz/ja/)からLINEデベロッパーズのコンソールにログインし、「LINEログイン」を選択してチャネルを新規作成します。
![スクリーンショット 2024-11-05 20.49.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364507/e1829501-f046-2c3b-5252-1acd113ab8b2.png)

![スクリーンショット 2024-11-05 19.58.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364507/5f556f6c-7e97-9809-f04f-e9c80227e7e0.png)

各項目を入力し、作成ボタン押下でLINEログインのチャネルが作成されます。
表示されているチャネルIDは後ほど使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364507/10b6a41d-d0ae-bebf-99fd-0131901e506f.png)
![スクリーンショット 2024-11-05 20.01.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364507/c3d087d9-0eb6-51fd-336c-b42c928dbaf6.png)

### 作成したチャネルにコールバックURLを設定
「LINEログイン設定」タブにあるコールバックURLの箇所から編集ボタンを押してコールバックURLを指定します。これはLINEログインのAPIがログイン処理のあとにこちらのアプリケーションにリダイレクトさせる場所です。今回はこのコールバックURLとLaravelのルーティングを同期させてコントローラーに処理を投げます。
![スクリーンショット 2024-11-05 20.08.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364507/ede8d1eb-6edc-5f98-9d1b-e1e2c887ef51.png)

### LINEログイン用のルーティング設定
ここからはLaravel側の操作です。プロジェクトは作成済みの前提です。今回はweb.phpを編集します。
ユーザーがLINEログインを開始するためのURL生成処理、callbackの2つのルーティングが必要なので以下のように定義します。
```PHP:web.php
<?php
use App\Http\Controllers\Auth\LineLoginController;

 // LINEログインを開始するためのURL生成処理
 Route::get('/line-login', [LineLoginController::class, 'createLoginLink'])->name('linelogin');
 // コンソールで指定したコールバックURLと合わせる
 Route::get('/callback', [LineLoginController::class, 'callback'])->name('callback');
```
コントローラーやメソッド名はご自由にで大丈夫です。

:::note warn
callbackのルーティングはコンソールで設定したパス名と合わせるようにしてください！
:::

### コントローラー作成
続いてコントローラーです。私は以下のように実装しました。
```PHP:LineLoginController
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;

class LineLoginController extends Controller
{
    /**
     * LINEログイン画面表示
     *
     * @return void
     */
    public function createLoginLink()
    {
        $state = Str::random(32);
        $nonce  = Str::random(32);
      
        $uri ="https://access.line.me/oauth2/v2.1/authorize?";
        $response_type = "response_type=code";
        $client_id = "&client_id=".config('services.line.client_id');
        $redirect_uri ="&redirect_uri=".config('services.line.redirect');
        $state_uri = "&state=".$state;
        $scope = "&scope=openid%20profile";
        $prompt = "&prompt=consent";
        $bot_prompt = "&bot_prompt=aggressive";
        $nonce_uri = "&nonce= . $nonce";
  
        $url = $uri . $response_type . $client_id . $redirect_uri . $state_uri . $scope . $prompt . $bot_prompt . $nonce_uri;
 
        return response()->json(['url' => $url]);
    }
 
    public function getAccessToken($req)
    {
        $headers = [ 'Content-Type: application/x-www-form-urlencoded' ];
        $post_data = array(
            'grant_type'    => 'authorization_code',
            'code'          => $req['code'],
            'redirect_uri'  => config('services.line.redirect'),
            'client_id'     => config('services.line.client_id'),
            'client_secret' => config('services.line.client_secret'),
        );
        $url = 'https://api.line.me/oauth2/v2.1/token';
    
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post_data));
    
        $res = curl_exec($curl);
        curl_close($curl); 
        $json = json_decode($res);
        $accessToken = $json->access_token;
    
        return $accessToken;
    }
 
    public function getProfile($at)
    {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $at));
        curl_setopt($curl, CURLOPT_URL, 'https://api.line.me/v2/profile');
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET');
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    
        $res = curl_exec($curl);
        curl_close($curl);
    
        return json_decode($res);
    }
 
    /**
     * システムにリダイレクト、ログイン（or登録）
     *
     * @param Request $request
     * @return void
     */
    public function callback(Request $request)
    {
        try {
            $accessToken = $this->getAccessToken($request);
            $profile = $this->getProfile($accessToken);

            // provide_user_idでDB検索し、ヒットしたらログイン
            $user = User::where('provider_user_id', $profile->userId)->first();

            // ヒットしなかったらユーザーを新規登録
            if(!$user){
                $user = User::create([
                    'name' => $profile->displayName,
                    'provider_user_id' => $profile->userId,
                ]);
            }
           
            Auth::login($user);
            return redirect(config('app.front_url'));
        } catch (\Exception $e) {
            Log::error($e->getMessage());
            return redirect(config('app.front_url'));
        }

    }
}
```
上記でconfigから値をとってきているのですが以下のように設定してます。
```PHP:config/services.php
<?php

return [

    ...省略

    'line' => [
        'client_id'=>env('LINE_CHANNEL_ID', ''),
        'client_secret' =>env('LINE_CHANNEL_SECRET', ''),
        'redirect'=>env('LINE_REDIRECT', ''),
    ],
];

```
```.env
LINE_CHANNEL_ID=チャネルID
LINE_CHANNEL_SECRET=チャネルシークレット
LINE_REDIRECT=コールバックに設定してるURL
```

チャネルID、チャネルシークレットはいずれもLINEデベロッパーズコンソールのチャネル基本設定タブに表示されています。

ではコントローラーのポイントを押さえておきます。最初のcreateLoginLinkメソッドですが、ここは見た通りなので色々なパラメータを設定してガッチャンコしてURLを生成してるってだけです。ちなみに、私は今回、apiとしてLaravelを利用しているのでreturnは必要に応じてviewを指定してください（ルーティングがweb.phpにしてるのは極力話をシンプルにするためなのでわざとです）。ユーザーがこの生成されたURLを踏むとLINEログインのAPIが走り、Laravel側のcallbackにリクエストが返ってきます。
getAccessTokenとgetProfileメソッドはこのコントローラないで利用するメソッドなので飛ばします。

そして肝心のcallbackメソッドです。まず以下の記述ですが、ここでLINEログインしたユーザー情報を上述のgetAccessToken, getProfileメソッドにて取得しています。
```
$accessToken = $this->getAccessToken($request);
$profile = $this->getProfile($accessToken);
```
後述の処理はユーザーのログインor新規登録です。今回はusersテーブルに「provider_user_id」カラムを用意してここでユーザーの存在確認を行います。LINEログイン時にLINE側がアカウントを識別するユニークIDがprovier_idです。最後に`` Auth::login($user) ``としてユーザーを認証状態にしておけば完了です。

# まとめ
いかがでしょうか。大事な部分の処理はLINE側が用意してくれてるので開発者は容易に実装できる仕組みになってると個人的には思います。LINEログインは最近のアプリケーションでは当たり前に用意されてるので、新規登録フローでのユーザー離脱を防ぐ上で効果的なのでぜひ導入してみてはいかがでしょうか。（LINEログインの勧誘みたいになっちゃった）
もし記述内容に誤りがあればご指摘いただけますと幸いです。
また、[こちら](https://qiita.com/oono_dev/items/bf647cb77b65764d14c5)の記事にて僕が日々どんなモチベーションや心構えでエンジニアリングしているのかを執筆しているのでよければ覗いてみてください🎵最後まで読んでいただきありがとうございました！！