タグ: OAuth2

Node.js + JWT (JSON Web Token) を使用して Analytics API から月間ユニークユーザー数を取得する

とあるサイトの月間ユニークユーザー数を定期的に取得する必要があったため、NodejsでAnalytics APIを利用することにしました。OAuth2での認可にはサービスアカウントを使い、APIリクエストにはJWTを使用します。

サービスアカウントとアクセスキーの作成

Google Developers Consoleでサービスアカウントの作成とアクセスキーを取得する。

プロジェクトの作成

「プロジェクトを作成」ボタンを押下し現れたダイアログに、任意のプロジェクト名を入力。他はそのままで、「作成」ボタン押下する。

01.create_project

Analytics APIの有効化

プロジェクトページに移動するので「APIと認証」の「API」をクリック。

02.select_api

左側に表示されるテキストボックス(100件以上のすべてのAPIを検索)に「analytics」と入力すると、その下のリストがフィルタリングされて、「Analytics API」が現れるので、それをクリック。

03.search_api

「APIを有効にする」をクリック。

04.enable_api

サービスアカウントの作成

左メニューの「APIと認証」の「認証情報」をクリック。

05.select_auth

「OAuth」の「新しいクライアントIDを作成」をクリックするとダイアログが表示されるので、「サービスアカウント」を選択し、「クライアントIDを作成」ボタンを押下する。

06.create_servie_account

キーの生成が完了すると、JSONキーのダウンロード画面が現れれますが、今回は不要なので、保存してもしなくてもOK。

アクセスキーのダウンロード

「新しいクライアントIDを作成」の右側に先ほど生成したサービスアカウントの情報があるので「メールアドレス」(※1)をメモしておく。

07.created_service_account

次にその下にある「新しいP12キーを作成」ボタンを押下する。

生成されたキーのダウンロード画面が表示されるので、保存する。(※2)
また同時に表示されているダイアログに「秘密キーのパスワードが下に表示されます。後でもう一度表示することはできません。」の下にキーのパスワード(※3)が表示されているので、メモしておく。

08.create_p12key

Google Analyticsの閲覧権限の付与と対象情報の取得

Google Analyticsで、月間ユニークユーザー数を取得するanalyticsのビューIDと、それに対する閲覧権限をサービスアカウントに付与する。

ビューIDの取得

Google Analyticsにアクセスし、対象のサイトを選択する。

上部メニューの「アナリティクス設定」を選択する。

09.analytics_view

「ビュー」列の「ビュー設定」を選択し、「基本設定」の「ビューID」(※4)をメモしておく。

10.analytics_viewid

閲覧権限をサービスアカウントに付与する

アナリティクス設定を選択し、「ビュー」列の「ユーザー管理」を選択。

09.analytics_view

「権限を付与するユーザー」テキストボックスにサービスアカウントのメールアドレス(※1)を入力、権限を「表示と分析」にして「追加」ボタンを押下する。

12.analytics_input_email

上部のユーザーリストに追加されているのを確認する。

13.analytics_userlist_refresh


以降の作業はCentOS上で行っています。

下準備

JavaScriptの記述をCoffeeScriptで行っているため、インストールする。

$ npm install -g coffee-script

Nodejs用のGoogle APIモジュールのインストールする。

$ npm install googleapis

日時計算にMoment.jsを用いるのでインストールする。

$ npm install moment

アクセスキーのフォーマット変換

先ほどダウンロードしたキー(※2)をgoogleapi-privatekey.p12とリネームしてUPする。

opensslコマンドを使用して、PKCS12形式からPEM形式に変更する。途中、パスワードの入力を促されるので、ダウンロード時に表示されていたパスワード(※3)を入力する。

$ openssl pkcs12 -in googleapi-privatekey.p12  -out googleapi-privatekey.pem -nocerts -nodes
Enter Import Password:
MAC verified OK

プログラム

2015年4月の月間ユニークユーザー数を取得する。

はサービスアカウントのメールアドレス(※1)
googleapi-privatekey.pemはPEM形式のアクセスキーファイル。(googleapi-privatekey.pemとanalytics.coffeeは同一階層に配置)
はGoogleAnalyticsのビューID(※4)

実行

$ coffee analytics.coffee
Visitors: 12345

メモ

閲覧権限付与後すぐにプログラムを実行した場合、

 reason: 'insufficientPermissions',
 message: 'User does not have any Google Analytics account.' 

