13.02.27
バグの発生箇所を掴んだ。OpenIDサーバーだった。開発環境ではエラーが出なくて本番環境でのみ出現するのでサーバーを疑っていたのだけど違った。
本番環境でデバッグログ出したら
Error attempting to use stored discovery information: Claimed ID does not match (different subjects!) Expected <wrong-openid> got <right-openid> Attempting discovery to verify endopoint Performing discovery on <wrong-openid>
というメッセージが出ていた。
OpenIDサーバーから戻ってきた所でおかしくなっているようだ。referrer出力するようにしたらOpenIDサーバーが認証して戻すところで入れ替わっていた。
なんでそうなるのかワケがわからないが、OpenIDサーバーを再起動したらとりあえずは治った。ploneでも似たような症状を見つけたが、Flaskのせいではないということが分かったので一安心。
仕様をきちんと押さえておくか、本を読んでおくかしておいたほうがいいけど、とりあえずはいいや。
13.02.26
初回のログイン時じゃなくて、クッキーがエクスパイアしたあとにアクセスしてもセッションの入れ替わりが起こるみたいだ。スレッドかなぁと思ったけど、帰りの車のなかでよく考えてみたらやっぱ違うよなぁと思った。
セッションのデータがサーバー側に残っていてクッキーが送られないときにそのデータが使われているんじゃないかなぁ。
sessionをよく読んでみよう。
13.02.25
なおってなかった。
open-idサーバーで認証するところまではちゃんとユーザーが認識されているんだけど、アプリケーションにリダイレクトされたところで、ユーザーのセッションが違うユーザーのものに入れ替わってしまうというパターン。
- バージョン0.8でも0.9でも起きる
- 開発環境では起きないが、本番環境(Gunicorn)で起きる
- IE8でのみ確認されている
多分ブラウザは関係ないと思う。
@app.before_requestにセッションが残っているっぽいので@oid.loginhandlerにアクセスする時に@app.before_requestのユーザーをチェックする処理をとばしてログイン処理をすればいいような気がしてきたのであとでやってみる。
Flask-OpenIDでログインするとなぜか初回に別アカウントになってしまい、一度ログアウトしてから再ログインすると正常にログインできるというよくわからないバグが頻発していて悩んでいた。しかもdevelopmentでは全く再現できず、production(Flask+Gunicorn+Supervisord)でしか起こらないので厄介だった。
Flaskのバージョンを0.8から0.9にあげてみたけどなおらなかったので、調べたら既に報告されていた。
最初のスレッドは、「ユーザー付きあわせて違っていたらセッション破棄するぜ」みたいな解決方法だったのでちょっと使えなかったが、二番目のスレッドでsessionインターフェースを自前で用意したらなおったという報告があったので試してみた。
Flask-OpenIDでこのスニペットをそのまま利用するとopenid.yadis.manager.YadisServiceManagerがJSONシリアライズできないというエラーが出た。これはpython-openidに由来する問題らしく、コメントで指摘されているようにpickleでdumpすればsave_sessionはできるようになる。
しかし、今度はopen_sessionでコケる。managerがstrだ云々っていうエラーなのでpickle.loadsがうまくいってないようだ。Flask-OpenIDのソース読むとSessionWrapperで' p'っていうkeyのDictionaryを作っているので、Sessionのほうもそのようにしてみた。
Better Client-side sessionsをちょっと変えたらうまくいった。
from werkzeug.datastructures import CallbackDict from flask.sessions import SessionInterface, SessionMixin from itsdangerous import URLSafeTimedSerializer, BadSignature import pickle class ItsdangerousSession(CallbackDict, SessionMixin): def __init__(self, initial=None): def on_update(self): self.modified = True CallbackDict.__init__(self, initial, on_update) self.modified = False class ItsdangerousSessionInterface(SessionInterface): salt = 'cookie-session' session_class = ItsdangerousSession def get_serializer(self, app): if not app.secret_key: return None return URLSafeTimedSerializer(app.secret_key, salt=self.salt) def open_session(self, app, request): s = self.get_serializer(app) if s is None: return None val = request.cookies.get(app.session_cookie_name) if not val: return self.session_class() max_age = app.permanent_session_lifetime.total_seconds() try: data = s.loads(val, max_age=max_age) data = data[' p'] for k in data: if k.startswith('yoc') or k.startswith('lt')\ or k.startswith('_openid_consumer_last_token')\ or k.startswith('_yadis_services__openid_consumer_'): data[k] = pickle.loads(data[k]) return self.session_class(data) except BadSignature: return self.session_class() def save_session(self, app, session, response): domain = self.get_cookie_domain(app) if not session: if session.modified: response.delete_cookie(app.session_cookie_name, domain=domain) return expires = self.get_expiration_time(app, session) d = {} for k in session: if k.startswith('yoc') or k.startswith('lt')\ or k.startswith('_openid_consumer_last_token')\ or k.startswith('_yadis_services__openid_consumer_'): d[k] = pickle.dumps(session[k]) else: d[k] = session[k] val = self.get_serializer(app).dumps({' p': d}) response.set_cookie(app.session_cookie_name, val, expires=expires, httponly=True, domain=domain)