LoginSignup
9
8

More than 5 years have passed since last update.

CakePHP3でTwitterOAuthをComponentとして実装してみる

Last updated at Posted at 2018-03-26

概要

PHPもCakePHPもTwitter OAuth認証も初めてな人間がTwitter OAuth認証を実装した話

開発環境

CakePHP 3.5.13
PHP 5.6.30

Twitter認証にはAbraham\TwitterOAuthを使う。
参考:【CakePHP3】Twitter API ライブラリTwitterOAuthをComposerでインストール – atomicbox

準備

APIキーの取得は済ませておく。
Twitter Application Management
取得方法については調べればいくらでも出てくる。

Twitter OAuth認証の流れ

図付きの説明が公式にある。(英語)
Implementing Sign in with Twitter — Twitter Developers

https://twitteroauth.com/ にライブラリを使ったPHPの動作デモがあるので分かりやすい。

  1. Twitterにrequest_token, request_token_secretを発行してもらう
    POST oauth/request_token — Twitter Developers
    4で検証に使うので、セッションに保存しておく。
  2. Twitterに認証URLを発行してもらう
    GET oauth/authorize — Twitter Developers
    GET oauth/authenticate — Twitter Developers
    authorizeのほうだと毎回認証確認され、authenticateのほうだと認証済みの場合はすぐにコールバックされる。
  3. 2で発行したURLにリダイレクトする
  4. 認証完了後、コールバックで指定したURLに返ってくるのでよしなにする
    • 1で発行したトークンとコールバックで帰ってきたトークンが同一かどうかチェックする
    • 認証拒否時のリダイレクト処理など
  5. request_token, request_token_secret, oauth_verifierを用い、Twitterにaccess_token, access_token_secretを発行してもらう
    POST oauth/access_token — Twitter Developers
    これが投稿などを行う時に必要なキーとなる。セッションに保存しておく
    この段階でOAuth認証が完了した扱いになる。(=ユーザーの連携アプリ一覧に表示される)
  6. 以降、5で取得したトークンを用いてTwitter APIを叩く

Component実装

やることは分かったのでコンポーネントとして実装する。

  • コントローラー側ではTwitter API、セッション管理を意識しないように
    • getAuthenticateUrlを呼びリダイレクト
    • コールバック側でvalidateCallbackを呼び出す
    • その後postTweetを呼び出す
  • tokenなどはセッションに保存
    • Twitter.oauth_token
    • Twitter.access_token
    • Twitter.user_id
  • 異常系はとりあえずInternalErrorExceptionをthrowする
    • とりあえずtokenは削除する
  • とりあえずツイートの投稿のみ実装
TwitterComponent.php
<?php
namespace App\Controller\Component;

use Cake\Controller\Component;
use Abraham\TwitterOAuth\TwitterOAuth;

use Cake\Network\Exception\InternalErrorException;

class TwitterComponent extends Component {
    // アプリケーションのConsumer Key (API Key)
    const TWITTER_CK = '<Consumer Key (API Key)>';
    // アプリケーションのConsumer Secret (API Secret)
    const TWITTER_CS = '<Consumer Secret (API Secret)>';
    // アプリケーションのCallback URL
    const CALLBACK_URL = '<Callback URL>';

    public function initialize(array $config) {
        $this->controller = $this->_registry->getController();
        $this->session = $this->controller->request->session();
    }

    /**
     * 現在のセッションでTwitter認証が済んでいるかを判定する
     *
     * @return bool
     */
    public function isAuthorized()
    {
        return $this->getAccessToken() !== null;
    }

    /**
     * ユーザー認証URLを取得する
     *
     * @return string
     */
    public function getAuthenticateUrl()
    {
        $connection = new TwitterOAuth(self::TWITTER_CK, self::TWITTER_CS);
        $request_token = $connection->oauth('oauth/request_token', array('oauth_callback' => self::CALLBACK_URL));
        $authenticate_url = $connection->url('oauth/authenticate', array('oauth_token' => $request_token['oauth_token']));

        if (!isset($request_token) || !isset($authenticate_url))
        {
            $this->clearSessionData();
            throw new InternalErrorException('Twitter認証に失敗しました');
        }

        $this->setRequestToken($request_token["oauth_token"], $request_token["oauth_token_secret"]);

        return $authenticate_url;
    }

    /**
     * コールバックで初期化を行う
     */
    public function initializeOnCallback()
    {
        $query = $this->controller->request->query;

        if(isset($query["denied"]))
        {
            $this->clearSessionData();
            throw new InternalErrorException('認証がキャンセルされました');
        }

        // Twitterから返却されたOAuthトークンとセッションに保存されたOAuthトークンを比較
        $return_oauth_token = (isset($query['oauth_token'])) ? $query['oauth_token'] : null;

        $request_token = $this->getRequestToken();
        if (!isset($request_token) || $return_oauth_token != $request_token['token'])
        {
            // セッション削除
            $this->clearSessionData();
            throw new InternalErrorException('OAuthトークンが無効です');
        }

        $this->createAccessToken($query['oauth_verifier']);
    }

