SNSを使用したログイン – LINE 実装編

投稿者: | 2018年6月21日

LINE によるログインから実装します。実装の難易度は私見で、中間からやや簡単になります。日本語のドキュメントがあり、必要な情報はほぼ得られますので、学習にはもってこいと思われます。

各種情報

前提条件

  • 言語はPHPで、バージョンは 5.6 を想定
  • フレームワークに CodeIgniter 3 を使用
  • WebサイトのURLは、https://example.com と想定して説明します

参考URL

多用している CodeIgniter のコマンド

redirect(リダイレクト先, リダイレクトメソッド, HTTPレスポンスコード)

リダイレクト先へリダイレクトします。リダイレクト先に ‘login’ と指定していれば、Loginコントローラーのindexメソッドへリダイレクトします(http://example.com/login/index)。素のPHPであれば、header(‘Location: http://example.com/login/index’); のようになります。

$this->input->get_post('・・・')
$this->input->post('・・・')

$_GET$_POSTから値を取り出します。その値(項目)がないときは、null を返してくれます。get_post() は先に $_GET を調べて、値がなければ $_POST を調べてくれます。post()$_POSTだけを調べます。

$this->load->library('・・・')
$this->load->view('・・・')

ライブラリやビューとして作成した PHP ファイルをロードします。require_once '・・・' と同じ意味です。

random_string('md5')

ランダムな文字列を作成する関数です。'md5' を指定しているのはちょうど 32 文字の文字列を返してくれるからで、他意はありません。推測されにくいランダムな英数字の文字列であることが重要です。

LINE用ライブラリの作成

LINE ログイン用のライブラリを作成します。処理の大部分はこのライブラリに書いていきます。CodeIgniter でのライブラリは、以下のようにクラスとして作成することになっています。
ログインに必要な情報は、あらかじめ変数(以下のコードでは private メンバー)に入れておきましょう。

Login_library.php

<?php
defined('BASEPATH') or exit('No direct script access allowed');

/**
 * Lineログインライブラリ
 */
class Line_library
{
    /**
     * フィールド
     */
     
    // CodeIgniterのライブラリで使用する場合があるもの。
    // CodeIgniterを使わないなら気にする必要なし
    private $ci;

    // アプリ認証情報
    // アプリを登録した際の Channel ID
    private $clientId = '00000000';
    
    // アプリを登録した際の Channel Secret
    private $clientSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

    // リダイレクト先
    private $redirectUri = 'https://example.com/login/line';

    // APIのURL(エンドポイント)
    private $authUrl = 'https://access.line.me/oauth2/v2.1/authorize?';
    private $tokenUrl = 'https://api.line.me/oauth2/v2.1/token';
    private $verifyUrl = 'https://api.line.me/oauth2/v2.1/verify';
    private $revokeUrl = 'https://api.line.me/oauth2/v2.1/revoke';
    
    // トークンの発行者
    private $lineAuthorUri = 'https://access.line.me';

    // curlのヘッダーに追加するデータ
    private $curlHeader = 'Content-Type: application/x-www-form-urlencoded';

    /**
     * コンストラクタ
     */
    public function __construct()
    {
        // CodeIgniterのライブラリから他のライブラリなどを呼び出すときに必要
        $this->ci = &get_instance();
        
        // cURLを使うのでライブラリをロードしておく
        // 素のPHPでは require_once 'curl_library.php'; を実行するのと同じ
        $this->ci->load->library('curl_library');
    }
}

エンドポイント(APIのURL)は変更になる可能性がありますので、公式ドキュメントを参照して入力してください。なお、上記コードに書いているエンドポイントのうち、今回使用するのは$authUrl$tokenUrlの2つです。

Loginコントローラーの準備

LINE ログインのための準備を、Login コントローラーで行います。
以下のようにコンストラクタで、Line_library ライブラリをロードしておきます。
Login.php

/**
 * コンストラクタ
 */
public function __construct()
{
    parent::__construct();
    
    // 以下の行を追加
    // line_library をロードする
    $this->load->library('line_library');
}

なお、CodeIgniterを使っていない場合、次のようなコードになると思います。
require_once __DIR__ . ‘/line_library.php’;

indexメソッドの修正(Loginコントローラー)

Loginコントローラーのindexメソッドに、LINEログイン時の飛び先を指定します。
Login.php

/**
 * インデックスページを表示する
 */
public function index()
{
    // 押されたボタンによってログインの処理を分岐
    switch ($this->input->post('submit')) {
        case 'line':
            // LINEログイン開始メソッドへ移動
            redirect('login/login_line', 'location', 302);
            break;

LINEの認証画面へのURLを組み立てて、リダイレクトする

login_lineメソッドの作成(Loginコントローラー)

Loginコントローラーに login_line メソッドを追加します。このメソッドがログイン処理のスタートになります。
ログイン処理で最初に行うのは、LINE の認証画面へリダイレクトすることです。コードは以下のとおりです。
Login.php

/**
 * LINEでのログインを行う
 */
public function login_line()
{
    // LINEの認証エンドポイントを取得してリダイレクト
    $api = $this->line_library->snsLoginApi();
    redirect($api, 'location', 302);
}

snsLoginApiメソッドの作成(Line_libraryライブラリ)

snsLoginApi メソッドを、Line_library ライブラリに追加します。
Line_library.php

/**
 * ログイン認証APIのURLを返す
 * @return  string  認証APIのURL
 */
public function snsLoginApi()
{
    // random_string関数を使用するためのヘルパーをロード(CodeIgniterの機能です)
    $this->ci->load->helper('string');

    // セキュリティ用にランダムな文字列を作成してセッションに一時保管
    $state = random_string('md5');
    $nonce = random_string('md5');
    $auth = [
        'state' => $state,
        'nonce' => $nonce,
    ];

    $_SESSION['sns_auth'] = $auth;

    // 認証エンドポイントへ送信するパラメーター
    $params = [
        'response_type' => 'code',
        'client_id' => $this->clientId,
        'redirect_uri' => $this->redirectUri,
        'state' => $state,
        'scope' => 'profile openid',
        'nonce' => $nonce,
    ];

    // 認証エンドポイントのURLを作成
    $result = $this->authUrl . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
    return $result;
}

まず、state と nonce を作成します。
stateはクロスサイトリクエストフォージェリ防止用の文字列です。英数字を使用します。ランダムな文字列で、適度な長さが必要です。32バイト以上の長さが望ましいです。上記のコード例では random_string という CodeIgniter の関数を使用して md5 のハッシュを作成していますが、特定のアルゴリズムを使用する必要はありません。
nonce はリプレイアタックを防止するための文字列です。こちらも適当な長さのランダムな文字列を作成します。32バイト以上が望ましいです。
作成した state と nonce は、セッションに保管しておきます。LINE でログインするために一旦自分のサイトを離れるので、state と nonce が失われないようにします。

認証エンドポイントにはいくつかのパラメーターを付けてリダイレクトします。

パラメーター必須内容
response_type必須'code' という文字列を指定します。
client_id必須LINEから発行されたChannel IDです。
redirect_uri必須LINEに登録したリダイレクトURLです。登録したとおりの文字列を指定する必要があります。
state必須作成したランダムな文字列です。
scope必須アプリが求める権限です。profile、openid、emailの権限を指定できます。複数の権限を指定するには空白で区切りますが、空白は%20にエンコードする必要があります。詳しくは後で説明します。
nonce任意nonceは任意です。しかし、openidの権限を指定したときは、セキュリティ向上のために必ず指定しましょう。

そのほかにもパラメーターがあります。すべてのパラメーターを確認するには、公式ドキュメントを参照してください。

配列にしたパラメーターを、http_build_query 関数でエンコードして、クエリ文字列にします。このとき、第4引数(enc_type)には PHP_QUERY_RFC3986 を必ず指定してください。これを指定しなかった場合、空白が'+'(プラス)にエンコードされるため認証できません。空白を %20 にエンコードするには、第4引数を正しく指定します。

認証ページからリダイレクトされた後(コールバック)の処理

ユーザーが LINE でログインして権限を認可すると(あるいは権限を認可しなかった場合でも)、パラメーターのリダイレクトURL へ処理が戻ってきます。
認可された場合、認可コードが URL に付加されています。認可されなかった場合、URL にエラーが付加されています。
また、URL には state の文字列がそのまま付加されています。戻ってきた state と、セッションに保管している state が一致していることを確認します。

認可コードが付加されており、state に問題がなければ、認可コードを使用してアクセストークンをリクエストします。
ユーザーにメッセージを送ったり、プロフィールを取得したりするときにはアクセストークンを使用します。なお、アクセストークンには有効期限があります。

認証時の権限に openid を指定した場合、アクセストークンと一緒に、IDトークンを取得できます。IDトークンにはユーザープロフィールや各種情報が含まれています。IDトークンは JWT でエンコードされています。単にログイン認証を行うだけであれば IDトークンを取得する必要はありませんが、JWT のライブラリを作成したので、IDトークンのデコードと検証まで説明します。

コールバック関数(この例では Loginコントローラーの line メソッド)

Login.php

/**
 * LINEのログイン認可コードを処理する
 */
public function line()
{
    // アクセストークンをリクエストし、IDトークンをデコードする
    $access = $this->line_library->getAccessToken();
    
    // 戻り値にerrorが含まれている場合、最初のログイン画面へ戻る
    if (!empty($access['error'])) {
        $_SESSION['message'] = $access['error'];
        redirect('login', 'location', 302);
    }

    // ログインできたので、結果を表示する
    $viewdata = [
        'message' => 'LINEでログインしました。',
        'data' => $access,
    ];

    $this->load->view('logined', $viewdata);
}

このメソッドでは処理を Line_library ライブラリに丸投げします。ライブラリの getAccessToken でアクセストークンのリクエストから、IDトークンのデコードまで行います。
戻り値の配列に error が含まれている場合、ログインページへ戻り、エラーメッセージを表示します。
error が含まれていないときはログイン完了ページを表示します。

getAccessTokenメソッドの作成(Line_libraryライブラリ)

Line_library.php

/**
 * 認証コールバックを処理してアクセストークンを取得する
 * @return  array   処理結果
 */
public function getAccessToken()
{
    // jwtライブラリのロード(require_once 'jwt.php' と同義)
    $this->ci->load->library('jwt');

	// レスポンスの取得
	// if (isset($_GET['code'])) { $code = $_GET['code']; } と同義
    $code = $this->ci->input->get_post('code');
    $state = $this->ci->input->get_post('state');
    $error = $this->ci->input->get_post('error');
    $errorDesc = $this->ci->input->get_post('error_description');

    // セッションに保管していた state と nonce を読み出す
    $savedState = $_SESSION['sns_auth'];
    
    // セッションの情報は削除しておく
    unset($_SESSION['sns_auth']);

    // 戻り値の初期化
    $result = [
        'error' => '',
        'access' => '',
        'refresh' => '',
        'expires' => 0,
        'data' => [
            'code' => 0,
        ],
    ];

    // レスポンスのstateと、最初に作成したstateが一致しているか確認
    if ($savedState['state'] !== $state) {
        $result['error'] = '認証エラーが発生しました。';
        return $result;
    }

    // レスポンスにエラーが含まれているか
    if (!is_null($error)) {
        if (!is_null($errorDesc)) {
            $result['error'] = $errorDesc;
        } else {
            $result['error'] = 'LINEの認証がキャンセルされました。';
        }

        return $result;
    }

    // 認証コードを使用してアクセストークンを取得する
    // 送信するパラメーター
    $posts = [
        'grant_type' => 'authorization_code',
        'code' => $code,
        'redirect_uri' => $this->redirectUri,
        'client_id' => $this->clientId,
        'client_secret' => $this->clientSecret,
    ];

    // curlのヘッダーを指定
    $options = [
        CURLOPT_HTTPHEADER => [$this->curlHeader],
    ];

    // curlの実行
    $data = $this->ci->curl_library->execPostCurl($this->tokenUrl, $posts, $options);
    $idToken = $data['response']['id_token'];
    $result['refresh'] = $data['response']['refresh_token'];
    $result['access'] = $data['response']['access_token'];
    $result['expires'] = intval($data['response']['expires_in']);

    // ペイロードの検証用データの設定
    $auths = [
        'iss' => $this->lineAuthorUri,
        'aud' => $this->clientId,
        'nonce' => $savedState['nonce'],
    ];

    // IDトークンをデコード
    $value = $this->ci->jwt->decode($idToken, $this->clientSecret, $auths);
    if (is_null($value)) {
        $result['error'] = 'データが不正です。再度、「LINEでログイン」からログインを行ってください。';
        $result['data']['error'] = $this->ci->jwt->errorCode;

        return $result;
    }

    // デコードしたペイロードから必要なデータだけ取り出す
    $result['data']['sub'] = $value['sub'];
    $result['data']['exp'] = intval($value['exp']);
    $result['data']['iat'] = intval($value['iat']);
    $result['data']['name'] = isset($value['name']) ? $value['name'] : null;
    $result['data']['picture'] = isset($value['picture']) ? $value['picture'] : null;
    $result['data']['email'] = isset($value['email']) ? $value['email'] : null;

    // トークンの有効期限を確認
    if ($result['data']['exp'] <= time()) {
        $result['data']['code'] = 200;
        $result['error'] = 'ログインの有効期限が切れています。再度、「LINEでログイン」からログインを行ってください。';
    }

    return $result;
}

LINEの認証画面からリダイレクトされたとき、URLのパラメーターにcodeという名前で、認可コードが付加されてきます。アクセストークンのリクエストにはこの認可コードが必要になります。ユーザーが権限に同意しなかったり、何らかのエラーが発生したりした場合、error というパラメーターが付いてきます。また、場合によっては error_description というパラメーターが付くこともあるようです。
さらに、URL には state というパラメーターが付加されています。これは最初に作成した state の文字列が、そのまま返ってきます。返ってきた state と最初に作成したstateを比較して、同じ文字列のときに処理を続行します。もし state が異なっている場合、クロスサイトフォージェリ(CSRF)の可能性を考え、処理を中断します。

認可コードを取得できた場合、アクセストークンをリクエストします。アクセストークンのリクエストは、token エンドポイントに cURL でデータを POST します。このときヘッダーに 'Content-Type: application/x-www-form-urlencoded' を入れる必要があります。POST するデータは以下のとおりです。すべて必須項目です。

パラメーター内容
grant_type'authorization_code' を指定します。
code取得した認可コードです。
redirect_uriリダイレクトURLです。認証のために必要で、このURLにリダイレクトされることはありません。
client_idアプリのChannel IDです。
client_secretアプリのChannel Secretです。

cURL のレスポンスにはアクセストークンのほかに、アクセストークンの有効期限が切れるまでの秒数、リフレッシュトークンなどが返ってきます。また、権限(scope)に openid を指定していた場合、IDトークンも含まれます。

上記のコードはIDトークンも取得している前提となっています。IDトークンは JWT の形式で取得できます。これをデコードするとヘッダー、ペイロード、署名に分割できますが、必要としているユーザー情報はペイロードに入っています。ペイロードにはユーザー情報のほかに、検証用のデータも含まれます。その一つが最初に作成した nonce です。ペイロードをデコードした後、nonce などのデータを検証します。
検証が必要(または、検証した方がよい)データは次のとおりです。

ペイロードの項目説明
issIDトークンの発行者。IDトークンを生成したURLです。LINEから送信されたIDトークンであることを確認します。
audアプリのChannel ID。自分(アプリ)がリクエストして発行されたIDトークンであることを確認します。
nonce最初に作成したnonceと同じ文字列。リプレイアタック防止用です。セッションにnonceを保管したままだと、何度も同じ文字列で認証できてしまいます。nonceをセッションから読み出したときに、セッションのデータは削除しておきましょう。

これらの検証用データを配列にして JWT ライブラリに渡せば、ライブラリ内で検証してくれます。
もし、データに不一致があった場合、JWT ライブラリの decode メソッドは、null を返します。

LINE の JWT の署名アルゴリズムは、HS256 です。ハッシュのキーは Channel Secret の値となっています。decode メソッドの secret 引数には、Channel Secretを渡します。
デコードしたIDトークンのペイロードには、以下の情報が含まれています。ただし、権限(scope)によっては含まれないものもあります。

IDトークン内の項目内容
subユーザーID。ログインしたユーザーの、LINEにおけるユーザーID。
expトークンの有効期限。UNIX時刻。
iatIDトークンを生成した時間。UNIX時刻。
nameユーザーの表示名。scopeにprofileを指定したときだけ含まれる。
pictureユーザープロフィール画像のURL。scopeにprofileを指定したときだけ含まれる。
emailユーザーのメールアドレス。scopeにemailを指定したときだけ含まれる。

デコードしたペイロードを取得したら、exp(トークンの有効期限)を確認しておきます。有効期限切れの場合、取得した情報は無意味なものです。

以上で LINE でのログインとユーザー情報の取得の完了です。一般的にこの後に考えられる処理は、アクセストークンとリフレッシュトークンを DB に保管し、取得したユーザー情報を元に、自分のサイトでユーザー登録をさせることです。自分のサイトでユーザー登録を行えば、アクセストークンはもう必要ないかもしれません。

LINE でボットを登録し、ユーザーにメッセージを送信するなどの場合、アクセストークンが必要となります。アクセストークンには有効期限があるので、有効期限が切れる前、または有効期限が切れて10日間が経過する前に、リフレッシュトークンでアクセストークンを更新できます。

ADs
  

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)