丁寧に言語化をする

人が成長する瞬間、あるいは「今得られる成長を最大化するやり方」みたいなのはいくつかあると思うのだけど。

例えば、環境が変わった瞬間。これは大いに伸びる。
中高の部活など顕著な例だろう。「今までに全く見知りしなかった仲間に囲まれ練習をする」とは、下手したらその1ヶ月だけで、それまでの数倍くらいの威力があるのでは。
転職もそう。新しい会社に入って、流石に思春期に得る部活動でのそれとは伸び代が違えど、「異なる面子」「異なるルール」に囲まれながら過ごす新鮮で刺激的な期間は、自身の本質を相対化し研ぎ澄ませる。

これらは、自分においても大いに実感がある。学生インターンでプログラマを始めた瞬間、その会社で(結果的にそのまま新社会人になっても同じポジションで続投した)先輩ポジションの人も社内にいなくなり1人webアプリ開発チームみたいな状況、そして転職して「先輩がいるチームで既存のプロダクトをやる」状況。
全て自分にとってターニングポイントだったとはっきり言える。

もう1つ、これは「0→1を得る」ではないが「1を10にする」ベクトルで、何であれ「丁寧に言語化をする」というのはあるのではないか。
エンジニア職であれば、コードレビューなど絶好の機会であろう。あるいは、社内外の勉強会でLTをする。これらは、「新しい知識を手に入れる」ものではないものの、自身のスキルやセンスをメタから顕在へと引きずり落とす・・・みたいな意味で、学術の深化・定着の文脈で非常に大きな成長ポテンシャルを引き出す!と思う。
残念かな、「外部の刺激を拡大する」のではなく「自身の領域の密度を上げる」ためのムーブとなるので、頭打ちもあれば自動的に成り立つものでもないが。

そんなことを、今日、自分が投げたprに対して質問を得ながら考えた。
それは「使い方は分かったけど、これまでのやり方と比べた際の利点をどう考えているか?」というものだった。ここで、「今までと何も全く変わらないが、最近ハマっているので!!」などと答えれば、それはチームに対する不遜であろう。対して、しっかりと「何となく」を形式知化する努力を達成すれば、それは自身の内においても「説明可能な知識」となるわけで。そして、「説明可能」同士は、少なくともそうでないものより、遥かに効率的であり意義深きまのになるわけで。

やったこと・書いたもの{2019,02}

会社ブログ

tech.connehito.com

社内LT

OSS

勉強会・LT

その他

昨年の下半期で結構ずっとやってたやつが、やっと・・・

TECHNICAL MASTER はじめてのPHPプロフェッショナル開発 PHP7対応 (TECHNICAL MASTER 91)

TECHNICAL MASTER はじめてのPHPプロフェッショナル開発 PHP7対応 (TECHNICAL MASTER 91)

「PHPをやっていきましょう」みたいな本を書きました

ご縁があり、同じ会社のサーバーサイドやインフラをやっている人達で本を書かせていただくことになりました。 「はじめてのPHPプロフェッショナル開発 」というタイトルです。

f:id:o0h:20190223170503p:plain
Amazonで「カテゴリ1位」になってて嬉しい(ドキドキ・・

どんな本ですか?

少し、書き手から見ての内容について触れます。

プロフェッショナル本の立ち位置

「どんな本であるか」という事については、一緒に取り組んだ & 今回の取り組みにあたって諸々のプロジェクトマネジメント的な役割も務めてくれた id:itosho525 の記事がわかりやすいです。

itosho525.hatenablog.com

もちろん、PHPの本ではあるのですが、扱っているトピックが多いので、ややもすると「帯に短し襷に長し」という印象を持たれるかもしれません。ただ、そういう印象はある意味間違っておらず、プロフェッショナル本は 入門書は巷に(特にPHPは)溢れているものの、それを読み終えた初心者が実際の現場で活躍するための次に読むべき本がないのでは? という問題意識から企画が始まっています。

ですので、プロフェッショナル本は最新のPHPを学びながら入門書と個別具体的な技術の専門書との架け橋となる本を目指して書きました。

f:id:o0h:20190223211922p:plain 『はじめてのPHPプロフェッショナル開発』という本が出版されます - comix

「一通りのトピックを扱ってみよう」というのも意識した内容であり、引用したブログ記事の図中にある通り「より専門的な内容は、それぞれの文献を探してそっちで掘り下げてみる」というような使い方になってくれれば良いなと思っています。

設計やコーディングについては、詳細で高度なとても優れた書籍が非常に多く存在しています。
単体テストも、技法・設計については書籍などがありますし、テスティングフレームワーク個別の話は、コアコードやそれを利用しているプラクティスに振れることで多くのことを学べるのかと思います。
CakePHPについては、書籍としては「上級者向けの決定版」と言えるようなものは未だ無いかもしれませんが、GitHub/Slackなどのコミュニティ、コアチームの面々を始めとした強力なデベロッパの情報発信を追うのが良いでしょう。
コンテナ技術においては、まだまだ非常に変化の激しい分野でもあると思うので、最新の情報だったりGCPやAWSといったPaaS/IaaSプロバイダの発する情報・コミュニティの発する情報を拾ったり、専門書籍から「良いやり方」を見つけていけるようにも思います。

どの領域も「この本の次」があることを意識しながら執筆チーム一同取り組んでいました。
「どこから手を付ければよいかわからない」「そもそも全体像がわからない」時に、道を指し示せればと思います。

現場感

そのように制作を進めていく中で、大事にしていたことの1つが「現場感」です。

「一通り〜」というのを意識していた、と上に書きました。そのため「体系的」「網羅的・全体的」という性格をもたせたいとは願っています。
とはいえ、「あまりにも『座学的』『教習的』でありすぎては、この本で想定している読者がワクワクしないかもね」というのは、企画段階から出ていた話です。

今回の執筆チームについては、普段から「自分がバリバリ現場でやっている」面子です。また、そもそも普段の活動が「上流や下流の別け隔てがない(そのチーム規模にない)組織で、自社サービスを提供する」というものです。
それを踏まえて、「自分たちがやっている・やりそうな内容を、エッセンスとして抜き出して並べれば、ある程度の『現場の動き』が現れてくるのではないか」という発想で、書籍内容のアウトラインが組まれました。

おそらく「この一冊で、PHPのプログラムをとことん書けるようになるぞ!!」というには、実際のPHPコードが登場する章は少なく感じられるでしょう。
裏を返せば、具体的なコードの比率をある程度抑えてまで「触れておきたかった」話を扱っています。例えばチーム開発の話題だったり、CIについて丸々1章を割いているのは、その顕れと言えます。

CakePHP

「『入門』という単語を冠したPHP本にしては、相対的に見てコードが少ない」と述べました。とはいえ、「丸々1つのWebアプリケーションを作成する(テストも込みで)」ところまでは扱います。

アプリケーションの作成にはCakePHPを選択しました。
実際に出てくるコードは、「CakePHPをめちゃくちゃカッコよく使っている」と言うには不足があるかもしれません。確かに、もっと「尖った」「Cakeぽく」書ける部分はあるなと感じます。
CakePHPの本ですか?といえば、そこが主眼でもないし、また「動くようにアプリケーションを作成する」のを本質として説明を絞ってもいるからです。

それでも、Cakeアプリケーションがテストコードも揃った状態のアプリケーションが、解説付きで世の中に出されるという事は、いちCakeファンとして嬉しいなぁと思います。

この本は、位置づけとして「『プログラミングをする』と『チームでサービスを作る』の間の架け橋」ものです。そういう意味では「PHP」の部分を置き換えても良かったかもしれないし、まして「CakePHP」の部分は他フレームワークに置換しても、同様に成り立つような感覚があります。
インターフェイスと具体実装の関係のような話ですが、少なくとも「現場っぽいやつ」としては具体的な話が必要なわけで、その目的でCakePHPを舞台に立たせました。
結果として、公式のチュートリアルよりはやや手厚めで、GitHub等に置かれている一般的なOSSよりも日本語情報が揃っていて、各実装部分・テスト部分における「代表的なスニペット」よりも網羅的に「ちゃんと運用できそうなくらい」のコードが揃っている・・というサンプルコードが作成されています。

担当パートについて

アウトラインは、以下のようになっています

  1. Part01 導入編
    • Chapter01 進化するPHP
    • Chapter02 PHPのエコシステム
    • Chapter03 PHPをはじめよう
    • Chapter04 モダンPHPの文法と基礎文法
  2. Part02 入門編  * Chapter05 チームのための開発環境構築
    • Chapter06 設計から始める
    • Chapter07 CakePHPを使ってみよう
    • Chapter08 質問と回答機能の実装
    • Chapter09 ユーザー管理機能の実装
    • Chapter10 テストコードを書く
  3. Part03 実践編
    • Chapter11 チーム開発の現場
    • Chapter12 Pull Request 駆動によるコードレビュー
    • Chapter13 開発に役立つツール
    • Chapter14 継続的インテグレーション
    • Chapter15 デプロイの自動化
  4. Part04 発展編
    • Chapter16 障害と向き合う
    • Chapter17 SQLチューニング
    • Chapter18 PHPとセキュリティ
    • Chapter19 外の世界に飛び出そう

このうち、私は

  1. Chapter01 進化するPHP
  2. Chapter02 PHPのエコシステム
  3. Chapter13 開発に役立つツール
  4. Chapter14 継続的インテグレーション
  5. Chapter16 障害と向き合う

及び「Chapter04 モダンPHPの文法と基礎文法」の一部(PHP7の新文法の話題)と、自分の担当Chapterのコラム、その他の一部コラムを担当しました。

言い換えると、今回は書籍内におけるアプリケーション作成部分は他のメンバーが書いてくれており、私は眺めていた形に😇 *1

そんな主観もあってか、「これからより『チーム』や『プロジェクト』でのサービス開発に踏み込んでいくにあたっての、橋渡しになる一冊」になれば良いなぁと感じているところです。

あとがき

ツイッターでも言及してくださっている方がいたりと、大変ありがたく感じています。
発売してからの反応がどうなるか、が最も肝心なのですが・・・・・心臓が飛び出そうですね!
誰かの役にたてばいいな〜と思います。

アフィリンクを貼りますので、よろしければポチっとお願いします😉

TECHNICAL MASTER はじめてのPHPプロフェッショナル開発 PHP7対応

TECHNICAL MASTER はじめてのPHPプロフェッショナル開発 PHP7対応

*1:とはいえ、全くPHPを書いていない!!!ということではなく、サンプルコード公開用のレポジトリにある、テストコードの作成はヘルプしたりしています

Firebase Authenticateに触れてみる②

前回の続き!!

daisuki.nichiyoubi.land

取り急ぎの作業としては

  1. cakephp-jwt-auth の仕事や中身について理解する
  2. 「How to add JWT Authentication to a CakePHP 3 REST API」で説明されているシナリオについて理解する
  3. 実際にFirebase Authenticateを利用したGItHubログインを実装してみる

desu!

・・・の前に、一旦おさらい

qiita.com ありがとうございます、ありがとうございます・・・

こうしたモチベーションというか背景みたいなのを理解した上で、「どの部分を使うのか」っていうのも理解しておかないといけない。
リンクされている記事に、「各フィールドはそれぞれ何なの」という解説があった。

JWTについて簡単にまとめてみた - hiyosi's blog

「署名付きでエンコードされて渡ってくるJSONだよ」みたいなもんなのだけれども、この「検証」「デコード」ができないといけないよね、的な。 で、そのあたりのビジネスロジックを「JWT認証プラグイン」に期待するわけで、手っ取り早くソースコード読んで見る。

cakephp-jwt-authの中身をかいつまんで見る

実態は JwtAuthenticate.php に入っている。*1

cakephp-jwt-auth/JwtAuthenticate.php at master · ADmad/cakephp-jwt-auth · GitHub

多分、関心を持たないと行けないのは

  1. getToken()
  2. _decode()

の2つだな。次点で getUser() があって、 sub フィールドを用いたfind()を打っているのが目に入るけども、これはAuthComponent/AuthenticateというCakePHP一般の話になるので割愛。 subは認証側のユーザー識別子。

    /**
     * Get token from header or query string.
     *
     * @param \Cake\Http\ServerRequest|null $request Request object.
     *
     * @return string|null Token string if found else null.
     */
    public function getToken($request = null)
    /**
     * Decode JWT token.
     *
     * @param string $token JWT token to decode.
     *
     * @return object|null The JWT's payload as a PHP object, null on failure.
     */
    protected function _decode($token)

となると、このクラス自体は「検証」をしていなくて。
鍵を使うとかの話はどこに・・・?っていうのを気にしていくと、 _decodeの中にある。

<?php
         $payload = JWT::decode(
                $token,
                $config['key'] ?: Security::getSalt(),
                $config['allowedAlgs']
            );

となっているので、configに「検証のための情報を渡さなきゃいけないのね!!」っていうのが見て取れて納得感を得られるわけです。

「How to add JWT Authentication to a CakePHP 3 REST API」で説明されているシナリオについて

いったん、こちらの記事に立ち返ります。

www.bravo-kernel.com

行っているのは

  1. ユーザー登録用のエンドポイント /api/user/register -> IDトークンを返す
  2. ログイン用のエンドポイント /api/users/token -> IDトークンを返す
  3. その他の通常のエンドポイント利用時に「トークンの検証を行う」

という感じでしょうか。

我々と前提が違うのは、

  • JWT認証サーバーも兼ねている
    • 我々は、認証についてはFirebaseに任せっきり!
  • 認証を任せていることで、2つの要件が増える
    • ユーザー登録 & tokenを生成・発行する能力が必要
    • 非ログイン→ログイン状態を生み出すための、「ログイン処理」も自前でもつ必要がある

「トークンを生成する」というのは、この部分ですね。
https://github.com/bravo-kernel/application-examples/blob/master/blog-how-to-add-jwt-authentication-to-a-cakephp-3-rest-api/src/Controller/Api/UsersController.php#L27

「salt」を自前で設定したり、 「sub]をセットしたり〜というのは、今回の我々においてはノータッチな部分です。
逆に、認証サーバー側と共通の認識を持った「鍵」を用意する必要があるわけですが。