と表示されエラーとなりますが、反映に時間がかかるので、しばらく待てば正常に取得できます。

参考

CodeIgniterにOAuth2のサービスプロバイダーとクライアントを実装する。

CodeIgniterにOAuth2のサービスプロバイダーとクライアントを実装する。

コードはこちらで公開しています。(データベースの設定、oauth2-server-phpのインストールと修正が必要)

凡例

ドメイン
example.jp
ドキュメントルートパス
$HTDOC_DIR
サービスプロバイダー用ライブラリ
bshaffer/oauth2-server-php · GitHub
クライアント用ライブラリ(CodeIgniter Sparks)
philsturgeon/codeigniter-oauth2 · GitHub
サービスプロバイダー側
http://example.jp/oauth2/
http://example.jp/api/
クライアント側
http://example.jp/client/
  • OAuthの情報を保存するためにMySQLを使用する
  • サービスプロバイダー側はデモなのでHTTPでアクセスする。(本来はHTTPSを使用するべき)

前準備

  1. ドキュメントルート下にCodeIgniterをインストール済み(http://example.jp/でWelcomeページが表示)
  2. .htacess設置を設置してindex.phpをURLから除去する
    CodeIgniter の URL : CodeIgniter ユーザガイド 日本語版
  3. application/config/config.phpで以下を変更
    // index.phpを取り除く
    $config['index_page'] = '';
    
    //デフォルトのセッションを使う
    $config['encryption_key'] = '適当な値';
  4. application/config/autoload.phpで以下を変更
    $autoload['libraries'] = array('session');
  5. サービスプロバイダライブラリ(oauth2-server-php )インストール
    cd $HTDOC_DIR
    cd application/third_party
    git clone https://github.com/bshaffer/oauth2-server-php.git
  6. CodeIgniter Sparksインストール
    cd $HTDOC_DIR
    php -r "$(curl -fsSL http://getsparks.org/go-sparks)"
  7. クライアントライブラリ(codeigniter-oauth2 )インストール
    php tools/spark install oauth2
  8. DB設定
    データベースを作成し、ユーザ権限を与え、application/config/database.phpを設定しておく。
  9. テーブル作成&初期データ追加
    -- テーブル作成(oauth2-server-phpのテストを流用)
    CREATE TABLE oauth_clients (client_id TEXT, client_secret TEXT, redirect_uri TEXT);
    CREATE TABLE oauth_access_tokens (access_token TEXT, client_id TEXT, user_id TEXT, expires DATETIME, scope TEXT);
    CREATE TABLE oauth_authorization_codes (authorization_code TEXT, client_id TEXT, user_id TEXT, redirect_uri TEXT, expires DATETIME, scope TEXT);
    
    -- クライアントアプリケーションを登録
    INSERT INTO oauth_clients (client_id, client_secret) VALUES ("demoapp", "demopass");

作成したファイル

サービスプロバイダ側OAuth2コントローラー

application/controllers/oauth2.php

<?php
require_once APPPATH . 'third_party/oauth2-server-php/src/OAuth2/Autoloader.php';

class Oauth2 extends CI_Controller
{
    private $_server;
    private $_request;

    public function __construct()
    {
        parent::__construct();

        OAuth2_Autoloader::register();

        include(APPPATH.'config/database'.EXT);
        $db_config = $db['default'];
        $connection = array(
            'dsn' => sprintf(
                '%s:dbname=%s;host=%s',
                $db_config['dbdriver'],
                $db_config['database'],
                $db_config['hostname']
            ),
            'username' => $db_config['username'],
            'password' => $db_config['password']
        );
        $storage = new OAuth2_Storage_Pdo($connection);
        $this->_server = new OAuth2_Server($storage);
        $this->_server->addGrantType(new OAuth2_GrantType_UserCredentials($storage));
        $this->_request = OAuth2_Request::createFromGlobals();
    }

    public function authorize()
    {
        switch ($this->input->server('REQUEST_METHOD')) {
        case 'GET':
            // リクエストチェック
            $params = $this->_server->validateAuthorizeRequest($this->_request);

            if (!$params) {
                $this->_server->getResponse();
            } else {
                $this->load->view('oauth2', $params);
            }
            break;
        case 'POST':
            /*
             * ここで認証処理
             */
            // 認証処理の結果、ユーザIDが決定
            $userid = 1;

            // 認可応答
            $authorized = (bool) $this->input->post('authorize');
            $response = $this->_server->handleAuthorizeRequest($this->_request, $authorized, $userid);
            $location = $response->getHttpHeader('Location');
            header("Location: $location");
            break;
        }
    }

    public function access_token()
    {
        $this->_server->handleGrantRequest($this->_request)->send();
    }
}

サービスプロバイダ側APIコントローラー

application/controllers/api.php

<?php
require_once APPPATH . 'third_party/oauth2-server-php/src/OAuth2/Autoloader.php';

class Api extends CI_Controller
{
    private $_server;
    private $_request;

    public function __construct()
    {
        parent::__construct();

        OAuth2_Autoloader::register();

        include(APPPATH.'config/database'.EXT);
        $db_config = $db['default'];
        $connection = array(
            'dsn' => sprintf(
                '%s:dbname=%s;host=%s',
                $db_config['dbdriver'],
                $db_config['database'],
                $db_config['hostname']
            ),
            'username' => $db_config['username'],
            'password' => $db_config['password']
        );
        $storage = new OAuth2_Storage_Pdo($connection);
        $this->_server = new OAuth2_Server($storage);
        $this->_server->addGrantType(new OAuth2_GrantType_UserCredentials($storage));
        $this->_request = OAuth2_Request::createFromGlobals();
    }

    public function friends()
    {
        if (!$this->_server->verifyAccessRequest($this->_request)) {
            $this->_server->getResponse()->send();
        } else {
            $response = new OAuth2_Response(array('friends' => array('friend1', 'friend2', 'friend3')));
            $response->send();
        }
    }
}

クライアント側コントローラー

application/controllers/client.php

<?php

class Client extends CI_Controller
{
    private $_provider;

    public function __construct()
    {
        parent::__construct();

        $this->load->helper('url_helper');
        $this->load->spark('oauth2/0.4.0');

        $this->_provider = $this->oauth2->provider('example', array(
            'id' => 'demoapp',
            'secret' => 'demopass',
            'redirect_uri' => site_url().'client/'
        ));
    }

    public function index()
    {
        $data = array(
            'auth_url' => null,
            'access_token' => null
        );

        if (!$this->input->get('code')) {
            $data['auth_url'] = $this->_provider->authorize();
        } else {
            try {
                // アクセストークンの取得
                $token = $this->_provider->access($this->input->get('code'));

                // アクセストークン
                $access_token = $token->access_token;
                $data['access_token'] = $access_token;

                /*
                 *以後使えるように保存しておく
                 */

                // 保存していおいたアクセストークンを使ってAPIアクセス
                $token = OAuth2_Token::factory('access', array('access_token' => $access_token));
                $friends = $this->_provider->get_friends($token);
                $data['friends'] = $friends;
            } catch (OAuth2_Exception $e) {
                show_error($e->getMessage());
            } catch (Exception $e) {
                show_error($e->getMessage());
            }
        }

        $this->load->view('client', $data);
    }
}

サービスプロバイダの認可ページビュー

application/views/oauth2.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>サービスプロバイダー</title>

<!-- Bootstrap -->
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
    padding-top: 60px;
}
</style>
<link href="/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
</head>
<body>
<div>
  <div>
    <div> <a href="#">サービスプロバイダー側</a> </div>
  </div>
