読者です 読者をやめる 読者になる 読者になる

Carpe Diem

備忘録。https://github.com/jun06t

Node.jsでFacebookのOAuth2.0認証

概要

FacebookのOAuth2.0認証を使うことがあったのでまとめました。
Node.jsなのでpassportというライブラリを使用します。

環境

  • Node.js 5.0.0
  • Express 4.13.1
  • npm 3.4.0

事前準備

以下のFacebookページでOAuth用のアプリを用意してください。

Facebook Developers

f:id:quoll00:20151107213024p:plain

こんな感じになります。
App Domainsには許可するURLドメインを追加していきます。
ここに登録されていないコールバックURLは許可されません。
これは下のサイトURLドメインと異なってはいけないです。
ただし異なるサブドメインの登録はできます。

例)
dev.example.com, local.example.com

なので1つのアプリで複数環境(dev, stg, prdなど)を扱うことは可能です。

実装

完成物

最初に今回の完成形を貼っておきます。

jun06t/oauth-facebook

基本的にExpressのスケルトンコードを利用します。

$ npm install -g express-generator
$ express oauth-facebook

これに以下のようなconfigmiddlewareなどを追加していきます。

フォルダツリー

完成物のフォルダ構造です。

.
├── app.js
├── bin
│   └── www
├── config
│   └── local.js
├── lib
│   └── passport.js
├── middleware
│   └── auth.js
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── routes
│   ├── auth.js
│   ├── index.js
│   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

各ファイルの説明

主な部分を簡単に説明します。

フォルダ、ファイル 説明
config facebookのclientIDなどを記述
lib passportの処理をまとめて記述
middleware 認証済みかどうかのチェック処理
routes/index ログインページ
routes/auth OAuthの処理。コールバック処理など
routes/users 認証済みの場合のみ開けるページ

package.json

今回使うsessionpassportに関するライブラリを追記します。

    "express-session": "*",
    "passport": "0.3.0",
    "passport-facebook": "2.0.0",

config/local.json

先ほどFacebookで作ったアプリのclientIDclientSecretを記述します。
callbackURLは先ほど登録したドメインのものにしましょう。
scopeには使用したいFacebookの情報の許可範囲を指定します。
この値はgoogle, twitter各サービスによって異なるのでそれぞれ調べましょう。
今回はログインだけなので特に要らないですが、書き方の例として以下のようなscopeをつけます。

'use strict';

let config = {};

config.oauth = {
  facebook: {
    clientID: 'your_client_id',
    clientSecret: 'your_client_secret',
    callbackURL: 'http://example.com:3000/auth/callback',
    scope: ['email', 'user_friends', 'user_birthday', 'user_location']
  }
}

module.exports = config;

lib/passport.js

最低限passportを使う上での処理を書きます。
引数のpassportapp.jsで渡します。
ここで渡すprofileは、認証後req.session.passportに保持されます。

'use strict';

const config = require('../config/local');
const FacebookStrategy = require('passport-facebook').Strategy;

let initPassport = function(passport) {
  passport.use(new FacebookStrategy(config.oauth.facebook, (accessToken, refreshToken, profile, done) => {
    // asynchronous verification, for effect...
    process.nextTick(() => {
      return done(null, profile);
    });
  }));

  passport.serializeUser((user, done) => {
    done(null, user);
  });

  passport.deserializeUser((obj, done) =>{
    done(null, obj);
  });
};

module.exports = initPassport;

app.js

以下の内容を追記します。
ライブラリ読み込み

const routes = require('./routes/index');
const auth = require('./routes/auth');
const users = require('./routes/users');
const session = require('express-session');
const passport = require('passport');
const authorized = require('./middleware/auth');

ミドルウェア登録

app.use(session({
  secret : 'cuaM6reezu7aechooLoh',
  resave : false,
  saveUninitialized : true,
}));
app.use(passport.initialize());
app.use(passport.session());

ルーティング /meページは認証済みでないと入れないようにmiddlewareをはさみます。

// passport
require('./lib/passport')(passport);

// routing
app.use('/', routes);
app.use('/auth', auth);
app.use('/me', authorized, users);

middleware/auth.js

req.isAuthenticated()というメソッドで認証済みか確認することができます。

'use strict';

let authorized = function(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/');
};

module.exports = authorized;

views/index.jade

ログインするためのリンクだけ追記します。

extends layout

block content
  h1= title
  p Welcome to #{title}
  a(href="/auth/") Sign In with Facebook

routes/auth.js

OAuth2.0はCSRF脆弱性があるので、stateパラメータで検証する必要があります。
先ほどreq.session.passportprofileが保持されると言いましたが、初回の/callbackではまだ認証が終わってないのでセッションには入ってません。
req.isAuthenticated()が通った状態でならセッションに入っています。

'use strict';

const express = require('express');
const crypto = require('crypto');
const passport = require('passport');

let router = express.Router();

router.get('/', (req, res, next) => {
  if (!req.session.state) {
    var current_date = (new Date()).valueOf().toString();
    var random = Math.random().toString();
    var hash = crypto.createHash('sha1').update(current_date + random).digest('hex');

    req.session.state = hash
  }

  passport.authenticate('facebook', {
    state: req.session.state
  })(req, res, next);
});

router.get('/callback', (req, res, next) => {
  if (!req.session.state) {
    return res.status(400).send({err: 'no state parameter'});
  }

  // CSRF verification
  if (req.query.state !== req.session.state) {
    return res.status(400).send({err: 'invalid state parameter'});
  }

  passport.authenticate('facebook', {
    failureRedirect: '/',
    successRedirect: '/me'
  })(req, res, next);
});

module.exports = router;

routes/users.js

ログインしたことが分かるように、titleを標準のExpressからOAuthに変更しておきます。

'use strict';

const express = require('express');
let router = express.Router();

/* GET users listing. */
router.get('/', (req, res, next) => {
  res.render('index', { title: 'OAuth' });
});

module.exports = router;

動作確認

http://example.com:3000/

へアクセスします。

f:id:quoll00:20151107225023p:plain

ログインリンクをクリックすると以下のFacebookの認可ページに飛ばされます。

f:id:quoll00:20151107225041p:plain

その際のリンクは以下のようになります。
redirect_uristateパラメータがちゃんと付いてますね。

https://www.facebook.com/v2.2/dialog/oauth?response_type=code&redirect_uri=http%3A%2F%2Fexample.com%3A3000%2Fauth%2Fcallback&scope=email%2Cuser_friends%2Cuser_birthday%2Cuser_location&state=a4207ca7680905d06fde96dcdec7d0ec6d220eef&client_id=your_client_id

問題無ければ以下のように/meページヘ飛ばされます。 f:id:quoll00:20151107225253p:plain

以上です。お疲れ様でした。

ソース