Azure WebAppsでmyThings Developersのサンプルコードを動かしてみる。

myThings Advent Calendar 2016 9日目です。（やばいもうすぐ10日になってしまう）

myThings DeveropersのハンズオンではBluemix上にサーバを構築しますが、用意するサーバはPHPが動けばいいので、Microsoft AzureのWebAppsをサーバにしてみました。簡単にできると思ったら以外に大変でした・・・・

##準備
・Microsoft Azure サブスクリプション
・Yahoo! Japan ID

##目標
こちらの[チュートリアルStep3](https://mythings-developers.yahoo.co.jp/document/tutorial_section3)をAzure WebAppsで実行できるようにします。

##手順
###WebAppsの作成
・Azureポータルから新規＞Web+モバイル＞Web Appを選択します。
![1.PNG](https://qiita-image-store.s3.amazonaws.com/0/62606/e743dba9-4a2e-9121-f92f-871ab7d17bc8.png)

・アプリ名とリソースグループを入力して下の「作成」ボタンをクリックします。
![2.PNG](https://qiita-image-store.s3.amazonaws.com/0/62606/9d035b31-6eef-a0ff-08d6-074ed1facfe3.png)

・Web Appのデプロイが完了したら「拡張機能」＞「拡張機能の追加」＞「拡張機能の選択」の中から「Composer」を選択して「OK」をクリックします。つづいて「拡張機能の追加」の「OK」をクリックするとComposerが追加されます。
![3.PNG](https://qiita-image-store.s3.amazonaws.com/0/62606/9e780ee7-c28b-c931-6e2a-6a1dfe833e60.png)

・Composerを追加した後は一度サイトを再起動します。

・[こちら](https://getcomposer.org/download/)のページの下の方にあるComposerの実行ファイル(composer.phar)をダウンロードしておきます。

・Yahoo! ID連携 SDK for PHPをインストールするためのcomposer.jsonを次の内容で作成します。

``` 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」と入力して実行します。しばらくすると文字化けの状態ですがインストールが完了したメッセージが表示されます。
![4.PNG](https://qiita-image-store.s3.amazonaws.com/0/62606/e951bca0-56a4-eb43-efc3-3e98d02fd6f5.png)

・myThingsDevelopersサイトで実装を選択して必要な内容をすべて入力します。
![5.PNG](https://qiita-image-store.s3.amazonaws.com/0/62606/0fc1a4bd-04ef-94bb-0486-1ca6579e498f.png)

・サンプルコードを表示して次のファイルを一旦保存します。
　　・index.php
　　・callback.php
　　・geturl.php
　　・setting_callback.php
　　・customtrigger.php
　　・errorlist.php

・一部のファイルを変更します。

``` PHP:callback.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:geturl.php
<?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 "原因は以下の４つの可能性があります。<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;
	}
?>
```

```PHP:customtrigger.php
<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 "原因は以下の５つの可能性があります。<br>";
		echo "・ユーザー設定が完了していない可能性があります。ステップ２で実装した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日になってしまった・・・