myThings Advent Calendar 2016 9日目です。(やばいもうすぐ10日になってしまう)
myThings DeveropersのハンズオンではBluemix上にサーバを構築しますが、用意するサーバはPHPが動けばいいので、Microsoft AzureのWebAppsをサーバにしてみました。簡単にできると思ったら以外に大変でした・・・・
##準備
・Microsoft Azure サブスクリプション
・Yahoo! Japan ID
##目標
こちらのチュートリアルStep3をAzure WebAppsで実行できるようにします。
##手順
###WebAppsの作成
・Azureポータルから新規>Web+モバイル>Web Appを選択します。
・アプリ名とリソースグループを入力して下の「作成」ボタンをクリックします。
・Web Appのデプロイが完了したら「拡張機能」>「拡張機能の追加」>「拡張機能の選択」の中から「Composer」を選択して「OK」をクリックします。つづいて「拡張機能の追加」の「OK」をクリックするとComposerが追加されます。
・Composerを追加した後は一度サイトを再起動します。
・こちらのページの下の方にあるComposerの実行ファイル(composer.phar)をダウンロードしておきます。
・Yahoo! ID連携 SDK for PHPをインストールするためのcomposer.jsonを次の内容で作成します。
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/yahoojapan/yconnect-php-sdk"
}
],
"require": {
"yahoojapan/yconnect-php-sdk": "dev-master"
}
}
・先ほどダウンロードした「composer.phar」と「composer.json」をWebAppsにアップロードします。FTPクライアントを使うのが簡単でしょうか。アップロード先はサイトのルートディレクトリ(WWWROOT)になります。
・再びAzureポータルからWebAppsのコンソールを選択します。コンソールが起動したら「php composer.phar install」と入力して実行します。しばらくすると文字化けの状態ですがインストールが完了したメッセージが表示されます。
・myThingsDevelopersサイトで実装を選択して必要な内容をすべて入力します。
・サンプルコードを表示して次のファイルを一旦保存します。
・index.php
・callback.php
・geturl.php
・setting_callback.php
・customtrigger.php
・errorlist.php
・一部のファイルを変更します。
<?php
require("vendor/autoload.php");
use YConnect\Credential\ClientCredential;
use YConnect\YConnectClient;
// アプリケーションID, シークレット
$client_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
$client_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// コールバックURL
$redirect_uri = "http://yoursite.azurewebsites.net/callback.php";
$state="xxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
$nonce="xxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
$cred = new ClientCredential( $client_id, $client_secret );
$client = new YConnectClient( $cred );
try {
// Authorization Codeを取得
$code_result = $client->getAuthorizationCode( $state );
// Tokenエンドポイントにリクエスト
$client->requestAccessToken( $redirect_uri, $code_result );
// アクセストークン, リフレッシュトークンを取得
$access_token = $client->getAccessToken();
$refresh_token = $client->getRefreshToken();
// IDトークンを検証
$verify_result = $client->verifyIdToken( $nonce );
$id_token = $client->getIdToken();
/*
try {
$verify_result = $client->verifyIdToken( $nonce );
if ( $verify_result ) {
$id_token = $client->getIdToken();
} else {
}
} catch ( Exception $e ) {
}
*/
// TODO : ここから下はリリースまでにトークンの保存方法の修正が必要です。↓↓↓↓↓↓
// アクセストークン、リフレッシュトークンの保存
save_token($access_token, $refresh_token, $id_token->user_id, $state);
// TODO : ここから上はリリースまでにトークンの保存方法の修正が必要です。↑↑↑↑↑
} catch ( TokenException $e ) {
// 再度ログインして認可コードを発行してください
}
// TODO : ここから下はリリースまでにトークンの保存方法の修正が必要です。↓↓↓↓↓↓
/*
* アクセストークン、リフレッシュの保存
*
* アクセストークン($access_token)、リフレッシュトークン($refresh_token)は安全な場所に保管してください。
* ここでは仮実装として、mcrypt関数で暗号化してファイルに保存しています。
* 動作確認が完了したらファイルに保存したアクセストークン、リフレッシュトークンは必ず削除してください。
* アクセストークン、リフレッシュトークンを保存したファイルは/tmpに配置されます。
*
* @param string $access_token アクセストークン
* @param string $refresh_token リフレッシュトークン
* @param string $key ユーザーを一意に判定できるキー
* @param string $state ランダムな文字列
*
*/
function save_token($access_token, $refresh_token, $key, $state)
{
//echo 'AT: '.$access_token.' RT: '.$refresh_token.' key: '.$key.'ST: '.$state;
/*
$td = mcrypt_module_open('des', '', 'cbc', '');
$key = substr($key, 0, mcrypt_enc_get_key_size($td));
$iv_size = mcrypt_enc_get_iv_size($td);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
echo "iv value: " .base64_encode( $iv)."\n";
// アクセストークンの暗号化
//$size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
//$iv = substr($state, 0, $size);
//$iv=mcrypt_create_iv($size, MCRYPT_RAND);
exec("echo $iv > /tmp/access_token_iv_$key");
$_SESSION['access_token_iv_'.$key]=$iv;
$enc_token = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $access_token, MCRYPT_MODE_CBC, $iv);
echo $enc_token;
$base64_token = base64_encode( $enc_token );
// 暗号化したアクセストークンをファイルに保存(本来は外部からアクセスできない環境で保管してください)
exec("echo $base64_token > /tmp/access_token_$key");
$_SESSION['access_token_'.$key]=$base64_token;
// リフレッシュトークンの暗号化
$size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = substr($state, 0, $size);
exec("echo $iv > /tmp/refresh_token_iv_$key");
$_SESSION['refresh_token_iv_'.$key]=$iv;
$enc_token = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $refresh_token, MCRYPT_MODE_CBC, $iv) ;
$base64_token = base64_encode( $enc_token );
// 暗号化したリフレッシュトークンをファイルに保存(本来は外部からアクセスできない環境で保管してください)
exec("echo $base64_token > /tmp/refresh_token_$key");
$_SESSION['refresh_token_'.$key]=$base64_token;
*/
//echo "access_token : ".$access_token."\n";
session_start();
$enc_access_token=openssl_encrypt($access_token,'AES-128-CBC',$key);
$_SESSION['access_token_'.$key]=$enc_access_token;
$enc_refresh_token=openssl_encrypt($refresh_token,'AES-128-CBC',$key);
$_SESSION['refresh_token_'.$key]=$enc_refresh_token;
//echo "access : ".$enc_access_token;
//echo "refresh : ".$enc_refresh_token;
}
// TODO : ここから上はリリースまでにトークンの保存方法の修正が必要です。↑↑↑↑↑
?>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div class="wrap">
<!-- 「Step.2 ユーザー設定画面のURL取得処理」を呼び出す -->
<form action="geturl.php" method="POST">
<input type="hidden" name="user_id" value="<?php echo $id_token->user_id ?>">
<input type="submit" value="ユーザー設定">
</form>
</div>
<style type="text/css">body{margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0}.wrap,body,html{width:100%;height:100%}.wrap{width:40pc;height:60px;position:absolute;top:50%;left:50%;margin-top:-30px;margin-left:-20pc}form{width:300px;float:left;margin:10px}input[type=submit]{width:100%;color:#76b729;border:solid 1px #76b729;text-align:center;display:block;font-size:14px;box-sizing:border-box;background-color:#fff;cursor:pointer;padding:0 4px;min-height:36px;line-height:36px;border-radius:6px;font-weight:700}input[type=submit]:hover{background-color:#eee;text-decoration:none}</style>
</body>
</html>
<?php
require("vendor/autoload.php");
use YConnect\Credential\ClientCredential;
use YConnect\YConnectClient;
// ユーザー設定画面URL取得APIエンドポイント
$url = "https://mythings-developers.yahooapis.jp/v2/services/3be8d46d1715dd2af8111595a44743a0/mythings/3e3c5a118ee11fe5b9ee95e0258c6a25/url";
// アプリケーションID
$appid = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// シークレット
$secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// アクセスしたユーザーのアクセストークン取得
$user_id = $_POST["user_id"];
$access_token = decrypt_token($user_id, 'access_token');
//echo "access_token : ".$access_token;
// ユーザー設定画面URL取得
$ret = request_with_token($url , $access_token);
$code = $ret["code"];
// 401認証エラーが発生した場合はアクセストークンの有効期限が切れている
if($code === 401){
// アクセストークンの更新
$access_token = refresh_access_token($appid, $secret, $user_id);
// ユーザー設定画面URL取得
$ret = request_with_token($url , $access_token);
}
$result = $ret["result"];
$res=json_decode($result,true);
//echo $res["url"];
if(!empty($res["url"])){
// ユーザー設定画面にリダイレクト
header("Location: ".$res["url"]);
}
else{
echo "ユーザー設定画面のURLを取得に失敗しました。<br>";
echo "原因は以下の4つの可能性があります。<br>";
echo "・テストユーザーに登録されていないユーザーでログインしている可能性があります。myThings Developersからテストユーザーを設定してください。<br>";
echo "・IP登録したサーバー以外からアクセスしている可能性があります。サーバーのIPとmyThings Developersに登録しているIPを確認してください。(未設定の場合は関係ありません。)<br>";
echo "・URLが間違っている可能性があります。サンプルコードの".'$url'."とmyThings Developersに表示されているAPIエンドポイントが一致している事を確認してください。<br>";
echo "・アクセストークンが正しく取得出来ていない可能性があります。アクセストークンの保存(callback.php)、および、取得を確認してください。<br>";
}
/*
* APIエンドポイントへのリクエスト
*
* トークンをヘッダーに設定してAPIエンドポイントへリクエストする
*
* @param string $url APIエンドポイント
* @param string $access_token アクセストークン
* @param array $post POSTデータ
*
* @return array $ret APIの戻り値とHTTPコード
*/
function request_with_token($url , $access_token, $post=array())
{
// HTTPヘッダーにアクセストークンを設定
$http_header = array(
"Content-Type: application/x-www-form-urlencoded; charset=utf-8",
"Authorization: Bearer " . $access_token,
);
// curlのオプション設定
$curl_setopt_array = array(
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $http_header,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_TIMEOUT => 10,
);
// POSTデータの設定
if(!empty($post)){
$curl_setopt_array[CURLOPT_POST] = true;
$curl_setopt_array[CURLOPT_POSTFIELDS] = http_build_query($post);
}
// curlでAPIエントリーポイントへリクエストを実行
$ch = curl_init();
curl_setopt_array($ch, $curl_setopt_array);
$result = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$ret = array(
"result" => $result,
"code" => $code,
);
return $ret;
}
/*
* アクセストークンの更新
*
* リフレッシュトークンを元にアクセストークンを更新する
*
* @param string $appid アプリケーションID
* @param string $secret シークレット
* @param string $user_id ユーザー識別子
*
* @return string $access_token アクセストークン
*/
function refresh_access_token($appid, $secret, $user_id)
{
$cred = new ClientCredential( $appid, $secret );
$client = new YConnectClient( $cred );
$access_token = '';
try {
// リフレッシュトークンの復号化
$refresh_token = decrypt_token($user_id, 'refresh_token');
// Tokenエンドポイントにリクエストしてアクセストークンを更新
$client->refreshAccessToken( $refresh_token );
$access_token = $client->getAccessToken();
} catch ( YConnect\Exception\TokenException $te ) {
// リフレッシュトークンが有効期限切れであるかチェック
if( $te->invalidGrant() ) {
// index.phpへアクセスし、認証から実施してください
$index_url = (empty($_SERVER["HTTPS"]) ? "http://" : "https://") . $_SERVER["HTTP_HOST"] . "/index.php";
header("Location: ".$index_url);
}
}
return $access_token;
}
/*
* トークンの復号化
*
* トークンを復号化する
*
* @param string $user_id ユーザー識別子
* @param string $kind トークンの種別('access_token' or 'refresh_token')
*
* @return string $token 復号化したトークン
*/
function decrypt_token($user_id, $kind)
{
/*
// アクセストークンの復号化
$iv = exec("cat /tmp/".$kind."_iv_".$user_id);
$base64_token = exec("cat /tmp/".$kind."_".$user_id);
$iv_test=$_SESSION[$kind.'_iv_'.$user_id];
$base64_token_test=$_SESSION[$kind.'_'.$user_id];
echo $iv_test;
echo $base64_token_test;
$enc_token = base64_decode( $base64_token );
$dec_token = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $user_id, $enc_token, MCRYPT_MODE_CBC, $iv);
$token = rtrim($dec_token);
*/
session_start();
$enc_token=$_SESSION[$kind."_".$user_id];
//echo "enc_token : ".$enc_token ."\n";
//echo "key : ".$user_id."\n";
$token=openssl_decrypt($enc_token,'AES-128-CBC',$user_id);
//echo "token : ".$token;
return $token;
}
?>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<?php
require("vendor/autoload.php");
use YConnect\Credential\ClientCredential;
use YConnect\YConnectClient;
// カスタムトリガー実行APIエンドポイント
$url = "https://mythings-developers.yahooapis.jp/v2/services/3be8d46d1715dd2af8111595a44743a0/mythings/3e3c5a118ee11fe5b9ee95e0258c6a25/run";
// アプリケーションID
$appid = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// シークレット
$secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
// カスタムトリガーに設定したキー名と値を指定
$post = array();
$post_args = array();
$post["entry"] = json_encode($post_args);
// アクセスしたユーザーのアクセストークン取得
$user_id = $_POST["user_id"];
$access_token = decrypt_token($user_id, 'access_token');
// カスタムトリガーの実行
$ret = request_with_token($url , $access_token, $post);
$code = $ret["code"];
// 401認証エラーが発生した場合はアクセストークンの有効期限が切れている
if($code === 401){
// アクセストークンの更新
$access_token = refresh_access_token($appid, $secret, $user_id);
// カスタムトリガーの実行
$ret = request_with_token($url , $access_token, $post);
}
$result = $ret["result"];
if(empty($result) || !$result["flag"]){
echo "カスタムトリガーの実行に失敗しました。";
echo "原因は以下の5つの可能性があります。<br>";
echo "・ユーザー設定が完了していない可能性があります。ステップ2で実装したgeturl.phpにアクセスし、ユーザー設定画面に「利用停止」ボタンが表示されているかを確認してください。 <br>";
echo "・テストユーザーに登録されていないユーザーでログインしている可能性があります。ログインしているユーザーをmyThings Developersからテストユーザーを設定してください。<br>";
echo "・IP登録したサーバー以外からアクセスしている可能性があります。サーバーのIPとmyThings Developersに登録しているIPを確認してください。(未設定の場合は関係ありません。)<br>";
echo "・URLが間違っている可能性があります。サンプルコードの".'$url'."とmyThings Developersに表示されているAPIエントリーポイントが一致している事を確認してください<br>";
echo "・アクセストークンが正しく取得出来ていない可能性があります。アクセストークンの保存(callback.php)、および、取得を確認してください。<br>";
} else {
echo "カスタムトリガーの実行リクエストを受け付けました。";
}
/*
* APIエンドポイントへのリクエスト
*
* トークンをヘッダーに設定してAPIエンドポイントへリクエストする
*
* @param string $url APIエンドポイント
* @param string $access_token アクセストークン
* @param array $post POSTデータ
*
* @return array $ret APIの戻り値とHTTPコード
*/
function request_with_token($url , $access_token, $post=array())
{
// HTTPヘッダーにアクセストークンを設定
$http_header = array(
"Content-Type: application/x-www-form-urlencoded; charset=utf-8",
"Authorization: Bearer " . $access_token,
);
// curlのオプション設定
$curl_setopt_array = array(
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $http_header,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_TIMEOUT => 10,
);
// POSTデータの設定
if(!empty($post)){
$curl_setopt_array[CURLOPT_POST] = true;
$curl_setopt_array[CURLOPT_POSTFIELDS] = http_build_query($post);
}
// curlでAPIエントリーポイントへリクエストを実行
$ch = curl_init();
curl_setopt_array($ch, $curl_setopt_array);
$result = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$ret = array(
"result" => $result,
"code" => $code,
);
return $ret;
}
/*
* アクセストークンの更新
*
* リフレッシュトークンを元にアクセストークンを更新する
*
* @param string $appid アプリケーションID
* @param string $secret シークレット
* @param string $user_id ユーザー識別子
*
* @return string $access_token アクセストークン
*/
function refresh_access_token($appid, $secret, $user_id)
{
$cred = new ClientCredential( $appid, $secret );
$client = new YConnectClient( $cred );
$access_token = '';
try {
// リフレッシュトークンの復号化
$refresh_token = decrypt_token($user_id, 'refresh_token');
// Tokenエンドポイントにリクエストしてアクセストークンを更新
$client->refreshAccessToken( $refresh_token );
$access_token = $client->getAccessToken();
} catch ( YConnect\Exception\TokenException $te ) {
// リフレッシュトークンが有効期限切れであるかチェック
if( $te->invalidGrant() ) {
// index.phpへアクセスし、認証から実施してください
$index_url = (empty($_SERVER["HTTPS"]) ? "http://" : "https://") . $_SERVER["HTTP_HOST"] . "/index.php";
header("Location: ".$index_url);
}
}
return $access_token;
}
/*
* トークンの復号化
*
* トークンを復号化する
*
* @param string $user_id ユーザー識別子
* @param string $kind トークンの種別('access_token' or 'refresh_token')
*
* @return string $token 復号化したトークン
*/
function decrypt_token($user_id, $kind)
{
/*
// アクセストークンの復号化
$iv = exec("cat /tmp/".$kind."_iv_".$user_id);
$base64_token = exec("cat /tmp/".$kind."_".$user_id);
$enc_token = base64_decode( $base64_token );
$dec_token = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $user_id, $enc_token, MCRYPT_MODE_CBC, $iv);
$token = rtrim($dec_token);
*/
session_start();
$enc_token=$_SESSION[$kind."_".$user_id];
$token=openssl_decrypt($enc_token,'AES-128-CBC',$user_id);
return $token;
}
?>
</body>
</html>
・変更点はTokenを保存するのにSESSIONを使っている点と暗号化をOpenSSLで行うようにした点です。
・変更が終わったらすべてのファイルをサイトにアップロードします。
サイトにアクセスしてチュートリアルと同じように動作するか確認してください。
以上 Azure WebAppsでmyThings Devlopersチュートリアルを実行するため説明でした。
うう、公開するのが10日になってしまった・・・