Sails.js(0.10.x)でFacebookログインを実装する

# 追記
Sails.jsを0.11系にすると動かなかった＋たぶん色々間違っていたのでコードを修正。0.11系から(?)MiddlewareでPasportを初期化しないと動かない。

``` config/http.js
module.exports.http = {
  middleware: {
    order: [
      'startRequestTimer',
      'cookieParser',
      'session',
      // Customized Start
      'passportInit',
      'passportSession',
      // Customized End
      'bodyParser',
      'handleBodyParserError',
      'compress',
      'methodOverride',
      'poweredBy',
      '$custom',
      'router',
      'www',
      'favicon',
      '404',
      '500'
    ],
    passportInit: require('passport').initialize(),
    passportSession: require('passport').session()
  }
}
```

そしてリクエストを受けるControllerはこんな感じ。

``` api/AuthController.js
/**
 * AuthController
 *
 * @description :: Server-side logic for managing auths
 * @help        :: See http://links.sailsjs.org/docs/controllers
 */

var passport = require('passport');
var FacebookStrategy = require('passport-facebook').Strategy;

passport.use(new FacebookStrategy({
	clientID: process.env.FACEBOOK_CLIENT_ID || 'xxxxx',
	clientSecret: process.env.FACEBOOK_CLIENT_SECRET || 'xxxxx',
	callbackURL: '/auth/facebook/callback'
},
function(accessToken, refreshToken, profile, done) {
	var id = profile.id;
	var username = profile.displayName;
	var iconPath = 'https://graph.facebook.com/' + id + '/picture?width=320&height=320';

	UserService.signInOrSignUpFacebook({
		facebookId: id,
		name: username,
		iconPath: iconPath,
		accessToken: accessToken
	}).then(function(user) {
		return done(null, user);
	}).catch(function(error) {
		sails.log.error(error);
		return done(error);
	});
}));

passport.serializeUser(function(user, done) {
	done(null, user.id);
});

passport.deserializeUser(function(userId, done) {
	User.findOne({id: userId}).then(function(user) {
		done(null, user);
	}).catch(function(error) {
		done(error);
	});
});


module.exports = {
	facebook: function(req, res, next) {
		passport.authenticate('facebook', {})(req, res);
	},
	facebookCallback: function(req, res, next) {
		passport.authenticate('facebook', {
			failureRedirect: '/',
		}, function(error, user) {
			if (error) {
				sails.log.error(error);
				res.redirect(500);
			}

			req.login(user, function(error) {
				res.redirect('/');
			});

		})(req, res);
	}
};
```

ユーザ情報の取得API

``` api/UserController.js
module.exports = {
	me: function(req, res) {
		var user = req.user;

		if (!user) {
			return res.send(400);
		}

		res.send(user);
	},
}
```

---

Sails.jsを使って、少しだけ拡張性を考えたFacebookログイン方法を書いています。

レポジトリにもコードを置いていますので、コチラも合わせて御覧ください。
https://github.com/KeitaMoromizato/sails-facebook-auth

