2015年2月14日土曜日

Firebaseで認証付きアプリを作るには

Firebaseはバックエンドサービスです。BaaS(Backend as a Service)と言われますが、Firebaseのサイトには直接BaaSという言葉はなさそうです。
一言で言うと、ドキュメント型データベースとそのライブラリからなるシステムです。ドキュメント型データベースはMongoDBが有名です。Firebaseはデータ全体がJSONです。Firebaseのデータをビジュアルに編集できるWebページを提供しています。
これだけなら単にMongoDBをWebサービスにしても同じことです。しかし、Firebaseが面白いのはAngularJSなどのクライアントサイドフレームワークとの相性が抜群だということです。AngularJS用にAngularFireというライブラリを提供しています。AngularFireでは、MVCをクライアントサイドで実現します。AngularFireのModelは、DOM、Firebaseと3方向に同期されます。その結果、クライアント単体でアプリケーションを実現します。つまり、アプリケーショサーバがいらないのです。

Firebaseにはホスティングの機能があり、Webサーバとしても働きます。しかし、それはWebページを配信するためのものであり、サーバ側で処理するアプリケーションサーバを意味しません。
Webアプリケーションは3層モデルを採用してきました。3層からアプリケーションサーバが除かれると2層のクライアントサーバに戻ります。しかし、アプリケーション配信が不要などWebアプリのメリットは維持しています。
なお、その他のJavaScriptでも同じく2層モデルを構成できます。しかし、JavaScript以外の言語では従来通り3層モデルとなります。
2層化にはセキュリティの問題があります。クライアントに自由にデータベースを操作することを許すので、不正アクセスも簡単です。よって、読み取り専用データを除けば、必ず認証(Authentication)・承認(Authorization)と組み合わせる必要があります。

FirebaseではOAuthのライブラリも提供しています。GoogleでログインするWebアプリも簡単に作れます。OAuthのクライアントIDおよび秘密鍵はFirebase側に登録され、リダイレクトページも用意されます。承認はFirebase側でセキュリティルールを設定することで実現します。一般的には、ログインしたユーザだけがアクセスできるように設定します。
問題となるのは、クライアントコードが不正に改変された場合です。特にOAuthを使うと、例えばGogoleのユーザなら誰でも不正アクセスすることができます。そのため、OAuthに加えて独自の絞り込みが必要になります。なお、OAuthには送信元が登録されるので、クライアントコードをコピーしてもOAuth認証を回避できるわけではありません。

実際に、AugularFireで認証付きアプリを作成しようとすると、情報が少ないことに驚きます。そこで、具体的な方法を紹介します。ただし、この方法は試行錯誤の結果なので、必ずしもベストあるいは公式に推奨される方法とは限りません。

ポイントは$firebaseと$firebaseAuthの2つを使う点です。
$firebaseAuthからFirebaseデータにアクセスできるかと思って試したのですが、どうやら認証専門のようです。二重の参照は美しくありませんが、やむをえません。$firebaseAuthが$firebaseを継承してくれればよいのですが。
Googleのメールアドレスの中で@toyo.jpを持つユーザだけに限定しています。auth.uidは意味不明の数字なので、{scope: "email"}がかかせません。該当しないユーザは$unauthしてしまえばログインを拒否したことになります。ちなみにメールアドレスはFirebaseのnameにできません。メールアドレスに含まれる特殊記号がはじかれます。

var app = angular.module("sampleApp", ["firebase"]);
app.controller("SampleCtrl", ["$scope", "$firebase", "$firebaseAuth", function($scope, $firebase, $firebaseAuth) {
  var ref = new Firebase("https://minoru-uehara.firebaseio.com/");
  var auth = $firebaseAuth(ref);
  $scope.logout = function() {
    auth.$unauth();
  };
  $scope.login = function() {
    auth.$authWithOAuthPopup('google', {scope: 'email'});
  };
  auth.$onAuth(function(authData) {
    $scope.authData = authData;
    if (authData) {
      var mail = authData.google.email;
      if (! (/@toyo.jp$/i).test(mail)) {
        auth.$unauth();
      } else {
        var denys = [
          /^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9]+@toyo.jp$/i,
          /^[A-Za-z][0-9][A-Za-z0-9]+@toyo.jp$/i
        ];
        for(var i = 0; i < denys.length; i++) {
          if (denys[i].test(mail)) {
            auth.$unauth();
          }
        }
      }
      // unauthさせずに到達すればOK
      // 共有データ
      $scope.data = $firebase(ref.child('data')).$asObject();
      // プライベートデータ
      $scope.users = $firebase(ref.child('users').child(authData.uid)).$set({email: authData.google.email});
    }
  });
}]);


0 件のコメント: