明滅するプログラマの思索

WEBエンジニアとして勤務している一介の男が、日々気づいたことをまとめるブログです

PHP で Yahoo!ID連携v2 の認証を行う

Yahoo!ID連携は現在 v2 と呼ばれるバージョンで OpenId 認証が可能です。
それまで使用されていた v1 は今年の3月以降、新規の登録が不可能となりました。
Yahoo!ID連携v1 は、PHPSDKが用意されており、実装が楽になっていますが、v2 では Javascript SDK があるのみで、その他の言語の SDK は用意されていません。
ただ、OpenId Connect 準拠となっていることから、サードパーティの認証ロジックをほぼ変更することなく実装が可能となっています。
当記事では Yahoo!ID連携v2 の認証を PHP で実装し、クライアントのメールアドレスを取得するまでの流れをご紹介します。

導入環境

ソフトウェア バージョン
PHP 5.6

PEAR のライブラリ Net_URL および HTTP_Request2 を利用します。

フローについて

Yahoo!ID連携を行うためには、Yahoo!デベロッパーにてアプリケーションの登録およびリダイレクトURLの設定が必要となります。
当記事ではアプリケーション登録は済んでおり、クライアントIDおよびシークレットの発行が完了しているものとします。
また、サーバサイドのPHPで実装すべき部分のみを紹介します。

Yahoo!ID連携におけるサーバサイドの認証フローは

Yahoo! ID連携:Authorization Codeフロー - Yahoo!デベロッパーネットワーク

に詳しく記載があります。
流れとしては、

  1. ユーザ認証リクエスト(Authorizationエンドポイントへのリダイレクト)
  2. コールバックURLへのリクエストを受け付け、アクセストークンおよび認証情報を取得するためエンドポイントへリクエス

となります。
フロー内で推奨されているCSRF対策としてのstate検証と、IDToken検証については触れません。

コード

一連のフロー内で使用する定数・URLを定義します。

yahoo_constant.php
<?php
/*
 * Yahoo! ID連携のリクエストエンドポイント
 */
define('AUTHORIZATION_ENDPOINT', 'https://auth.login.yahoo.co.jp/yconnect/v2/authorization');
define('TOKEN_ENDPOINT', 'https://auth.login.yahoo.co.jp/yconnect/v2/token');

/*
 * Yahoo! ID連携のクライアントIDおよびシークレット
 */
define('CLIENT_ID', 'xxxxxxxxxxxxxxxxxxxx');
define('CLIENT_SECRET', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');

/*
 * アプリ側のコールバックURL
 * ここでトークンの取得・ユーザ情報の取得を行います
 */
define('CALLBACK_URL', 'https://lab.loose-leaf.com/callback');
/*
 * アプリ側のリダイレクトURL
 * すべてが完了した後の遷移先
 */
define('REDIRECT_URL', 'https://lab.loose-leaf.com/complete');

ユーザ認証リクエストを行う部分のコードは以下のようになります。

yahoo_oauth.php
<?php
/*
 * 定数読み込み
 */
require_once('yahoo_constant.php');
/*
 * まずは Authorization.
 * リクエストパラメータを作る
 */
$request_parameters = [
    'response_type' => 'code',
    'client_id' => \CLIENT_ID,
    'redirect_uri' => \CALLBACK_URL,
    'scope' => 'openid email'
];
$query = [];
foreach ($request_parameters as $key => $value) {
    $query[] = $key . "=" . urlencode($value);
}
/*
 * Yahooの Authorization ページへリダイレクト
 */
header(sprintf("Location:%s?%s", \AUTHORIZATION_ENDPOINT, implode("&", $query)));
exit;

Authorizationエンドポイントへのリクエストに必須なパラメータを設定し、GETでリダイレクトしています。
scope には openid, profile, email, address が設定でき、ここで設定した情報を取得することができます。
クライアントは Yahoo! の認証画面に遷移し、そこで許可を行うと、リダイレクトURIへと遷移します。
リダイレクトURIへのリクエストを受け付ける部分のコードは以下のようになります。

yahoo_callback.php
<?php
/*
 * 定数読み込み
 */
require_once('yahoo_constant.php');

/*
 * アクセストークンの取得
 * ここでは HTTP_Request2 を使用してみます
 */
require_once('Net/URL2.php');
require_once('HTTP/Request2.php');

$code = $_REQUEST['code'];

$http_options = [
    'protocol_version' => '1.1',
    'connect_timeout' => '300',
    'timeout' => '300',
    'follow_redirects' => true,
    'max_redirects' => 3,
    'ssl_verify_peer' => false
];
$req = new \HTTP_Request2(\TOKEN_ENDPOINT, \HTTP_Request2::METHOD_POST, $http_options);
/*
 * ヘッダをセット
 */
$req->setHeader('Connection', 'keep-alive');
$req->setHeader('Content-Type', 'application/x-www-form-urlencoded');
$req->setHeader('Accept-Charset', 'UTF-8');

/*
 * パラメータをセット
 */
$request_parameter = [
    'code' => $_REQUEST['code'],
    'client_id' => \CLIENT_ID,
    'client_secret' => \CLIENT_SECRET,
    'redirect_uri' => \REDIRECT_URL,
    'grant_type' => 'authorization_code'
];
foreach ($request_parameter as $key => $value) {
    $req->addPostParameter($key, $value);
}

// リクエスト
try {
    $res = $req->send();
    if ($res->getStatus() != 200) {
        error_log('doHttpRequest(): Request status is not 200: '.$res->getStatus());
        return false;
    }
    // get response
    $responses = json_decode($res->getBody());
    /*
    * sub / email を取得する
    */
    list($client_id_signature, $jwt_encoded) = explode(".", $responses->id_token);
    $jwt = json_decode(base64_decode($jwt_encoded));
    $sub = $jwt->sub;
    $email = $jwt->email;
    ...
} catch (\HTTP_Request2_Exception $e) {
    error_log('doHttpRequest(): HTTP_Request2_Exception: '.$this->getMessageByHttpRequest2ExceptionCode($e->getCode()));
    exit;
}

リクエストパラメータに含まれる code が認可コードとなっており、これによってアクセストークンの発行が可能となります。
また、レスポンスに含まれる id_token にはユーザ認証リクエストの scope に指定した情報が含まれており、取得が可能です。

まとめ

上記のコードは基本的な部分がすべて OpenId Connect に準拠しているため、ほかの OpenId Connect IdP でもほぼ変更することなく転用が可能です。