「ログインをする」というのは、AuthComponentで「Form Authenticate」を設定している部分となります。
email/passのPOSTをしてログイン→IDトークンください!というのがコレ。

f:id:o0h:20190105214233p:plain

あるいは、コード中で言えば次の部分です

https://github.com/bravo-kernel/application-examples/blob/master/blog-how-to-add-jwt-authentication-to-a-cakephp-3-rest-api/src/Controller/Api/UsersController.php#L46-L49

という感じで、全容とそれに対するサンプルアプリケーション実装の対応がつかめてきたかなーという感じです。

実装してみよ!

ということで、我々の場合で言うと

(まずはユーザー登録側)

  1. Firebase Authenticateによるログイン後に
  2. クライアント側で取得したIDトークンを、api/users/signup に投げて
    1. これはHeaderで渡す
  3. Usersレコードを作成後、ログイン状態の確立

まで、まずは行ければOKでしょうか。
(本来はajaxなアプリケーションにして、毎回headerに入れてもらったほうが良い気がするけども。まぁ練習用なので・・・)

認証用に、usersテーブルに2列追加

もはや我々は「どうすればJWTで渡ってきた情報をこちらの情報と同定できるのか」を把握していますので、それを達成するためにDBスキーマを揃えます

usersテーブルに、 sub は必須そうなので加えておくのと、 「どのサービスからのログインか」を見るために provider も足しておきます。こちらは github.com などの情報が入る想定。(直接つなぐなら、 iss なんだろうなー)((Firebase Authenticate的には uid の方が好ましいのかな?))

