Webサービスを作るとき、ユーザのログインパスワード情報を自前のDBを持たせずにGoogleやTwitterなどのOAuth認証に丸投げできれば楽ですし、何かと便利です。
そこで今回は、Express4とPassportを使った簡単な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 IDとClient Secretを作成します。
上記の画面では以下のとおりに情報を入力しClient IDとClient 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 件のコメント:
コメントを投稿