</div>
<div>
  <div>
    <p>この連携アプリを認証すると、次の動作が<strong>許可されます</strong>。</p>
    <ul>
      <li>XXXXXXXXXXXXXXXXXXXXXXXX</li>
      <li>YYYYYYYYYYYYYYYYYYYYYYYY</li>
    </ul>
  </div>
  <fieldset>
    <legend>Login(※デモなので入力の必要なし)</legend>
    <div>
      <label for="username">ユーザ名</label>
      <div>
        <input name="username" type="text" />
      </div>
    </div>
    <div>
      <label for="password">パスワード</label>
      <div>
        <input name="password" type="password" />
      </div>
    </div>
  </fieldset>
  <div>
    <form action="/oauth2/authorize?client_id=<?php echo $client_id ?>&redirect_uri=<?php echo $redirect_uri ?>&response_type=<?php echo $response_type ?>" method="post">
      <input type="hidden" name="authorize" value="1" />
      <button type="submit">許可する</button>
      <button type="button" onclick="document.getElementById('cancel').submit()">Cancel</button>
    </form>
    <form id="cancel" action="/oauth2/authorize?client_id=<?php echo $client_id ?>&redirect_uri=<?php echo $redirect_uri ?>&response_type=<?php echo $response_type ?>" method="post">
      <input type="hidden" name="authorize" value="0" />
    </form>
  </div>
  <div>
    <p>この連携アプリを認証しても、次の動作は<strong>許可されません</strong>。</p>
    <ul>
      <li>ZZZZZZZZZZZZZZZZZZZZZZZZ</li>
    </ul>
  </div>