bin/cake migrations create AddJwtAuthColumnsToUsers
<?php
use Migrations\AbstractMigration;

class AddJwtAuthColumnsToUsers extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $this->table('users')
            ->addColumn('sub', 'string', [
                'default' => null,
                'limit' => 128,
                'null' => false,
                'after' => 'avatar_url'
            ])
            ->addColumn('provider', 'string', [
                'default' => null,
                'limit' => 32,
                'null' => false,
                'after' => 'sub'
            ])
            ->update();
    }
}

どんなものを実装すればいいのか(認証周り・サーバー)

やりたいことは

  • ヘッダー中にIDトークンを入れて投げれば良い
    • デフォルトだと authorization フィールドになっている
  • IDトークンのデコードができれば良い
  • 引き出した subフィールドで、usersテーブルから持ってこれれば良い
  • 引っ張ってきたuser情報をAuth->setUser()で噛ませれば良い

だと思います。

AppController::initialize() の中身を、こんな感じか・・・?

<?php
public function initialize()
{
// 省略
        $this->loadComponent('Auth', [
            'storage' => 'Memory',
            'authenticate' => [
                'ADmad/JwtAuth.Jwt' => [
                    'key' => /** どこ! **/'', 
                    'userModel' => 'Users',
                    'fields' => [
                        'username' => 'sub'
                    ],
                    'queryDatasource' => true
                ]
            ],
            'unauthorizedRedirect' => false,
            'checkAuthIn' => 'Controller.initialize'
        ]);

ってことで、鍵情報を探しましょう

Firebase Authenticateで利用する「鍵」

これは公式Docに書いてある気がするので、当たってみる。

Verify ID Tokens  |  Firebase

最後に、トークンの kid 要件に対応する秘密鍵によって ID トークンが署名されたことを確認します。https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com から公開鍵を取得し、JWT ライブラリを使用して署名を確認します。該当エンドポイントからのレスポンスの Cache-Control ヘッダーに含まれる max-age の値を使用して、公開鍵を更新する時期を確認します。

これかな・・・?

https://github.com/search?q=https%3A%2F%2Fwww.googleapis.com%2Frobot%2Fv1%2Fmetadata%2Fx509%2Fsecuretoken%40system.gserviceaccount.com&type=Code

ふーむ。

というのと、JWT SDKを見ると「公開鍵のコンテンツを入れろ」であり、パスを指定するんじゃないのか。

f:id:o0h:20190105231743p:plain
vendor/firebase/php-jwt/src/JWT.php

(まぁ、 Security::salt() の値を入れるか?の択一なのだから、そうか)

ということで、一旦、ものすごく雑に

  • オンザフライで鍵情報をセットしてみる
  • それをもとにしてデコードができるかを確かめる

までをスコープとして、話を進めてみる。

やるのが

  1. Authenticate objectに渡す設定の完成
  2. AuthComponentで/api/users/signup.json の解放(非ログイン状態のアクセス許可)
  3. curlで叩く

というもの。

Authenticateの設定

  1. 鍵を入れる
  2. アルゴリズムの設定

をやる必要があった。

最終的に修正したのが、以下。

diff --git a/api/src/Controller/AppController.php b/api/src/Controller/AppController.php
index 8288dfd..f0de2d8 100644
--- a/api/src/Controller/AppController.php
+++ b/api/src/Controller/AppController.php
@@ -16,6 +16,7 @@ namespace App\Controller;

 use Cake\Controller\Controller;
 use Cake\Event\Event;
+use http\Client;

 /**
  * Application Controller
@@ -46,11 +47,15 @@ class AppController extends Controller
         ]);
         $this->loadComponent('Flash');

+        $keys = (new \Cake\Http\Client())
+            ->get('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com')
+            ->getJson();
         $this->loadComponent('Auth', [
             'storage' => 'Memory',
             'authenticate' => [
                 'ADmad/JwtAuth.Jwt' => [
-                    'key' => /** どこ! */ null,
+                    'key' => $keys,
+                    'allowedAlgs' => ['RS256'],
                     'userModel' => 'Users',
                     'fields' => [
                         'username' => 'sub'

/api/users/signup.json の解放

これをしないと、ログインする前にログインを求められる地獄が出来上がりますからね・・
ついでなので、この時点で「デコードされた結果」まで露呈させるようにしちゃおう

diff --git a/api/src/Controller/Api/UsersController.php b/api/src/Controller/Api/UsersController.php
index d3682f0..75f4fa0 100644
--- a/api/src/Controller/Api/UsersController.php
+++ b/api/src/Controller/Api/UsersController.php
@@ -2,15 +2,24 @@

 namespace App\Controller\Api;

+use ADmad\JwtAuth\Auth\JwtAuthenticate;
 use App\Controller\AppController;

 class UsersController extends AppController
 {
+    public function initialize()
+    {
+        parent::initialize();
+        $this->Auth->allow(['add']);
+    }

     public function add()
     {
+        $this->Auth->identify();
+        /** @var JwtAuthenticate $auth */
+        $auth = $this->Auth->getAuthenticate('ADmad/JwtAuth.Jwt');
         $this->set('_serialize', 'data');
-        $this->set('data', ['status' => 'OK']);
+        $this->set('data', $auth->getPayload());
     }

 }

実際にリクエストしてみる

IDトークンのとり方はすでに触れているので割愛するけれど、それを利用して次のような形でアクセスする。

curl -X "POST" "http://localhost:8101/api/users/signup.json" \
     -H 'Authorization: bearer ${実際にブラウザとかでとったIDトークン} ' \
     -H 'Content-Type: application/json'

これで上手くいってると、ちゃ〜んと情報が入ってきた!! f:id:o0h:20190105235353p:plain

もし、ココでそのまま $this->Auth->setUser((array)$auth->getPayload()); としてあげれば、ログイン状態の確立はそのままできちゃいそうかな? *2

clientとつなぐ

では、実際にJS側から叩いてみます。
corsとか面倒くさいので、 github.htmlはもうwebroot/下に置いてしまいましょう。 その上で、次のようなコードを書き加えてみました。

const config = {
    // いつもの
};
firebase.initializeApp(config);

const provider = new firebase.auth.GithubAuthProvider();
firebase.auth().signInWithPopup(provider).then((result) => {
  firebase.auth().currentUser.getIdToken(true).then((idToken) => {
    const xhr = new XMLHttpRequest()
    xhr.open('POST', 'http://localhost:8101/api/users/signup.json')
    xhr.setRequestHeader('Authorization', `Bearer ${idToken}`)
    xhr.send()
  })
}).catch((error) => {
  alert(error.message)
})

これをやると、「勝手にgithubのpopupが開いて」「勝手にsignup.jsonを叩いて」という挙動を起こします。
成功・失敗の見分けがつきにくいのですが、ChromeのNetworkパネルを開いておいてxhrが200を返していたら成功です。
もしくは、Auth::setUser()などに JWTAuthnticateから取得したpayloadをそのまま突っ込んでおくと、ログイン状態が成立しますので、 例えばその後に /users などを開くと、「要ログイン」状態にあったものを突破できます・・・!!

userの登録

あとはもう、なんてことなくて「普通にCakeかくぞ〜」という感じです。

例えば、 add()アクションの内部でこんな感じのメソッドでも呼んであげると良いのでないでしょうか。

<?php
$this->Auth->identify();
$auth = $this->Auth->getAuthenticate('ADmad/JwtAuth.Jwt');
$this->registerUser($auth->getPayload());
<?php
    private function registerUser($auth)
    {
        $user = $this->Users->newEntity([
            'sub' => $auth->sub,
            'provider' => $auth->firebase->sign_in_provider,
            'avatar_url' => $auth->picture,
            'name' => $auth->name,
        ]);

        return $this->Users->saveOrFail($user);
    }

これで usersテーブルへのsubフィールドの探索は実行されるので問題なさそ、という感じです。

残todoとまとめ

処理の流れを掴むため〜という目的の記事だったので、(あの酷いJS部分を差し引いても)実用できるレベルにはありません。
少なくとも、以下の点は必要だと思います。

  1. そもそも既存ユーザーログイン実装してないし!
  2. 取得した情報の、タイムスタンプ他の検証をしていない
  3. ache-Control ヘッダーに含まれる max-age の値を使用して、公開鍵を更新する時期を確認し てないので、非効率だし迷惑

など。まぁ、概念はつかめたと思うので、やってしまえば問題ないかな〜とは思います。

いずれにせよ何にせよ、あの面倒くさいユーザー情報管理があっという間にパワフルに完成するぞ!!すごいぞ!!!!!という気持ちになりました、使うぞ!


できたもの

github.com

*1:cakeの認証オブジェクトについてはhttps://book.cakephp.org/3.0/ja/controllers/components/authentication.html#id12 など

*2:コントローラーの中からAuthenticate Objectにメッセージを直接吐かせるのって、どうなんだろうな〜。他のSocial Loginとかの場合の実装ってどうしてんだろ

Firebase Authenticateに触れてみる①

イントロ

ちょっとした思いつきで、pet projectとして小さなサービス作ってみるか!!というのを昨日の夜に思いついたのですが。
・・年末〜年始でやろう!と思って熱の引いてたネタの焼き直しで。ふと「これなら形にできそうだぞ!」というアイディアが浮かんだので。

で、テーブル設計とかばっと出しつつ、必要な画面を考え始めたところで「ユーザー登録/認証周りどうしようかな〜」っていう・・・
作るだけなら強制ログイン状態を生み出して、まず「動くように作る」をして、そっから考えよ〜!でも良いのだし。そうするつもりだったのですが。

うん、パスワードとかtokenとか預かるの嫌だ!
やるにせよ、何か今風のないのかな〜と。

GitHub/Twitter/Gmail辺りのOAuth連携をやるか、昔ながらのId/Passログインを作るか。ああ、tokenも預かりたくないし、かといって改竄できそうなのも嫌だし、パスワードも預かりたくねぇな〜ていうかソーシャルのアバター使いてぇな〜なんかな〜〜〜

ってことで、Firebase Authenticateを使ってみようと思いたち。

すごい雑なログインシステム(まで

びっくりするくらい簡単に動いてしまって、まだびっくりしてます。

  1. まず プロジェクトを作る https://console.firebase.google.com/
  2. Authentication -> sign-in method で(とりあえず) email/passを有効にする f:id:o0h:20190105170721p:plain
  3. コンソールのPJ概要にあるスニペットとウェブサイトで Firebase Authentication を使ってみる  |  Firebase を参考にいじる f:id:o0h:20190105171203p:plain

で、出来上がったのが下のようなもの。

すごく雑なFirebaseAuthenticateログインフォーム · GitHub

・・・これだけでちゃんと動くっていう。。 驚きがありませんか。

やりたいことを整理する

モチベーションは「ログイン・認証機構をこっちで作りたくないし、秘密情報をこっちのストレージにおいておきたくない」です。
こんな我儘を実現するシステムとは・・・

  1. エンドユーザーの操作によって、3rd partyのプロバイダに対して、「有効なアカウント」のお墨付きをもらってくる
  2. エンドユーザーから、こちらのサーバーに「安全で安心な認証済み情報」が送られてくる
  3. こちらのサーバー上にあるユーザーデータと紐付けて、ログイン処理を実行する

このあたり、なんとなくJWTとか使ってみたら行けるのかな〜どうかな〜って思ってるんですけどね。使ったことないので。試してみる。

ココらへんを読み解けば良いかな?

fireabase的には firebase.auth().currentUser.getIdToken()で「トークン」がとれる、と。
この値をhttps://jwt.io/ でデバッガに渡してみると、認証情報が入ってるっぽい。

f:id:o0h:20190105174459p:plain

このuidの値から、こちらのユーザーデータに対して同一性を手繰り寄せて行けばよいのかな?
やってることはコレっぽいな。
Firebase Auth のユーザ認証機能を自前のデータベースと連携する - Qiita 「Firebase Admin SDKを使うか、JWTライブラリを使うか」という、正にドキュメント上で言及されていた分岐があるだけ〜という感じがする。

どっちも内容は同じな気がしていて、「Firebase Admin SDKを利用するメリットはなにか」というと、ドキュメントに書かれているこの辺りか。

提供された ID トークンが正しい形式で、期限切れではなく、適切に署名されていれば(デコードされた情報を取得できます)

逆にJWTについての但し書きは

(利用するためには)ID トークンのヘッダー、ペイロード、署名を確認します。

となっている。

つまり、JWT実装した場合は自前で検証をしなければならないーと。

先に貼った「CakeでJWT認証やってみよう」の記事においては 10. Testing JWT Authentication の部分に当たるかな?

ということで、やることとしては

  1. client側では、Firebase Authenticateを通ったあとに「トークン」をこっちのサーバーに投げる
  2. こっちのサーバーでは、投げられたトークンを検証する
  3. 検証に通ったらアプリケーション上でのユーザー同定・ログイン処理を行う

ができりゃ、いーかなぁ

実際に「外部プロバイダ」でログインやってみる with GitHub

大体の処理の流れは掴んだ?と思うので。あとは理屈で覚えるより動かしながら眺めてみる。

ってことで、GitHubログインをやってみたい

JavaScript を使用して GitHub で認証する  |  Firebase

  • まずは sign-in method でGitHubを有効にして f:id:o0h:20190105175810p:plain
  • callback urlを確認して
  • GitHubの認証appを新規作成して https://github.com/settings/applications/new
  • さっき確認したcallback urlを参考にしつつ必要な情報を埋めて
  • 登録するとアプリケーション詳細ページに飛ぶのでClient ID/secretを確認して
  • Firebase consoleに戻って、ID/secretを記入する

サービス側の設定はこれでOK。
ココらへんから「HTMLファイルを直開き」だと動かなくなってくる(http:// or https:// で始める必要がある)ので、必要に応じて firebase serveコマンドなどを用いてローカルサーバーを利用する

先のドキュメントを参考にして、スクリプトを書いていく ・・・いちおうgistにも貼ってはいるけれど、これだけで動いちゃう・・・

const provider = new firebase.auth.GithubAuthProvider();
firebase.auth().signInWithPopup(provider).then((result) => {
  console.log(result.user)
}).catch((error) => {
  alert(error.message)
})

f:id:o0h:20190105184503g:plain

意味わからないですね簡単すぎて・・・・

ということで、あとはサーバーサイドの実装。

簡単なusersテーブル付きのログインできるcake appを書く

もう、べったり先のブログを参考にして。 プラグイン使ってJWT認証どのくらい簡単にできるかな?に挑みます。

github.com

あ、でもCRUDプラグインはいいや。

ココらへん使いつつ、Docker環境用意してcakephp/appのproject作って〜っていうのはいつもの通りなので省略。

1. cakephp-jwt-authプラグインの設置

このように

# composer require admad/cakephp-jwt-auth
Using version ^2.3 for admad/cakephp-jwt-auth
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing firebase/php-jwt (v5.0.0): Downloading (100%)
  - Installing admad/cakephp-jwt-auth (2.3.2): Downloading (100%)
Writing lock file
Generating autoload files
> Cake\Composer\Installer\PluginInstaller::postAutoloadDump
/app # bin/cake plugin load ADmad/JwtAuth

/app/src/Application.php modified

2. usersテーブルの設置

親切にMigrationファイルのサンプルとか用意してくれてるの。すげー

users.php

で、ちょっと内容を変えたいので(icon urlとか足してみたい)、参考にしつつ少しいじる

  1. bin/cake bake migration CreateUsers して
  2. こんな感じの内容を(下記
  3. bin/cake migrations migrate する
<?php
use Migrations\AbstractMigration;

class CreateUsers extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * http://docs.phinx.org/en/latest/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $this->table('users')
            ->addColumn('name', 'string', [
                'default' => null,
                'limit' => 64,
                'null' => false
            ])
            ->addColumn('avatar_url', 'string', [
                'default' => null,
                'limit' => 256,
                'null' => false
            ])
            ->addColumn('created', 'timestamp', [
                'default' => 'CURRENT_TIMESTAMP',
                'limit' => null,
                'null' => false
            ])
            ->addColumn('modified', 'datetime', [
                'default' => null,
                'limit' => null,
                'null' => true
            ])
            ->create();
    }
}

3. users周りのコードをザクッと

bin/cake bake all Users しておく。
いろいろなファイルが出来上がる。

この時点で、 /users/add とかを見ると、画面ができているよね f:id:o0h:20190105202838p:plain

4. サインイン用のAPIエンドポイント

ここからが本番!って感じ。

tokenを投げてユーザー登録をする先 が必要になるので。

/api/user/signup.json とでもしよう。

routes.phpに下記を追記

<?php
Router::scope('/', function (RouteBuilder $routes) {
    // 省略
    $routes->prefix('api', function (RouteBuilder $routes) {
        $routes->setExtensions(['json']);
        $routes->post('/users/signup', ['controller' => 'Users', 'action' => 'add', 'prefix' => 'api']);
    });

src/Controller/Api ディレクトリを新設し、以下のように UsersController を作成

<?php

namespace App\Controller\Api;

use App\Controller\AppController;
use Cake\Http\Exception\NotImplementedException;

class UsersController extends AppController
{

    public function add()
    {
        throw new NotImplementedException('これからね!');
        
    }
}

この時点で、一旦routes.phpから $routes->applyMiddleware('csrf'); をコメントアウトしておいて、こんな感じのcurlを打ってみる

curl -X "POST" "http://localhost:8101/api/users/signup.json" -H 'Content-Type: application/json; charset=utf-8'

これで status:ok といったレスポンスが返ってくれば、routingはこれでOK!

5. Enabling JWT Authentication

とのことなのだけど・・・はて・・・

長くなってきそうなので、一旦ここまで!

次にやることは

  1. cakephp-jwt-auth の仕事や中身について理解する
  2. 「How to add JWT Authentication to a CakePHP 3 REST API」で説明されているシナリオについて理解する
  3. 実際にFirebase Authenticateを利用したGItHubログインを実装してみる

で!

daisuki.nichiyoubi.land