[sails.js Advent Calendar 2014](http://qiita.com/advent-calendar/2014/sailsjs)、もうこれでネタ切れです...


## 注意
node.jsのバージョンが0.11系だと(今のところ)動きません。

## Facebook Appの準備
[Facebook Developer](https://developers.facebook.com/)にアクセス。[Apps] -> [Add a New App]からウェブサイトを選択する。アプリ名を選択して[Create New Facebook App ID]。カテゴリ選べとか言われるけど適当でいいです。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/52054/1d00b7c4-2e54-8204-50bb-dcc924e3b0a7.png)

今回はローカルで動作確認するので、サイトURLはlocalhostに。
(※URLはポート番号まで含めたもの[http://localhost:1337/]が必要でした。)
![2.png](https://qiita-image-store.s3.amazonaws.com/0/52054/f1b6a349-cf02-d99d-e8c9-ef19059e09d1.png)

次のステップで「ログイン」を選択。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/52054/e81f2dc5-2cf7-fc49-85bb-28dd75038da7.png)

[Apps]から先ほど作成したアプリケーションを選択。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/52054/3bb538cf-bfd2-95dd-23a8-ad80c7b8fc68.png)

App IDとApp Secretを入手します(Secretは[show]をクリックすると表示されます)。

![6.png](https://qiita-image-store.s3.amazonaws.com/0/52054/ece2938e-e414-87c4-bdac-328f3f460c71.png)

## 実装
Sails.jsアプリケーションを作成し、必要なモジュールをインストール。今回は[Passport.js](http://passportjs.org/)という認証を請け負ってくれるモジュールを使います。Passport.jsはStrategy(認証方法)を色々なものに切り替えることが出来ます。今回はFacebookAuthを使うので、passport-facebookも一緒にインストール。

```
$ sails new facebook-auth-test
$ cd facebook-auth-test
$ npm install passport --save
$ npm install passport-facebook --save
```

それぞれ必要なController/Modelを作成します。

```
$ sails generate api user
$ sails generate controller auth
$ sails generate controller page
```

### Facebookモジュール
ログイン用モジュールです。昨今のサービスだとFacebook/twitter/googleなど多様なサービスでログインすることが出来ますね。そのような拡張性も考え、Signinというモジュールにfacebookログイン用関数を書いています。

``` api/services/Signin.js
module.exports = (function(){
  var passport = require('passport');
  var FacebookStrategy = require('passport-facebook').Strategy;

  passport.use(new FacebookStrategy({
      clientID : "xxxxxxxxxxxxxxxxxxxxxxxx",
      clientSecret: "xxxxxxxxxxxxxxxxxx",
      callbackURL: "/auth/facebook/callback"
    },
    function(accessToken, refreshToken, profile, done) {

      var id = profile.id;
      var username = profile.displayName;
      var icon = 'https://graph.facebook.com/' + id + '/picture?width=320&height=320';

      User.signinOrSignup("facebook", id, {name: username, icon: icon}, function(err, user) {
        return done(err, user);
      });
    })
  );

  var facebook = function(req, res, callback) {
    passport.authenticate('facebook', {}, function(err, user) {
      if (user) {
        req.session.authenticated = true;
        req.session.user_id = user.id;
      }

      callback(err, user);
    })(req, res);
  };

  return {
    facebook: facebook
  };
})();
```

地味に重要なところがここ。認証済みフラグ。

```
req.session.authenticated = true;
```

### ログインAPI

ログイン用APIです。特に複雑なことはなく、ログインでエラーが発生したら/loginにリダイレクトし、成功した場合はルートに入る。

``` api/controllers/AuthController.js
module.exports = {
  facebook : function(req, res) {

    SignIn.facebook(req, res, function(err, user) {
      if (err) {
        res.redirect('/login');
      }
      else {
        res.redirect('/');
       }
    });
  },
  facebookCallback : function(req, res) {
    passport.authenticate('facebook', {
      successRedirect: '/',
      failureRedirect: '/login' }
    );
  }
};
```

### Userモデル
ユーザモデル。attributesのポイントとしては、認証情報(auth)を配列で持っている事。先ほども少し触れたように、将来的に別の方法でもログイン出来るようにと考えた結果こうなりました。`signinOrSignup()`は面倒なので登録/ログインを同時にしちゃった関数。

``` api/models/User.js
module.exports = {

  attributes: {
    auth: 'json',
    name: {
      type: 'string'
    },
    icon: {
      type: 'string'
    }
  },
  signinOrSignup : function(auth, id, data, callback) {
    var query = {};
    query["auth." + auth] = id;
    User.findOne(query).exec(function(err, user) {
      if (err) {
        return callback("ERROR, find User");
      }

      if (user) {
        return callback(null, user);
      }

      // SignUp
      data.auth = {};
      data.auth[auth] = id;
      User.create(data).exec(function(err, user) {

        if (err) {
          return callback("ERROR, create User");
        }

        return callback(null, user);
      });
    });
  }
};
```

### 公開/非公開設定
どのページ/APIを公開するかという設定はpolicies.jsに書きます。一番最初にワイルドカードですべて認証必須にし、その後にログイン無しでも公開するController/Actionについてtrueにしていきます。
ちなみに最初に設定している`sessionAuth`は、`api/policies/sessionAuth.js`です。つまり、APIごとに認証方法も変えられるというわけ。どこで使うかはわかりませんけど。

``` config/policies.js
module.exports.policies = {

  '*' : 'sessionAuth',

  PageController : {
    login : true
  },

  AuthController : {
    facebook : true
  }
};
```

### ページの表示
基本的にページを表示するだけなら実装は何もいらないはずですが、policiesによるフィルタをかけるばあい、Controllerを経由しないと正常に動きません。(Controllerを通さずに出来る方法あれば教えて下さい!)
なのでログインページとログイン後ページを表示するActionを追加。ついでにログイン後にはFacebookから取ってきたユーザ名とアイコンを表示しましょう。

``` api/controllers/PageController.js
module.exports = {
  login: function(req, res) {
    res.view('login');
  },
  index: function(req, res) {
    var userId = req.session.user_id;

    User.findOne(userId).exec(function(err, user) {
      if (err) res.send(500);

      res.view('index', {
        user: user
      });
    });
  }
};
```

### 後は細々したものを

``` config/routes.js
  'GET /': {
    controller : 'Page',
    action : 'index'
  },

  'GET /login' : {
    controller : 'Page',
    action : 'login'
  },
```

``` views/index.ejs
<div>
  <h1><%= user.name %></h1>
  <img src='<%= user.icon %>' />
</div>
```

``` views.login.ejs
<h2>Facebook Login</h2>
<a href="/auth/facebook">Login</a>
```

## 動かしてみる
まずはlocalhost/にアクセスしてみると...
なんか陽気なForbiddenが出ました。

![7.png](https://qiita-image-store.s3.amazonaws.com/0/52054/3c1347eb-d758-9d40-9f92-9b9b0a6bf0e0.png)

それならとlocalhost/loginから[Login]をクリックすると...

![8.png](https://qiita-image-store.s3.amazonaws.com/0/52054/8d13c5e5-b825-2b48-975f-1ed0136727d4.png)

Facebookに遷移して認証後、無事localhost/が表示されました!

![9.png](https://qiita-image-store.s3.amazonaws.com/0/52054/a142cddb-90f9-2a6d-fba0-a809d46bb72c.png)