node.js+socket.io+oauth+SessionWebSocketでログイン付きチャットを作るメモ

(※2011/09月 追記: この記事の方法は既に古いので Socket.IOとHTTPセッションの共有は Socket.IO と Express でセッションの共有 - Block Rockin’ Codes などを参考にしましょう)


node.jsでchatアプリっぽいもの作るメモ - すぎゃーんメモ
node.js+socket.ioでライブコーディング的なものを作るメモ - すぎゃーんメモ
に引き続き、第3弾。


現在、サンプルを以下の場所で動かしています。
http://www1216u.sakura.ne.jp:3001/
ソースは GitHub - sugyan/node-oauth-chat: node.js + socket.io chat (using Twitter OAuth) に置いてあります。

セッション共有

Socket.IOが便利なのは分かったのだけど、HTTPでのログイン情報などsession情報を使いたいこともありますね。
Google グループ ではexpressのdynamicHelpersを使うととれるよ、みたいなことが書いてあるようなんですが、試してみたところ少なくとも僕の環境ではうまくいきませんでした。
ということでSessionWebSocketを使います。SessionWebSocketについては以下の記事がとても詳しいです。
Node.js 日本ユーザグループ Blog: HTTP と WebSocket でセッションを共有する
ここにも書いてある通り、どうも挙動がおかしいようで。少なくとも現在の0.1.1ではsecureイベントのあとmessageが渡りません。のでpull requestで放置されてしまっている(?)このパッチを入れて使いました。

仕組みとしては、connnectする前にAjaxでhttpリクエストを行い、サーバー側のミドルウェアでそのhttp requestからsessionを取得して管理するようにする、ということのようです。クライアント側では、

function SessionWebSocket(cb) {
    $.ajax({
        url: "/",
        dataType: "json",
        cache : false,
        beforeSend: function(xhr) {
            xhr.setRequestHeader("x-access-request-token", "simple");
        },
        success: function(data) {
            var socket = new io.Socket();
            socket.connect();
            socket.send(data["x-access-token"].split(";")[0]);
            cb(socket);
        }
    });
}

のようなものを用意しておき、

SessionWebSocket(function(socket) {
    socket.on('message', function(msg) {
        ...
    });
})

という形でコールバックを渡すようにして使います。
サーバー側は、expressなら

var express = require('express');
var app     = express.createServer();
var io      = require('socket.io');
var sws     = require('SessionWebSocket')();

app.configure(function() {
    app.use(express.cookieDecoder());
    app.use(express.session());
    app.use(sws.http);
});

...

var socket = io.listen(app);
socket.on('connection', sws.ws(
    function(client) {
        client.on('secure', function() {
            console.log(client.session);
        });
    }
));

というかたちで設定して使うことで、socket.ioのコールバックに渡されるclientからclient.sessionでhttpのrequest sessionにアクセスできるようになるみたいです。

ログイン

ログインの仕組みは色々な実装方法が考えられると思いますが、今回はTwitter OAuthを使う方法を試してみました。
npmに"connect-auth"というのがあってTwitterだけでなくGithubやFacebookや色々なものに対応してログインする仕組みが用意されているようだったんですが、いまいち使いこなせずよく分からなかったので"oauth"だけつかって自前で実装することにしました。
oauthでのログインの実装は下記の記事がとても参考になりました。
node.js/express で CouchDB をパワーアップ大作戦 - Web屋の人の日記 || WebJourney 開発ログ

...
    var oauth = new (require('oauth').OAuth)(
        'https://api.twitter.com/oauth/request_token',
        'https://api.twitter.com/oauth/access_token',
        '***********************************', // consumer key
        '***********************************', // consumer secret
        '1.0',
        'http://localhost:3000/signin/twitter', // callback URL
        'HMAC-SHA1'
    );

    app.get('/signin/twitter', function(req, res) {
        var oauth_token    = req.query.oauth_token;
        var oauth_verifier = req.query.oauth_verifier;
        if (oauth_token && oauth_verifier && req.session.oauth) {
            oauth.getOAuthAccessToken(
                oauth_token, null, oauth_verifier,
                function(error, oauth_access_token, oauth_access_token_secret, results) {
                    if (error) {
                        res.send(error, 500);
                    } else {
                        req.session.user = results.screen_name;
                        res.redirect('/');
                    }
                }
            );
        } else {
            oauth.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results) {
                if (error) {
                    res.send(error, 500);
                } else {
                    req.session.oauth = {
                        oauth_token: oauth_token,
                        oauth_token_secret: oauth_token_secret,
                        request_token_results: results
                    };
                    res.redirect('https://api.twitter.com/oauth/authorize?oauth_token=' + oauth_token);
                }
            });
        }
    });
...

普通のリクエストならtokenを生成して認証ページへリダイレクトさせてやり、callbackで返って来たときはoauth_token, oauth_verifierを使って認証処理を行う、正しくユーザー情報が取得できればsessionにセットして他のページに飛ばしてやる、という流れです。

まとめ

この2つを組み合わせて冒頭のサンプルを作りました。未ログインのユーザーはsocket.ioのsessionIdを、Twitterでログインしているユーザーはそのscreen_nameを使ってチャットに書き込めるようにしています。
チャットの仕組みは前々回書いた通り。今回はログをメモリ上に100件まで保持しておいてconnect時に一気に流すようにしてみています。

次に試したいこと

複数のSocket.IOチャネルを使うような方法がまだよくわかっていないので、例えば「ライブコーディングしながら 横ではテキストチャットも動いている」みたいなのをどう実装するのか調べて、やってみようと思います。