</div>
<script src="http://code.jquery.com/jquery-latest.js"></script> 
<script src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

クライアントページビュー

application/views/client.php

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>クライアント</title>
<!-- Bootstrap -->
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<style>
body {
    padding-top: 60px;
}
</style>
<link href="/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" />
</head>
<body>
<div>
  <div>
    <div> <a href="/">クライアント側</a> </div>
  </div>
</div>
<div>
  <?php if ($auth_url) { ?>
  <p><a href="<?php echo $auth_url ?>">認証・認可ページへ</a></p>
  <?php } else { ?>
  <p>アクセストークン:<?php echo $access_token ?></p>
  <p>APIからのレスポンス<br />
    <?php var_dump($friends) ?>
  </p>
  <?php } ?>
</div>
<script src="http://code.jquery.com/jquery-latest.js"></script> 
<script src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

codeigniter-oauth2内で使用されるサービスプロバイダーを定義したExampleプロバイダー

sparks/oauth2/0.4.0/libraries/Provider/Example.php

<?php
/**
 * Example OAuth2 Provider
 *
 * @package    CodeIgniter/OAuth2
 * @category   Provider
 */

class OAuth2_Provider_Example extends OAuth2_Provider
{
    protected $method = 'POST';

    public function url_authorize()
    {
        return site_url() . 'oauth2/authorize';
    }

    public function url_access_token()
    {
        return site_url() . 'oauth2/access_token';
    }

    public function get_friends(OAuth2_Token_Access $token)
    {
        $url = site_url() . 'api/friends?' . http_build_query(array('access_token' => $token->access_token));
        $response = file_get_contents($url);
        $data = json_decode($response);

        return $data;
    }
}

oauth2-server-phpの一部修正

codeigniter-oauth2がアクセストークンリクエストを行う際、デフォルトはGETメソッドでレスポンスはapplication/x-www-form-urlencodedで返される前提で作られている。またPOSTメソッドの場合はJSONで返される前提になっている。

一方oauth2-server-phpのアクセストークンレスポンスはJSONで返される。

そこで、今回はアクセストークンリクエストをPOSTで行うようになっている。

しかし、そのままだとアクセストークンリクエストを受け付けるoauth2-server-php側でリクエストの際送られるパラメータcode(認可コード)が見つからないというエラーになる。

これは、application/third_party/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php内で、codeを取得するのが、URLのクエリストリングから取得するようになっているためである。
一方 OAtuhの仕様書(参考資料の1.)ではアクセストークンリクエストのパラメータはHTTPリクエストボディで与えるようになっているため、application/third_party/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.phpを編集してHTTPリクエストボディからcodeを取得するように変更する。

application/third_party/oauth2-server-php/src/OAuth2/GrantType/AuthorizationCode.php

23c23
< if (!isset($request->query['code']) || !$request->query['code']) {
---
> if (!isset($request->request['code']) || !$request->request['code']) {
33c33
< if (!$tokenData = $this->storage->getAuthorizationCode($request->query['code'])) {
---
> if (!$tokenData = $this->storage->getAuthorizationCode($request->request['code'])) {
43c43
< if (!$request->query('redirect_uri') || urldecode($request->query('redirect_uri')) != $tokenData['redirect_uri']) {
---
> if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != $tokenData['redirect_uri']) {

デモ

http://example.jp/clientにアクセス

OAuthクライアント Top

サービスプロバイダー側の認可エンドポイントに遷移し、認証・認可する

サービスプロバイダー 認証・認可ページ

クライアント側に戻り、アクセストークンを取得し、さらにそれを使ってAPIにアクセスして結果を表示する。

アクセストークン取得&APIアクセス

参考資料

  1. draft-ietf-oauth-v2-31 – The OAuth 2.0 Authorization Framework
  2. OAuth 2.0でWebサービスの利用方法はどう変わるか – @IT