SNSを使用したログイン – Twitter実装編 Part.1 (失敗作)

投稿者: | 2018年7月9日

なぜ失敗作か、それはログインできないからです。いろいろと試行錯誤しましたが、エラーが返ってくるだけでログインどころか、Twitterの認証ページへ進むこともできません。
何で失敗作をわざわざ掲載するのか、それはログインの手順については間違っていないと考えるからです。

ここで示すコード例では、何度やってもエラーコード32、「Could not authenticate you」が返ってきます。エラー内容としては「認証できませんでした」ということです。素直に解釈すると、送信したデータに誤りがあるということになりますが、成功例のデータと比較しても違いがなく、なぜエラーになるのかさっぱり分かりません。
なお、このエラーが返ってく原因は単に送信データの間違いだけではなさそうです。自分のWebサイトが https になっていないのが原因とか、Consumer Secretを再生成すると直るとか、同じ症状について述べられているサイトが多数見つかります。どれも試してみましたが、結局解決に至っていません。
どなたか解決方法をご存じの方がいらっしゃいましたら、その方法を教えてください。

Twitterでログインするためのライブラリがいくつか公開されています。このライブラリのうち、TwitterOAuth を使用したログインを成功例として次回実装します。

各種情報

前提条件

  • 言語は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 文字の文字列を返してくれるからで、他意はありません。推測されにくいランダムな英数字の文字列であることが重要です。

Twitterログイン専用ライブラリの作成(Twitter_library.php)

Twitterログインに使用するライブラリを作成します。CodeIgniterの場合、以下のようにクラスとして作成することになっています。
Twitter_library.php

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

/**
 * twitterログインライブラリ
 */
class Twitter_library
{
    /**
     * フィールド
     */
    private $ci;

    // アプリ情報
    private $consumerKey = '00000000000000000000';
    private $consumerSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

    // コールバックURL
    private $callbackUrl = 'https://example.com/login/twitter';

    // エンドポイント
    private $apiOauthRequest = 'https://api.twitter.com/oauth/request_token';
    private $apiAuthenticate = 'https://api.twitter.com/oauth/authenticate';
    private $apiAccessToken = 'https://api.twitter.com/oauth/access_token';
    private $apiVerifyCredentials = 'https://api.twitter.com/1.1/account/verify_credentials.json';

    /**
     * コンストラクタ
     */
    public function __construct()
    {
        $this->ci = &get_instance();

        // ライブラリをロードしておく
        $this->ci->load->library('curl_library');
    }
}

Loginコントローラーの修正(Login.php)

Loginコントローラーのコンストラクターに、Twitter_libraryライブラリをロードするコマンドを追加します。
Login.php

/**
 * コンストラクタ
 */
public function __construct()
{
    parent::__construct();
    $this->load->library('line_library');
    $this->load->library('facebook_library');
    $this->load->library('yahoo_library');
    $this->load->library('instagram_library');
    $this->load->library('google_library');
    
    // 以下の行を追加
    $this->load->library('twitter_library');
}

Twitterログイン開始メソッドへリダイレクトされるよう、indexメソッドを以下のように修正します。
Login.php

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

        case 'yahoo':
            redirect('login/login_yahoo', 'location', 302);
            break;

        case 'twitter':
            // 以下の行を追加
            redirect('login/login_twitter', 'location', 302);
            break;

        case 'instagram':
            redirect('login/login_instagram', 'location', 302);
            break;

        case 'facebook':
            redirect('login/login_facebook', 'location', 302);
            break;

        case 'google':
            redirect('login/login_google', 'location', 302);
            break;
    }

Twitterの認証ページ(ログインページ)へリダイレクトするまでの処理

Twitterの認証ページへリダイレクトすることで、ユーザーが操作するブラウザーにTwitterのログイン画面が表示されます。認証ページへリダイレクトするには、以下の手順を踏む必要があります。

  1. 送信するパラメーターから署名を作成する
  2. パラメーターと署名をrequest_tokenエンドポイントへ送信し、認可コードを取得する
  3. authenticateエンドポイントへ、認可コードとともにリダイレクトする

これらの処理を実装するために、以下のメソッドを作成します。
Loginコントローラー(Login.php)

  • login_twitter メソッド
    認可コードを取得し、リダイレクトするまでの流れをコントロールするためのメソッドです。

Twitter_libraryライブラリ(Twitter_library.php)

  • getAuthorizationUrlメソッド
    認可コードをリクエストし、取得した認可コードからリダイレクト先URLを構築するメソッドです。
  • buildSignatureメソッド
    リクエストに必要な署名を作成するメソッドです。

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

