RPXを使って認証を賢く簡単に

Cat in a bag.

  • OpenIDだとかFacebook認証だとか、おじさんにはどういう仕組になっているのだか全然わからないのだが、世の中どんどん進化している。
  • この手の認証サービスを活用することで、利用者もサービス提供側も幸せになれる。
  • 利用者は新たなID/Passwordの組合せを覚える必要がなくなる。
  • サービス提供側は実装も運用も軽くなる。ID/Passwordに絡む処理の多くを削れるし、漏洩防止のための運用も楽になる。

RPXを使った認証の流れ

  • (1) 利用者が自サイトのSign-inフォームを投函。投函先はRPXドメイン。 認証処理完了後に戻ってくるtoken-urlも渡す。
  • (2)to(4) 指定されたidentity providerに処理が飛ぶ。
  • (5) (1)で指定されたtoken_urlに処理が戻ってくる。POSTで渡される引数の中に、token変数が入っている。
  • (6) (5)で渡されたtoken変数を元に、OpenID認証データやプロファイルデータを引っ張る
  • (7) 認証終了。

RPXを使う準備

  • RPXを使うには、利用者登録をする必要がある。→登録
  • Basic Accountだと無料。ただし、使える機能は最低限。
  • 利用者登録したら、自分のアプリケーションも登録する必要あり。→アプリケーション登録
    • まずアプリケーション名称と、自アドレスを入力。アプリケーション名称はRPXの名前空間で一意となる。たぶん「test」みたいな単純名称はとられている。自アドレスは、例えばローカルでテストするのならlocalhost127.0.0.1を入れておけばよかろう。以下、another-testというアプリケーション名で説明。
<iframe src="http://another-test.rpxnow.com/openid/embed?token_url=http%3A%2F%2Flocalhost%2Ftoken_url" scrolling="no" frameBorder="no" allowtransparency="true" style="width:400px;height:240px"></iframe>
  • このコードを、認証フォームを表示したいページのテンプレートに貼り込めば良いのである。

Pylonsを使った実装例

  • Pylons+SimpleDB+OpenIDチュートリアル風味同様の簡単なアプリケーションを開発。
    • ブラウザで当該アプリケーションにアクセスすることで、そのサーバの時刻を知ることができる。
    • RPX使ってログインすることで、サーバとの時差を設定し、かつ、時差を計算した時刻を表示できる。
    • RPXを使ってログインした人は、ニックネームを設定できる。
    • 時差やニックネームは、サーバ側に保存でき、次回ログインした時に取り出せる。
  • コードはGithubにおいてある。
  • ただし、development.iniは自分のRPX Access Keyが入っているのでGithubには入れてない。下記コードを参照に各自設定されたし。
[app:main]
use = egg:RPXClock
full_stack = true
static_files = true

cache_dir = %(here)s/data
beaker.session.key = rpxclock
beaker.session.secret = somesecret

rpx_token = RPXサイトが発行するAPI Key(下図)を入力
sdb_user_openid_domain = SimpleDB上にデータを保存するドメイン名
  • 表示を担当するcontrollers/clock.py
import logging

from pylons import request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect_to

from rpxclock.lib.base import BaseController, render
import datetime

log = logging.getLogger(__name__)

from rpxclock.lib import helpers as h
from rpxclock.lib.auth import *
from rpxclock.lib.sdb import UserPropertySDB

import formencode
from pylons.decorators.rest import restrict
from pylons.decorators import validate

from rpxclock.model import UserProperty

class TimeDiffForm(formencode.Schema):
    allow_extra_fields = True
    filter_extra_fields = True
    nickname = formencode.validators.String(not_empty = True)
    time_diff = formencode.validators.Int(not_empty = True)

