「ユーザー認証にはAmazon Cognitoが便利そうだけどPHPの既存システムも再利用するにはどうすればいい?」
こんにちは、カフーブログのタカフです。
本日の記事はたぶん役立つ人にはめちゃくちゃ役立つ記事かと思います。
今開発しているアプリにおいてフロントエンドでユーザー認証にCognitoを使おうと思っていますが、
サーバーサイド上でAPIを通じてそのユーザーのデータを取得する時などはそのユーザーかどうか検証しなくてはいけません。
本記事では、Cognitoで認証されたユーザーがPHP製のAPIを通してデータ提供するにはどうするかをお答えします。
本記事のシステム概要
公式ドキュメントにいい画像があったのでそのまま引用させて頂きます。
ご覧の通り、Cognitoのユーザープールから認証処理で取得したアクセストークンを、バックエンドで検証してこのユーザーにだけ許可されたデータを取得する、というものです。
Cognitoのユーザープールのセットアップ
まずはユーザー認証機構としてCognitoがなくてはならないので早速セットアップをしていきましょう。
Cognitoの画面にアクセスして「ユーザープールの管理」から始めます。
任意のプール名を入力して「ステップに従って設定する」を選択します。
ユーザーログイン時にユーザー名は忘れがちなので、「Eメールアドレスおよび電話番号」の方を選択します。
今時ユーザー名ログインってあまり見ないですよね。
この選択をした場合は必ずどの標準属性を選択するかのところでemailをONにしてください。
僕はこれをONにしてなくてログイン出来なくてハマりました。。
また、プール作成後には変更出来ないと記載されてるので作る時は仕様を要検討です。
パスワード強度は、警告文言にマケズ、特殊文字と大文字を一旦無しとします。(amazon.co.jp自体は小文字だけでいけたような気がしますが。。)
ユーザーの自己サインアップはもちろんON
「アプリクライアント」までは一旦デフォルト値とします。
「アプリクライアントの追加」をします。
アプリクライアントの設定です。ここ結構大事です。
「アプリクライアント名」は任意の文字列
「トークンの有効期限を更新(日)」はデフォルトでは30日ですが、
今回作ろうとしているのはWebベースのスマホアプリなので365日としてます。
FacebookアプリやTwitterアプリもスマホ上ではログインなんて初回くらいしか求められないですもんね。
要件によっては最長の3650日でもいいかもです。
「クライアントシークレットを生成」は今回ブラウザベースのアプリなのでOFFにします。
最後に「プールの作成」でユーザープールの作成完了です。
javascriptでCognitoのユーザー認証周りの実装
フロントアプリの作成
僕はjavascriptで作るスマホアプリなら圧倒的にFramework7をオススメします。
https://kahoo.blog/app-developing-with-framework7/
今回もFramework7をベースに作っていきます。
僕の運営しているFramework7日本語サイトもあるよ。
ログインセッション確認
では本題に入ります。
まず、アプリUIを開いた時に現在ログイン中かどうかを調べて、ログインしていないならログイン画面を出すようにします。
ログイン中かどうを調べるには以下のコードとなります。
先ほど作成したユーザープールの プールID と クライアントID を持ってきます。
// npm i amazon-cognito-identity-js してね♪
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
// Cognitoで作ったユーザープールIDとクライアントIDをセット
var data = {
UserPoolId: 'ap-northeast-1_xxxxxxxxxx',
ClientId: 'xxxxxxxxxxxxxxxxxxxxxxxx'
};
var CognitoUserPool = new AmazonCognitoIdentity.CognitoUserPool(data);
// 現在ログインしているかどうかはこの関数でOK
var cognitoUser = CognitoUserPool.getCurrentUser();
if (cognitoUser != null) {
cognitoUser.getSession(function (err, session) {
// ここに入ればログイン中処理に進める
});
}else{
// ログインしていないので、ログイン画面を開く
}
ログインしてない場合は、一般的にはログイン画面を出します。
Framework7だとこのあたり用意された部品を並べるだけなので楽です。
まだ会員登録してない人用に会員登録ページを促します。
モックでは「無料会員登録はこちら」リンクで下記のような会員登録画面を開くようにしました。
サインアップ(会員登録)処理の実装
今回の会員登録のモック画面はこんな感じです。
上記のような画面でメールアドレスとパスワードを入力して「無料会員登録」を押すとCognitoに対して仮登録をする処理の実装をします。
// emailとpasswordを取得
const email = this.$el.find('input[name="email"]').val();
const password = this.$el.find('input[name="password"]').val();
var poolData = {
UserPoolId: 'ap-northeast-1_xxxxxxxxx',
ClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var attributeList = [];
// 仮登録処理(Cognitoから変えるとコールバック関数がキックされる)
userPool.signUp(email, password, attributeList, null, function(err, result){
if (err) {
console.error(err);
return;
}
var cognitoUser = result.user;
console.log('user name is ' + cognitoUser.getUsername());
// この時点で登録メールアドレスに確認コードのメールが飛んでいるはずなので
// ここではプロンプトで入力を促す
var code = window.prompt("確認コードを入力してください");
// 確認コードをCognitoの提供する関数に渡すと検証が行われる
cognitoUser.confirmRegistration(code, true, function(err, result) {
if (err) {
alert(err);
return;
}
// ここまで到達したら成功したのでログイン画面にする。
// ここではリロードでトップに戻す。
location.reload();
});
});
実際にこれで実行するとメールが届きます。
現在のCognitoの管理画面では以下のような状態です。
これを上記のコードで confirmRegistration
に渡してやると、本登録のようになります。
Cognitoの管理画面でもユーザーが追加されて、Eメール確認済みがtrueとなります。
ログイン処理の実装
上記までの処理で会員登録が完了すればログインが可能となります。
先ほどのログイン画面での処理を実装します。
メールアドレスとパスワードを入力してログインボタンを押下したら、
Cognitoに対してログインする処理を実装します。
// emailとpasswordを取得
const username = this.$el.find('input[name="email"]').val();
const password = this.$el.find('input[name="password"]').val();
const authenticationData = {
Username: username,
Password: password
};
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);
var poolData = {
UserPoolId: 'ap-northeast-1_xxxxxxxxx',
ClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username : username,
Pool : userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
// Cognitoの提供するログイン関数
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
/* ここに到達するとログインの完了 */
// 以下のようにアクセストークンを取得できるが、
// IDトークン・アクセストークン・更新トークンがlocalStorageに保存してある
var accessToken = result.getAccessToken().getJwtToken();
var idToken = result.idToken.jwtToken;
console.log(accessToken);
console.log(idToken);
/* マイページの表示処理などはここ */
},
onFailure: function (err) {
/* ログインの失敗 */
// 失敗に応じて適切な案内を出す
console.error(err);
},
});
onSuccess
に入ればログイン処理が完了して、ローカルストレージにIDトークン・アクセストークン・更新トークンが保存されます。
PHP製のAPIでアクセストークンの検証
さぁここからがミソのコードとなります。
上記までのCognitoのサインイン処理は結構ネットに転がっていますが、
ログイン後のアクセストークンを使ってのサーバーサイドでの検証処理があまりなかったので苦労しましたが、
結果としては以下のようなコードとなります。
と、その前にまずjavascript側のコードです。
HTTPリクエストのヘッダーのアクセストークンを乗せてajaxでリクエストします。
Basic認証は Authorization
というキーで出しているのでそれと被らないようにここでは X-Authorization
とします。
var data = {
UserPoolId: 'ap-northeast-1_xxxxxxxxx',
ClientId: 'xxxxxxxxxxxxxxxxxxxxxxxx'
};
var userPool = new AmazonCognitoIdentity.CognitoUserPool(data);
var cognitoUser = userPool.getCurrentUser();
// getSessionでは内部で更新トークンを使ってアクセストークンをリフレッシュしてる
cognitoUser.getSession(function (err, session) {
window.session = session;
if (err) {
alert(err);
return;
}
app.request.promise({
url: "https://[yourapp.com]/api/yourapi.php",
dataType: "json",
headers: {
"X-Authorization": session.getAccessToken().getJwtToken()
},
}).then((res) => {
// API成功時の処理
console.log(res);
}).catch(() => {
// API失敗時の処理
});
});
Cognitoから取得できるアクセストークンの有効期限は1時間とかなのですが、
それではスマホアプリのようにそもそもの人物認証があるデバイスではあまり適していません。
先ほどCoginitoで設定した有効期限を365日とした更新トークンというのを使います。
これはAWSのサポートに聞いたのですが、 getSession()
からトークンを取得すれば内部で更新トークンを使って最新のアクセストークンを取得できるそうです。
さて、次にPHP側のコードです。
ポイントは、
クライアントから送られてきたアクセストークン(JsonWebToken)をBase64デコードして、Cognitoの発行する公開鍵と付き合わせる、です。
公式ドキュメントに詳しい解説がアップされています。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
今回ライブラリとして、
検証用に
https://packagist.org/packages/firebase/php-jwt
公開鍵変換用に
https://packagist.org/packages/codercat/jwk-to-pem
を使っています。
composerからインストールしました。
use \Firebase\JWT\JWT;
use CoderCat\JWKToPEM\JWKConverter;
// HTTPリクエストヘッダーからアクセストークン(JsonWebToken)を取得します
$headers = getallheaders();
$jwt = "";
foreach ($headers as $name => $value) {
if ($name == "X-Authorization") {
$jwt = $value;
break;
}
}
try {
if ($jwt) {
// Cognitoから公開鍵を取得します
$url = "https://cognito-idp.{リージョン名}.amazonaws.com/{ユーザープールID}/.well-known/jwks.json";
$jwks = file_get_contents($url);
// JSON Web Tokenをデコードしてヘッダーのkidを見つけます
$tks = explode('.', $jwt);
if (count($tks) != 3) {
throw new Exception('JWTのフォーマットがおかしいです');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
$jwt_header = json_decode(JWT::urlsafeB64Decode($headb64), true);
if (empty($jwt_header["kid"])) {
throw new Exception("JSON Web Tokenにkidがありません");
}
// JSON Web Keysをデコードしてjwtのkidと合致するjwkから公開鍵を取得します
$publicKey = "";
$jwks_data = json_decode($jwks, true);
foreach ($jwks_data["keys"] as $jwk) {
if ($jwk["kid"] == $jwt_header["kid"]) {
$jwkConverter = new JWKConverter();
// 公開鍵取得
$publicKey = $jwkConverter->toPEM($jwk);
break;
}
}
if ( !$publicKey ) {
throw new Exception("公開鍵が取得出来ません");
}
// JSON Web Tokenを公開鍵で検証もかねてデコードします。
$decoded = JWT::decode($jwt, $publicKey, array('RS256'));
if ( !$decoded ){
throw new Exception("検証エラー");
}
/* ここまで到達したら検証OKなので本来のAPIのデータを渡してもOK */
/* デコードデータにユーザーIDなどあるのでそれでデータを引っ張ることが出来る */
$api_data = [];
$data["data"] = $api_data;
header('content-type: application/json; charset=utf-8');
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo $json;
}
}catch(Exception $e){
// 失敗した時のコード
echo "errro";
}
無事decord出来るところまで処理が通れば検証OKということになります。
安心してこのユーザーのデータを提供出来ます。
まとめ
結構な長文記事となってしまいました。
Cognitoを使ったアプリ開発で、バックエンドはPHPを使った場合はこれがベースになるかと思います。
恐らくどんな環境でもつかいまわせることでしょう。
もしお役にたてたらこのツイートのいいね!をお願い致します。
CognitoとPHPを使った役立つ記事を書きました。
個人的には10いいね!はもらいたいくらいの記事。https://t.co/WWgnAX2VbT
— スズキタカフミ@ウェブ屋さん (@kahoo365) October 8, 2019