2015年5月6日水曜日

Express4+PassportでGoogle OAuth認証の簡単なサンプル



Webサービスを作るとき、ユーザのログインパスワード情報を自前のDBを持たせずにGoogleやTwitterなどのOAuth認証に丸投げできれば楽ですし、何かと便利です。
そこで今回は、Express4Passportを使った簡単なGoogle OAuth認証アプリのサンプルコードをご紹介したいと思います。

今回作るexpress4アプリのプロジェクト構成はこのようになります。
$ tree -I node_modules
.
├── app.js
├── bin
│   └── www
├── models
│   └── user.js
├── npm-debug.log
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   └── index.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

上記のプロジェクトを手作りするのは面倒なので、まずはツールを使ってExpressアプリの雛形を自動生成しましょう。プロジェクト名は passport-google-oauth-example としました。
$ express passport-google-oauth-example
$ cd passport-google-oauth-example

次に依存関係を追加します。
$ sudo npm install --save passport
$ sudo npm install --save express-session
$ sudo npm install --save mongoose
$ sudo npm install --save passport-google-oauth

出来上がったpackage.jsonはこのようになります。

package.json
{
  "name": "passport-google-oauth-example",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.12.0",
    "cookie-parser": "~1.3.4",
    "debug": "~2.1.1",
    "express": "~4.12.2",
    "jade": "~1.9.2",
    "morgan": "~1.5.1",
    "serve-favicon": "~2.2.0",
    "mongoose": "^4.0.2",
    "passport": "^0.2.1",
    "express-session": "^1.11.1",
    "passport-google-oauth": "^0.2.0"
  }
}

npmインストール
$ sudo npm install

npmインストールが終わったら、一旦 http://localhost:3000 を叩いてExpressアプリが起動していることを確認しておきます。

MongoDB起動


次にMongoDBの起動を確認します。
$ ps ax | grep mongod
 1053 ?        Ssl    0:18 /usr/bin/mongod --config /etc/mongodb.conf
↑ちゃんと起動していますね。
もしまだMongoDBが起動していなければ、こちらを参考に起動させておいてください。

Google Developers Console (GDC) で設定


Google OAuth認証を利用するためにはGoogle Developers Console(GDC)上でいくつか設定を行う必要があります。

まずはGDC上でClient IDClient Secretを作成します。



上記の画面では以下のとおりに情報を入力しClient IDClient Secretを取得しました。

アプリケーションの種類:
ウェブアプリケーション
承認済みの JavaScript 生成元:
http://localhost:3000
承認済みのリダイレクト URI:
http://localhost:3000/oauth2callback

そしてAPIと認証 > APIからGoogle+ APIの許可も出しておきます。
GDCの設定は以上です。

ではコードを書いていきましょう。

models


ユーザ情報を管理する User モデルを作成します。
$ mkdir models && touch models/user.js

models/user.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var User = new Schema({
  // User ID
  uid: {
    type: String,
    unique: true
  },
  // display name
  displayName: {
    type: String
  }
}, { 
  // define this collection's name explicitly
  collection: "users"
});

module.exports = mongoose.model('User', User);

app.js


app.js
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session');
var mongoose = require('mongoose');
var passport = require('passport');
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
var User = require('./models/user.js');

// API Access link for creating client ID and secret:
// https://code.google.com/apis/console/
var GOOGLE_CLIENT_ID = <YOUR_CLIENT_ID>;
var GOOGLE_CLIENT_SECRET = <YOUR_CLIENT_SECRET>;
var CALLBACK_URL = "http://localhost:3000/oauth2callback";

// mongoose
mongoose.connect('mongodb://localhost/passport-google-oauth-example');

// session serializer
passport.serializeUser(function(req, uid, done) {
  console.log("passport.serializeUser uid=", uid);
  // save userID into session
  done(null, uid);
});
// session deserializer
passport.deserializeUser(function(req, uid, done) {
  console.log("passport.deserializeUser uid=", uid);
  // get a user by uid from DB
  User.findOne({
    uid: uid
  }, function(err, user) {
    console.log("findOne: err:", err, "\nuser:", user);
    // Then pass "user" to req. It can be used as "req.user" on the next route
    done(null, user);
  });
});