class ClockController(BaseController):
    def __init__(self):
        BaseController.__init__(self)
        self.ups = UserPropertySDB()
        
    def now(self):
        if get_identifier():
            return self._registered()
        else:
            return self._guest_now()

    def _registered(self):
        up = self.ups.find(session['identifier'])
        c.title = 'My Clock'

        if up:
            name = up.nickname
            time_diff = up.time_diff
        else:
            name = session['identifier']
            time_diff = 0
            
        c.heading = 'Welcome! %s' % name
        c.content = "Current time is %s" % str(datetime.datetime.now() + datetime.timedelta(0, 0, 0, 0, 0, time_diff))
            
        return render('/derived/clock/registered.html')

    def _guest_now(self):
        c.title = 'My Clock'
        c.heading = 'Welcome!'
        c.content = "Current time is %s" % str(datetime.datetime.now())
        return render('/derived/clock/guest.html')

    @require_login
    def customize(self):
        return render('/derived/clock/customize.html')

    @restrict('POST')
    @validate(schema = TimeDiffForm(), form = 'time_diff')
    @require_login
    def save(self):
        up = UserProperty(session['identifier'],
                          self.form_result['nickname'],
                          int(self.form_result['time_diff']))

        self.ups.save(up)
        redirect_to('now')
    • lib/auth.pyで定義されているget_identifier()を見ることで、RPXにログインしているかどうかがわかる。 (in now())
    • デコレータ@require_loginも、lib/auth.pyで定義されている。
  • lib/auth.py
from pylons.controllers.util import abort, redirect_to, url_for
from pylons import session
from decorator import decorator

def get_identifier():
    if 'identifier' in session:
        return session['identifier']
    else:
        return None

def require_login(func, *args, **kwargs):
    if get_identifier() is None:
        redirect_to(url_for(controller = 'clock', action = 'now'))
    return func(*args, **kwargs)
require_login = decorator(require_login)
    • get_identifier()はセッション変数中のidentifierエントリをみているだけ。
    • セッションにidentifierを入れるのは、controller/auth.py
  • controllers/auth.py
import logging

from pylons import config, request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect_to, url_for

from rpxclock.lib.base import BaseController, render

log = logging.getLogger(__name__)

import urllib2
import md5
import simplejson as json_

class AuthController(BaseController):
    def on_login(self):
        pass

    def on_logout(self):
        pass

    def logout(self):
        session['identifier'] = None
        del session['identifier']
        session.save()
        redirect_to(url_for(controller = "clock", action = "now"))
        
    def rpx_token_url(self, *args, **kargs):
        'token' in request.params or redirect_to(url_for(controller="clock", action="now"))
        token = request.params['token']

        # contact rpx for the details:
        url = "https://rpxnow.com/api/v2/auth_info?token=%s&apiKey=%s" % (token, config.get('rpx_token'))
        json = json_.loads(urllib2.urlopen(url).read())

        if(json['stat'] == "ok"):
            json = json["profile"]
            session['identifier'] = json['identifier']
            session.save()

        redirect_to(url_for(controller="clock", action="now"))
    • rpx_token_urlは、RPXでの処理が終わった後呼び出されるエントリ。
    • POSTに入ってくるtokenを取り出し、それを元にRPXからユーザプロファイル情報を引っ張る。
    • 得られた結果をjsonでパースし、profileの中からidentifierを取り出す。これが認証後のキーとなる。
    • sessionに入れて、時計表示のnowにリダイレクト。

画面例

  • 認証前


  • 認証後、ニックネームや時差情報をカストマイズした画面。


考察

  • AuthKitを使ったOpenID認証よりも、こっちのほうがすっきりしている。
  • しかも、FacebookTwitterAPIを使った認証も同一コードで完結する。
  • ただし、複数のOpenIDFacebookあるいはTwitterアカウントを束ねる機能は今回のコードでは実現できない。
  • 何か別のキー(例えばメールアドレス)の下に複数のOpenID/Facebook/Twitterアカウントを持たせるように自分で実装するか、RPXの有料アカウントを取得する必要あり。
  • 値段を考えれば後者が圧倒的に楽っぽい。