気が向いたらなんか書く

そんな感じです

mongoose-authでOAuth認証

今回はeveryauthという認証ロジックのラッパーライブラリではなく、サービス毎に纏めて対応した認証ライブラリを更にmongooseのプラグインとして使えるmongoose-authを使って認証させます(ややこしい)

ようするに個人的なメモです

用意するもの

  • node.js
  • mongoose
  • mongoose-auth

その前に

このmongoose-authは形式としてはeveryauthをmongooseから使うためのpluginという形式であるため、できれば前もってeveryauthの知識はある程度欲しいところです。

実際のところ、READMEにあるサンプルが動かせれば大体できます(おぃ)。ただREADMEに無い部分でわかりにくい部分があるのでそのへんをサンプルで書いてみます

Schemaの拡張

mongoose-authを使うとgithubならgithubモジュール、facebookならfacebookモジュールを使うので、予め用意されたUserSchemaをベースにDBに格納されます

mongoose-auth:lib/modules/github/schema.js
module.exports = {
    github: {
        id: Number
      , login: String
      , gravatarId: String
      , name: String
      , email: String
      , publicRepoCount: Number
      , publicGistCount: Number
      , followingCount: Number
      , followersCount: Number
      , company: String
      , blog: String
      , location: String
      , permission: String
      , createdAt: Date

      // Private data
      , totalPrivateRepoCount: Number
      , collaborators: Number
      , diskUsage: Number
      , ownedPrivateRepoCount: Number
      , privateGistCount: Number
      , plan: {
            name: String
          , collaborators: Number
          , space: Number
          , privateRepos: Number
        }
    }
  , 'github.type': String
};

このままでもまあ最低限必要な情報は確保されているんですが、ここに追加で属性追加したいなぁ、なんて思いますよね。

var mongoose = require('mongoose'),
    mongooseAuth = require('mongoose-auth'),
    Schema = mongoose.Schema,
    ObjectId = mongoose.Schema.ObjectId;

// 共通項目としてdisplayNameという属性を追加する
var UserSchema = new Schema({
    displayName: {type:String, default: null}
});

こうしておくと各モジュールのSchemaで追加した項目がmongoose経由で更新できるようになります。

findOrCreateUserの設定

例えば、普通に利用する場合は以下のようなソースで動きます

    var mongoose = require('mongoose')
      , Schema = mongoose.Schema
      , mongooseAuth = require('mongoose-auth');

    var UserSchema = new Schema({})
      , User;

    // STEP 1: Schema Decoration and Configuration for the Routing
    UserSchema.plugin(mongooseAuth, {
        // Here, we attach your User model to every module
        everymodule: {
          everyauth: {
              User: function () {
                return User;
              }
          }
        }
      , github: {
          everyauth: {
              myHostname: 'http://localhost:3000'
            , appId: 'YOUR APP ID HERE'
            , appSecret: 'YOUR APP SECRET HERE'
            , redirectPath: '/'
          }
        }
    });
...

これはREADMEからの抜粋ですが、UserSchemaに対するpluginとしてgithubの設定項目を記述するだけです。これは実際にはeveryauthのgithubモジュールに対する設定に直結してるだけです。everyauthではfindOrCreateUser関数を実装している訳ではありませんが、everyauthのOAuth2の実装部分でfindOrCreateUserを認証シーケンスのstepとして呼び出しています。

everyauth:lib/modules/oauth2.js
  .get('callbackPath',
       'the callback path that the 3rd party OAuth provider redirects to after an OAuth authorization result - e.g., "/auth/facebook/callback"')
    .step('getCode')
      .description('retrieves a verifier code from the url query')
      .accepts('req res')
      .promises('code')
      .canBreakTo('authCallbackErrorSteps')
    .step('getAccessToken')
      .accepts('code')
      .promises('accessToken extra')
    .step('fetchOAuthUser')
      .accepts('accessToken')
      .promises('oauthUser')
    .step('getSession')
      .accepts('req')
      .promises('session')
    .step('findOrCreateUser') //<- ココ
      //.optional()
      .accepts('session accessToken extra oauthUser')
      .promises('user')
    .step('compile')
      .accepts('accessToken extra oauthUser user')
      .promises('auth')
    .step('addToSession')
      .accepts('session auth')
      .promises(null)
    .step('sendResponse')
      .accepts('res')
      .promises(null)

これを利用してfindOrCreateUser関数を定義として実装してやると認証完了時に自動的にfindOrCreateUserを実行できます。所謂hook的な物です。mongoose-authではdefaultの定義として予めfindOrCreateUser関数が定義されています。

mongoose-auth:lib/modules/github/everyauth.js
// Defaults
module.exports = {
  findOrCreateUser: function (sess, accessTok, accessTokExtra, ghUser) {
    var promise = this.Promise()
      , self = this;
    // TODO Check user in session or request helper first
    //      e.g., req.user or sess.auth.userId
    this.User()().findOne({'github.id': ghUser.id}, function (err, foundUser) {
      if (foundUser)
        return promise.fulfill(foundUser);
      self.User()().createWithGithub(ghUser, accessTok, function (err, createdUser) {
        return promise.fulfill(createdUser);
      });
    });
    return promise;
  }
};

したがってこの挙動を変えたい場合はこいつをoverrideする定義を追加してやれば良い訳です。

UserSchema.plugin(mongooseAuth, {
    everymodule: {
        everyauth: {
            User: function() {
                return User;
            },
            handleLogout: function(req, res) {
                req.logout();
                res.writeHead(303, {'Location': this.logoutRedirectPath()});
                res.end();
            }
        }
    },
    github: {
        everyauth: {
            myHostname: conf.myHostname,
            appId: conf.github.oauth.appId,
            appSecret: conf.github.oauth.appSecret,
            scope: 'user,public_repo,repo,gist',
            redirectPath: '/',
            // ココから
            findOrCreateUser: function(session, accessTok, accessTokExtra, ghUser) {
                var promise = this.Promise(),
                    User = this.User()();
                // mongodbから該当するidの情報を検索
                User.findOne({'github.id': ghUser.id}, function(err, foundUser) {
                    if(err) return promise.fail(err);
                    if(foundUser) return promise.fulfill(foundUser);

                    // 無い場合はUserSchemaを元に作成
                    console.log('CREATE GITHUB USER');
                    User.createWithGithub(ghUser, accessTok, function(err, createUser) {
                        if(err) return promise.fail(err);
                        // Schemaで定義した追加属性に値をセット
                        createUser.displayName = ghUser.name;
                        // 保存
                        createUser.save(function(err, modUser) {
                            if(err) return promise.fail(err);
                            return promise.fulfill(modUser);
                        });
                    });
                });
                return promise;
            }
        }
    }
});

使い道としては、例えば認証後のaccessTokenをsessionに入れておくとか(いいか悪いかは別にして…)、別のschemaを更新したりまあいろいろ使い道はあるかもしれません。