    /**
     * ツイートする
     *
     * @param string $message ツイート内容
     */
    public function postTweet($message)
    {
        $connection = $this->createConnection();
        $statues = $connection->post("statuses/update", ["status" => $message]);

        if ($connection->getLastHttpCode() != 200)
        {
            $this->clearSessionData();
            throw new InternalErrorException('ツイートに失敗しました');
        }
    }

    /**
     * アクセストークンの取得を行う
     */
    private function createAccessToken($oauth_verifier)
    {
        $request_token = $this->getRequestToken();

        if (!isset($request_token) || !isset($oauth_verifier))
        {
            $this->clearSessionData();
            throw new InternalErrorException('OAuth認証情報が存在しません');
        }

        $connection = new TwitterOAuth(self::TWITTER_CK, self::TWITTER_CS, $request_token["token"], $request_token["token_secret"]);
        $access_token = $connection->oauth("oauth/access_token", ["oauth_verifier" => $oauth_verifier]);

        if ($connection->getLastHttpCode() != 200)
        {
            $this->clearSessionData();
            throw new InternalErrorException('アクセストークンの取得に失敗しました');
        }

        $this->setAccessToken($access_token["oauth_token"], $access_token["oauth_token_secret"]);
        $this->setUserId($access_token["user_id"]);

        return $this->getAccessToken();
    }

    /**
     * TwitterAPIに接続するためのconnectionを取得する
     *
     * return TwitterOAuth
     */
    private function createConnection()
    {
        $access_token = $this->getAccessToken();

        return new TwitterOAuth(self::TWITTER_CK, self::TWITTER_CS, $access_token["token"], $access_token["token_secret"]);
    }

    /**
     *  セッションにOAuth認証のトークンを保存する
     */
    private function setRequestToken($oauth_token, $oauth_token_secret)
    {
        $this->session->write('Twitter.oauth_token', array("token" => $oauth_token, "token_secret" => $oauth_token_secret));
    }

    /**
     * セッション情報からOAuth認証のトークンを取得する
     *
     * @return null | array["token", "token_secret", ]
     */
    private function getRequestToken()
    {
        return $this->session->read('Twitter.oauth_token');
    }

    /**
     * セッションにTwitterのアクセストークンを保存する
     */
    private function setAccessToken($oauth_token, $oauth_token_secret)
    {
        $this->session->write('Twitter.access_token', array("token" => $oauth_token, "token_secret" => $oauth_token_secret));
    }

    /**
     * セッション情報からTwitterのアクセストークンを取得する
     *
     * @return null | array["token", "token_secret"]
     */
    private function getAccessToken()
    {
        return $this->session->read('Twitter.access_token');
    }

    /**
     * セッションにTwitterのuser_idを保存する
     */
    private function setUserId($user_id)
    {
        $this->session->write('Twitter.user_id', $user_id);
    }

    /**
     * セッション情報からTwitterのuser_idを取得する
     *
     * @return string
     */
    public function getUserId()
    {
        return $this->session->read('Twitter.user_id');
    }

    /**
     * セッションに保存されているOAuth認証情報などをクリアする
     */
    private function clearSessionData()
    {
        $this->session->delete('Twitter.oauth_token');
        $this->session->delete('Twitter.access_token');
        $this->session->delete('Twitter.user_id');
    }
}

アクセストークンの有効期限について

Oauth FAQ — Twitter Developers

How long does an access token last?
Access tokens are not explicitly expired. An access token will be invalidated if a user explicitly revokes an application in the their Twitter account settings, or if Twitter suspends an application. If an application is suspended, there will be a note on the apps.twitter.com page stating that it has been suspended.

What if an access token becomes invalid?
Assume a user’s access token may become invalid at any time. If this happens, prompt the user to re-authorize the application. Ensuring that this situation is handled gracefully is important for a good user experience.

一度取得したアクセストークンは特に有効期限などは設定されておらず、ユーザーが連携を解除するまでは使える模様。
しかし、ユーザーが連携を解除することでアクセストークンが無効になっている可能性が常に存在するため、エラーハンドリングはよしなにしてくれとの事。

Response Codes — Twitter Developers

認証失敗の場合はHTTP Status Code == 401(Unauthorized)が返ってくるはずなので、こちら側でいい感じに制御する。

まとめ

PHP触ったこと無いけど一通り実装してみた。思ったより簡単だった。普段Railsを触っているのでCakePHPはすんなり理解できた。
PHPのお作法的な所はよく分からないので、微妙な実装をしている所があるかもしれない。
ざざっと書いたので色々と雑。

参考

https://qiita.com/michimani/items/a74f8debeace2df63eec
https://qiita.com/frost_star/items/f652a798a211d606d60d

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8