// Use the GoogleStrategy within Passport.
//   Strategies in Passport require a `verify` function, which accept
//   credentials (in this case, an accessToken, refreshToken, and Google
//   profile), and invoke a callback with a user object.
passport.use(new GoogleStrategy({
  clientID: GOOGLE_CLIENT_ID,
  clientSecret: GOOGLE_CLIENT_SECRET,
  callbackURL: CALLBACK_URL
}, function(accessToken, refreshToken, profile, done) {

  process.nextTick(function() {
    var uid = profile.id;
    var displayName = profile.displayName;
    User.findOneAndUpdate({
      uid: uid
    }, {
      $set: {
        uid: uid,
        displayName: displayName
      }
    }, {
      upsert: true
    }, function(err, user) {
      console.log("findOneAndUpdate err:", err, "user: ", user);
      return done(null, uid);
    });
  }); // process.nextTick

}));

var routes = require('./routes/index');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: false
}));

// session & passport settings
app.use(cookieParser());
app.use(session({
  secret: "secret_secret",
  saveUninitialized: false,
  resave: false,
}));
app.use(passport.initialize());
app.use(passport.session());

app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});


module.exports = app;

※ session({ secret: "secret_secret",...}); の "secret_secret" はセッションを符号化するための鍵です。各自任意の文字列に変えておくことをおすすめします。

routes


routes/routes.js
var express = require('express');
var passport = require('passport');
var router = express.Router();

/* GET index */
router.get('/', function(req, res, next) {
  console.log("GET / req.user:", req.user);
  var displayName = "Anonymous";
  if (req.user)
    displayName = req.user.displayName;
  res.render('index', {
    title: "Google OAuth Test Page",
    displayName: displayName
  });
});

/* for Google OAuth link. */
router.get('/auth/google', passport.authenticate('google', {
    scope: ['https://www.googleapis.com/auth/plus.login']
  }),
  function(req, res) {} // this never gets called
);

/* for the callback of Google OAuth */
router.get('/oauth2callback', passport.authenticate('google', {
  successRedirect: '/',
  failureRedirect: '/login'
}));

/* You can GET this page after authenticated. */
router.get('/api',
  ensureAuthenticated,
  function(req, res) {
    res.json({
      message: "You are authenticated!"
    });
  }
);

/* logout */
router.get('/logout', function(req, res) {
  req.logout();
  res.redirect('/');
});

// check whether authenticated or not
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.sendStatus(401);
}

module.exports = router;

views


views/index.jade
extends layout

block content
  h1= title
  p Welcome to #{displayName}
  a(href='/auth/google') Google OAuth
  br
  a(href='/api') Authenticated member ONLY!
  br
  a(href='/logout') logout

これで完成です。
さっそく動かしてみましょう。

$ npm start

http://localhost:3000/ を叩くと以下の画面が表示されます。



まだGoogle OAuth認証がされていない状態なので画面には Anonymous と表示されています。この状態ではAuthenticated member ONLY!リンクをクリックしてもUnauthorizedが表示されるだけです。そこでGoogle OAuthリンクを押して認証画面へ移動してみましょう。



この画面で「認証する」ボタンを押してOAuth認証が完了すると、/oauth2callback(GDCで指定したコールバックURL)から/(トップページ)にリダイレクトされ、以下のように表示されます。Googleから貰ったprofile情報からdisplayNameを取得し画面に表示しています。



この後、再びAuthenticated member ONLY!リンクをクリックすればjsonメッセージを取得することができます。


OAuth認証時の動作


OAuth認証からトップページが表示されるまでの流れを簡単にまとめるとこのようになります。

OAuth認証画面を表示 (GET /auth/google)

ユーザが承認ボタン押下

new GoogleStrategy(...)
1. Googleプロファイル情報から profile.id = uid を取得
2. uidをキーにユーザ情報をDBに保存
3. uidを次の処理に渡す


passport.serializeUser(function(req, uid, done){...})
uidをセッションに格納

OAuth認証完了のコールバックURLにリダイレクト (GET /oauth2callback)

passport.deserializeUser(function(req, uid, done){...})
1. セッションからuid復元
2. uidを元にDBにからユーザ情報を取得


トップページにリダイレクト (GET /)

ユーザ情報を画面に表示

...以降は各ページが呼ばれる毎に、処理がroutesに移る直前で passport.deserializeUser(function(req, uid, done){...}) が実行されることになります。

こんな感じになります。

0 件のコメント:

コメントを投稿