「Twitterでログイン」ボタンを押すと実行されるメソッドです。
Login.php

/**
 * Twitterによるログインを開始する
 */
public function login_twitter()
{
    // 認証ページのURLを取得
    $response = $this->twitter_library->getAuthorizationUrl();
    
    // 戻り値があった場合はエラー
    if (is_array($response)) {
        $message = '';
        foreach ($response as $_response) {
            if (is_array($_response)) {
                foreach ($_response as $key => $msg) {
                    $message .= $key . ': ' . $msg . '<br>';
                }
            } else {
                $message .= $_response . '<br>';
            }
        }
        $_SESSION['message'] = $message;
    } else {
        $_SESSION['message'] = $response;
    }
    
    redirect('login', 'location', 302);
}

認可コードのリクエストから、認証ページへのリダイレクトまで、ライブラリのgetAuthorizationUrlメソッドで行います。エラーが発生した場合、メソッドからの戻り値はエラーメッセージになります。

getAuthorizationUrlメソッドの作成(Twitter_libraryライブラリ)

認可コードをリクエストして、認証ページへリダイレクトする処理を記述します。
Twitter_library.php

/**
 * 認証画面のURLを取得してリダイレクトする(ライブラリ未使用)
 * @return  mixed   エラーメッセージ、またはfalse
 */
public function getAuthorizationUr()
{
    // nonceの作成
    $nonce = random_string('md5');

    // OAuthリクエストエンドポイントへ送信するパラメーター
    $params = [
        'oauth_callback' => $this->callbackUrl,
        'oauth_consumer_key' => $this->consumerKey,
        'oauth_signature_method' => 'HMAC-SHA1',
        'oauth_timestamp' => time(),
        'oauth_nonce' => $nonce,
        'oauth_version' => '1.0',
    ];

    // 署名を作成し、パラメーターに入れる
    $signature = $this->buildSignature('', $params, 'POST', $this->apiOauthRequest);
    $params['oauth_signature'] = $signature;

    // パラメーターをヘッダー用に変換
    $post = '';
    foreach ($params as $name => $value) {
        $post .= rawurlencode($name) . '="' . rawurlencode($value) . '", ';
    }
    $post = rtrim($post, ', ');

    // ヘッダーの作成
    $options = [
        CURLOPT_HTTPHEADER => [
            'Accept: */*',
            'Content-Type: application/x-www-form-urlencoded',
            'Host: api.twitter.com',
            'Authorization: OAuth ' . $post,
        ],
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 5,
    ];
    return $options;

    // 認証トークンのリクエスト
    $response = $this->ci->curl_library->execPostCurl($this->apiOauthRequest, null, $options);
    if (empty($response['response'])) {
        return false;
    }

    // エラーが入っていたらエラーを返す
    if (isset($response['response']['errors'])) {
        return $response['response']['errors'];
    }

    // レスポンスから認証トークンを取得する
    $requestToken = [];
    parse_str($response, $requestToken);

    $auth = [
        'oauth_token' => $requestToken['oauth_token'],
        'oauth_token_secret' => $requestToken['oauth_token_secret'],
    ];

    $_SESSION['auth'] = $auth;

    // 認証画面のURLを作成
    $url = $this->apiAuthenticate . '?oauth_token=' . $auth['oauth_token'];

    // 認証画面へリダイレクト
    redirect($url, 'location', 302);
}

このメソッドでの処理が、Twitterログインのスタートになります。
送信するパラメーターを元に署名を作成し、その署名もパラメーターに入れます。

パラメーターは所定の書式でヘッダーに入れ、request_tokenエンドポイントへ認可コード(認証トークン)を POST でリクエストします。本来ならここで認可コードが返ってくるのですが、どうやってもエラーが返ってきてしまいます。
正常なレスポンスの場合、認可コードを取得できます。認可コードは OAuthトークンと、OAuthシークレットの2つの文字列になっており、これをペアで使用します。後でユーザー属性のリクエストなどに使用するため、セッションに保管しておきます。
ユーザーがTwitterにログインできるよう、OAuthトークンをパラメーターに入れ、authenticateエンドポイントへリダイレクトします。

buildSignatureメソッドの作成(Twitter_libraryライブラリ)

