LINEログイン v2.1を使ったログイン(PKCEも対応)

# 概要

LINEログイン v2.1を使ったログインとメッセージ送信のPHPサンプルコードです。

# コード

こちらに公開しています。
https://github.com/takezi/line-login

# コードの要点を解説

LINEのログインはOAuth 2.0/OpenID Connectに基づいた実装となっています。

## ユーザーに認証と認可を要求する

ドキュメントはこちらです。
https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request

    session_start();
    $_SESSION['oauth_state'] = bin2hex(random_bytes(16));
    $_SESSION['oauth_nonce'] = bin2hex(random_bytes(16));
    $_SESSION['oauth_code_verifier'] = base64url_encode(random_bytes(32));

    $url = LINE_LOGIN_AUTHORIZE_URL . '?' . http_build_query([
        'response_type' => 'code',
        'client_id' => LINE_LOGIN_CHANNEL_ID,
        'redirect_uri' => LINE_LOGIN_CALLBACK_URL,
        'state' => $_SESSION['oauth_state'],
        'scope' => LINE_LOGIN_SCOPE,
        'nonce' => $_SESSION['oauth_nonce'],
        'code_challenge' => base64url_encode(hash('sha256', $_SESSION['oauth_code_verifier'], true)),
        'code_challenge_method' => 'S256'
    ]);

    // 認可URLにリダイレクト
    header("Location: $url");

セキュリティ対策として、state,nonce,code verifier(PKCE)を使っています。
全ては必要ないかも知れませんが、目的が異なり、さほど手間でも無いため、全て入れておいて良いでしょう。

stateとnonceはrandom_bytesで生成しています。GUIDと同じ16バイトあれば十分でしょう。
code verifierは[RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)に、使える文字と最低文字文字数が43文字と定められています。
RFCに、32バイトのランダムバイトをBase64URLエンコードすれば43文字が生成できるとあります。

Base64URLエンコードとはBase64の一部の文字を、URLエンコードされない文字に置換して、末尾のパディングを除去したものです。
次のような関数を作って変換することができます。

    // Base64URLエンコードするための関数
    function base64url_encode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

## コールバックURLで認可コードを受け取る

認可が完了すると、ユーザー画面はコールバックURLにリダイレクトされます。

ドキュメントはこちらです。
https://developers.line.biz/ja/docs/line-login/integrate-line-login/#receiving-the-authorization-code-or-error-response-with-a-web-app

まずstateの検証を行います。

    session_start();

    // stateの検証
    if($_SESSION['oauth_state'] != $_GET['state']) {
        echo '認可サーバーが無効なstateパラメータを返却しました。';
        exit;
    }

次にエラーレスポンスかどうかチェックします。この辺りは [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) に従っています。

    // エラーチェック
    if(isset($_GET['error'])) {
        echo '<p>許可サーバーがエラーを返しました：' . htmlspecialchars($_GET['error'], ENT_QUOTES, 'UTF-8') . '</p>';
        echo '<p>' . htmlspecialchars($_GET['error_description'], ENT_QUOTES, 'UTF-8') . '</p>';
        exit;
    }

## アクセストークンを発行する

受け取った認可コードを使って、アクセストークンを発行します。

ドキュメントはこちらです。
https://developers.line.biz/ja/reference/line-login/#issue-access-token

    $body = [
        'grant_type' => 'authorization_code',
        'code' => $_GET['code'],
        'redirect_uri' => LINE_LOGIN_CALLBACK_URL,
        'client_id' => LINE_LOGIN_CHANNEL_ID,
        'client_secret' => LINE_LOGIN_CHANNEL_SECRET,
        'code_verifier' => $_SESSION['oauth_code_verifier']
    ];
    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/x-www-form-urlencoded',
            'content' => http_build_query($body, '', '&')
        ]
    ]);
    $res = json_decode(file_get_contents(LINE_LOGIN_TOKEN_URL, false, $context), true);

素のPHPでPOSTしているので多少冗長です。何かしらのライブラリを利用する場合は書き換えてください。

code verifierの検証は、このタイミングでLINEのサーバー側で行われます。
レスポンスにはアクセストークンに加えて、IDトークンも含まれます（scopeでopenidを指定した場合）。

## IDトークンを検証する

通常IDトークンは自力で検証しますが、LINEは検証用のAPIを用意してくれています。
https://developers.line.biz/ja/reference/line-login/#verify-id-token

nonceの検証も、このAPIで実施できます。

    $body = [
        'id_token' => $res['id_token'],
        'client_id' => LINE_LOGIN_CHANNEL_ID,
        'nonce' => $_SESSION['oauth_nonce']
    ];
    unset($_SESSION['oauth_nonce']);
    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/x-www-form-urlencoded',
            'content' => http_build_query($body, '', '&'),
            'ignore_errors' => true
        ]
    ]);
    $res = json_decode(file_get_contents(LINE_LOGIN_VERIFY_URL, false, $context), true);
    if (isset($res['error'])) {
        echo 'IDトークンの検証に失敗しました：' . htmlspecialchars($res['error_description'], ENT_QUOTES, 'UTF-8');
        exit;
    }

## ログインセッションを設定

最後に取得できたLINEのユーザーIDと名前をセッションに保存します。

    session_regenerate_id(true);
    $_SESSION = [];
    $_SESSION['user_id'] = $res['sub'];
    $_SESSION['name'] = $res['name'];

ユーザーIDを使ってLINE公式アカウントで、特定のユーザーだけにメッセージを送ることも可能です。
メッセージ送信のサンプルは [github](https://github.com/takezi/line-login) に置いてある `send.php` を参考にしてください。

ユーザーIDは、LINE Developersコンソールで最初に作成したプロバイダの単位でユニークです。
プロバイダが異なると同じLINEユーザーでも異なるIDが割り振られます。