Twitterへ何かをリクエストする場合、送信するパラメーターを素に署名を作成しなければなりません。署名の作成手順は難しいものではありませんが、間違えると認証が通りません。
署名の作成手順は、次のようになります。

  1. キーを作成する
    ハッシュ化するためのキーを作成します。ConsumerシークレットをURLエンコードした文字列と、OAuthシークレットをURLエンコードした文字列を & (アンパサンド)でつなぎます。これがキーになります。
    なお、最初の認可コードをリクエストする際は、まだOAuthシークレットを取得していません。そのため、OAuthシークレットは空文字列として扱います。
  2. パラメーターから oauth_callback を取り除く
    パラメーターの oauth_callback は、ハッシュ化するデータに含めないので、取り除きます。
  3. パラメーターを並べ替える
    パラメーターをアルファベット順に並べ替えます。
  4. パラメーターの値をつなげる
    パラメーターの値を、すべて & でつないで、1つの文字列にします。
  5. つないだ文字列をエンコードする
    値をつないだ文字列を、URLエンコードします。
  6. 送信メソッドをエンコードする
    POST、または GET をURLエンコードします。なお、POST、GET は大文字にしておく必要があります。
  7. リクエスト先エンドポイントをエンコードする
    エンドポイントのURLを、URLエンコードします。
  8. 文字列をつなげる
    URLエンコードしたメソッド、エンドポイント、パラメーターを、この順番で & でつなぎ、1つの文字列にします。
  9. ハッシュを作成する
    hash_hmac関数を使用して、ハッシュを作成します。アルゴリズムは sha1 で、最初に作成したキーを使用して、先ほど作成した文字列のハッシュを作成します。バイナリ形式で作成してください。
  10. BASE64エンコードする
    作成したハッシュをBASE64でエンコードします。これが署名になります。

URLエンコードは空白が %20 になるようにします。つまり、rowurlencode関数を使用します。

Twitter_library.php

/**
 * 署名を作成する
 * @param   string  $secret OAuthシークレット文字列
 * @param   array   $params 送信するパラメータ
 * @param   string  $method リクエストメソッド(POST or GET)
 * @param   string  $url    リクエストURL
 * @return  string  署名
 */
private function buildSignature($secret, $params, $method, $url)
{
    // キーの作成
    $key = rawurlencode($this->consumerSecret) . '&' . rawurlencode($secret);

    // データの作成
    $data = [];
    foreach ($params as $name => $value) {
        if ($name === 'oauth_callback') {
            continue;
        }

        $data[$name] = $value;
    }

    ksort($data);
    $request = implode('&', $data);
    $encReq = rawurlencode($request);
    $encMethod = rawurlencode($method);
    $encUrl = rawurlencode($url);
    $source = $encMethod . '&' . $encUrl . '&' . $encReq;

    // 署名作成
    $hash = hash_hmac('sha1', $source, $key, true);
    return base64_encode($hash);
}

コールバックの処理

ユーザーがTwitterにログインした後、Twitterから処理が戻ってきた(コールバックしてきた)ときの処理を作成します。
このコード例では Login コントローラーの twitter メソッドにコールバックするので、twitter メソッドを作成します。

そのほかには以下のメソッドを作成します。
connect メソッド(Twitter_libraryライブラリ)
アクセストークンをリクエストするメソッドです。

getUserInfo メソッド(Twitter_libraryライブラリ)
ユーザー属性をリクエストするメソッドです。

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

コールバックを処理する twitter メソッドを、Loginコントローラーに作成します。
twitterメソッド内の大まかな流れは、

  • コールバックのパラメーターから OAuth_Verifier を取得
  • コールバックされたOAuthトークンと、セッションに保管していたOAuthトークンの一致を確認
  • アクセストークンをリクエスト
  • ユーザー属性のリクエスト

となります。

Login.php

/**
 * twitterログインからのコールバックを処理する
 */
public function twitter()
{
    // セッションに保管したリクエストトークンを読み取る
    $requestToken = $_SESSION['auth'];
    unset($_SESSION['auth']);

    if (is_null($requestToken)) {
        redirect('login', 'location', 302);
    }

    // コールバックのパラメーターを取得
    $oauthToken = $this->input->get_post('oauth_token');
    $oauthVerifier = $this->input->get_post('oauth_verifier');

    // コールバックのOAuthトークンと、保管していたOAuthトークンが一致するか確認
    if ($oauthToken !== $requestToken['oauth_token']) {
        $_SESSION['message'] = '認証エラーが発生しました。';
        redirect('login', 'location', 302);
    }

    // アクセストークンのリクエスト
    $token = $this->twitter_library->connect($requestToken, $oauthVerifier);
    if ($token === false) {
        $_SESSION['message'] = 'アクセストークンを取得できませんでした。';
        redirect('login', 'location', 302);
    }

    // ユーザー情報のリクエスト
    $user = $this->twitter_library->getUserInfo($token);

    // ログイン完了、結果を表示する
    $viewdata = [
        'message' => 'Twitterでログインしました。',
        'data' => [
            'token' => $token,
            'user' => $user,
        ],
    ];

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

connectメソッドの作成(Twitter_libraryライブラリ)

connectメソッドではアクセストークンをリクエストします。
手順は認可コードのリクエストとほとんど同じです。異なるのはリクエスト先エンドポイント、送信するパラメーターにOAuth_VerifierとOAuthトークンが含まれること、署名作成に必要なOAuthシークレットを取得済みであること、それからレスポンスがアクセストークンであることくらいです。
Twitter_library.php

/**
 * アクセストークンのリクエスト(ライブラリ未使用)
 * @param   array   $token          リクエストトークンの配列
 * @param   string  $oauthVerifier  認証確認識別子
 * @return  mixed   成功時:アクセストークンの配列 失敗時:false
 */
public function connect($token, $oauthVerifier)
{
    // nonceを作成
    $nonce = random_string('md5');

    // 送信するパラメーター
    $params = [
        'oauth_consumer_key' => $this->consumerKey,
        'oauth_nonce' => $nonce,
        'oauth_signature_method' => 'HMAC-SHA1',
        'oauth_timestamp' => time(),
        'oauth_token' => $token['oauth_token'],
        'oauth_version' => '1.0',
        'oauth_verifier' => $oauthVerifier,
    ];

    // 署名の作成
    $signature = $this->buildSignature($token['oauth_token_secret'], $params, 'POST', $this->apiAccessToken);
    $params['oauth_signature'] = $signature;

    // パラメーターをヘッダー用に変換
    $post = '';
    foreach ($params as $name => $value) {
        $post .= $name . '="' . urlencode($value) . '", ';
    }
    $post = rtrim($post, ', ');

    // ヘッダーの作成
    $options = [
        CURLOPT_HTTPHEADER => [
            'Accept: */*',
            'Content-Type: application/x-www-form-urlencoded',
            'Host: api.twitter.com',
            'Authorization: OAuth ' . $post,
        ],
    ];

    // アクセストークンのリクエスト
    $response = $this->ci->curl_library->execPostCurl($this->apiAccessToken, null, $options);
    if (empty($response['response'])) {
        return false;
    }

    // エラーが入っていたらエラーを返す
    if (isset($response['response']['errors'])) {
        return $response['response']['errors'];
    }

    // レスポンスからアクセストークンを取得する
    $accessToken = [];
    parse_str($response, $accessToken);

    return $accessToken;
}

getUserInfoメソッドの作成(Twitter_libraryライブラリ)

ユーザー属性をリクエストするメソッドです。これも、先の connect メソッドとほとんど処理が変わりません。ただし、ユーザー属性のリクエストは GET で行います。
Twitter_library.php

/**
 * ユーザー属性を取得する(ライブラリ未使用)
 * @param   array   $token  アクセストークンの配列
 * @return  object  ユーザー情報
 */
public function getUserInfo($token)
{
    // nonceの作成
    $nonce = random_string('md5');

    // OAuthリクエストエンドポイントへ送信するパラメーター
    $params = [
        'oauth_consumer_key' => $this->consumerKey,
        'oauth_token' => $token['oauth_token'],
        'oauth_signature_method' => 'HMAC-SHA1',
        'oauth_timestamp' => time(),
        'oauth_nonce' => $nonce,
        'oauth_version' => '1.0',
    ];

    // 署名を作成し、パラメーターに入れる
    $signature = $this->buildSignature($token['oauth_token_secret'], $params, 'GET', $this->apiVerifyCredentials);
    $params['oauth_signature'] = $signature;

    // パラメーターをヘッダー用に変換
    $post = '';
    foreach ($params as $name => $value) {
        $post .= $name . '="' . urlencode($value) . '", ';
    }
    $post = rtrim($post, ', ');

    // ヘッダーの作成と、オプションの調整
    $options = [
        CURLOPT_HTTPHEADER => [
            'Accept: */*',
            'Content-Type: application/x-www-form-urlencoded',
            'Host: api.twitter.com',
            'Authorization: OAuth ' . $post,
        ],
        CURLOPT_POST => false,
        CURLOPT_CUSTOMREQUEST => 'GET',
    ];

    // ユーザー情報のリクエスト
    // ヘッダー情報を入れるためPOST用のメソッドを無理矢理GETで使用する
    $response = $this->ci->curl_library->execPostCurl($this->apiVerifyCredentials, null, $options);
    if (empty($response['response'])) {
        return false;
    }

    // エラーが入っていたらエラーを返す
    if (isset($response['response']['errors'])) {
        return $response['response']['errors'];
    }

    return $response['response'];
}

以上で完成となります。

コメントを残す

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

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