【備忘録】pyramidでtwitter OAuth


pyramidでのtwitter OAuthに手探りで挑戦した記録

参考: http://peterhudec.github.io/authomatic/examples/pyramid-simple.html

# 準備

## twitterにアプリ登録
https://apps.twitter.com/
ここでアプリを登録し、Consumer Key, Consumer Secretを取得
（電話番号をtwitterに登録していない人は登録を要求されるので注意）


## [pyramidインストール](http://docs.pylonsproject.org/projects/pyramid/en/latest/quick_tutorial/requirements.html)

今回のバージョンは1.7a1

## プロジェクト作成

```bash
$ $VENV/bin/pcreate oauthtut
```

OAuthには[Authomatic](http://peterhudec.github.io/authomatic/index.html)を利用した

```diff:setup.py
@@ -17,6 +17,7 @@ requires = [                                                                                                                                                                                                                
     'transaction',                                                                                                                                                                                                                           
     'zope.sqlalchemy',                                                                                                                                                                                                                       
     'waitress',                                                                                                                                                                                                                              
+    'authomatic',                                                                                                                                                                                                                            
     ]                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                              
 tests_require = [
```

```bash
$ $VENV/bin/python setup.py develop
```

# メンバー用ページ作成

`/for_member`作成。後でログインログインした場合のみ見られるようにする。

```diff:oauthtut/routes.py
@@ -1,3 +1,4 @@                                                                                                                                                                                                                               
 def includeme(config):                                                                                                                                                                                                                       
     config.add_static_view('static', 'static', cache_max_age=3600)                                                                                                                                                                           
     config.add_route('home', '/')                                                                                                                                                                                                            
+    config.add_route('for_member', '/for_member')
```

```diff:oauthtut/routes.py
@@ -31,3 +31,10 @@ might be caused by one of the following things:                                                                                                                                                                            
 After you fix the problem, please restart the Pyramid application to                                                                                                                                                                         
 try it again.                                                                                                                                                                                                                                
 """                                                                                                                                                                                                                                          
+                                                                                                                                                                                                                                             
+@view_config(route_name='for_member')                                                                                                                                                                                   
+def for_member(request):                                                                                                                                                                                                                     
+    response = Response()                                                                                                                                                                                                                    
+    response.write(u'<h1>Hi {0}</h1>'.format(request.authenticated_userid))                                                                                                                                                                  
+                                                                                                                                                                                                                                             
+    return response
```

## サーバ起動

```base
$ $VENV/bin/pserve development --reload
```

この時点で`http://localhost:6543`にアクセスすると

```
Hi None
```

と表示される

# 認証の設定

pyramidに対して認証周りの設定を行うがこの時点ではAuthomaticは出てこず変更内容は[チュートリアル](http://docs.pylonsproject.org/projects/pyramid/en/latest/tutorials/wiki2/authorization.html)のそれに近い

```diff:oauthtut/models/__init__.py
@@ -3,15 +3,31 @@ from sqlalchemy.orm import sessionmaker                                                                                                                                                                                     
 from sqlalchemy.orm import configure_mappers                                                                                                                                                                                                 
 import zope.sqlalchemy                                                                                                                                                                                                                       
                                                                                                                                                                                                                                              
+from pyramid.authentication import AuthTktAuthenticationPolicy                                                                                                                                                                               
+from pyramid.authorization import ACLAuthorizationPolicy                                                                                                                                                                                     
+                                                                                                                                                                                                                                             
 # import or define all models here to ensure they are attached to the                                                                                                                                                                        
 # Base.metadata prior to any initialization routines                                                                                                                                                                                         
 from .mymodel import MyModel # flake8: noqa                                                                                                                                                                                                  
                                                                                                                                                                                                                                              
+from .security import groupfinder                                                                                                                                                                                                            
+                                                                                                                                                                                                                                             
 # run configure_mappers after defining all of the models to ensure                                                                                                                                                                           
 # all relationships can be setup                                                                                                                                                                                                             
 configure_mappers()                                                                                                                                                                                                                          
                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                              
+from pyramid.security import (                                                                                                                                                                                                               
+    Allow,                                                                                                                                                                                                                                   
+    Everyone,                                                                                                                                                                                                                                
+)                                                                                                                                                                                                                                            
+                                                                                                                                                                                                                                             
+class RootFactory(object):                                                                                                                                                                                                                   
+    __acl__ = [ (Allow, Everyone, 'guest'),                                                                                                                                                                                                  
+                (Allow, 'group:member', 'member')]                                                                                                                                                                                           
+    def __init__(self,request):                                                                                                                                                                                                              
+        pass                                                                                                                                                                                                                                 
+                                                                                                                                                                                                                                             
 def get_engine(settings, prefix='sqlalchemy.'):                                                                                                                                                                                              
     return engine_from_config(settings, prefix)                                                                                                                                                                                              
                                                                                                                                                                                                                                              
@@ -58,6 +74,15 @@ def includeme(config):                                                                                                                                                                                                     
     """                                                                                                                                                                                                                                      
     settings = config.get_settings()                                                                                                                                                                                                         
                                                                                                                                                                                                                                              
+    config.set_root_factory('oauthtut.models.RootFactory')                                                                                                                                                                                   
+                                                                                                                                                                                                                                             
+    authn_policy = AuthTktAuthenticationPolicy(                                                                                                                                                                                              
+        'sosecret', callback=groupfinder, hashalg='sha512')                                                                                                                                                                                  
+    authz_policy = ACLAuthorizationPolicy()                                                                                                                                                                                                  
+                                                                                                                                                                                                                                             
+    config.set_authentication_policy(authn_policy)                                                                                                                                                                                           
+    config.set_authorization_policy(authz_policy)                                                                                                                                                                                            
+                                                                                                                                                                                                                                             
     # use pyramid_tm to hook the transaction lifecycle to the request                                                                                                                                                                        
     config.include('pyramid_tm')
```

`AuthTktAuthenticationPolicy`生成時に指定されている`groupfinder`ではユーザーとパーミッションを紐付ける。今回はログインさえできていれば、`group:member`のパーミッションを与えている。

```py3:oauth/models/security.py
GROUPS = {'member':['group:member']}

def groupfinder(userid, request):
    if userid:
        return ['group:member']
    return []
```

`/for_member`をのパーミッションを設定する

```diff:oauthtut/views/default.py
@@ -32,7 +32,7 @@ After you fix the problem, please restart the Pyramid application to                                                                                                                                                        
 try it again.                                                                                                                                                                                                                                
 """                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                              
-@view_config(route_name='for_member')                                                                                                                                                                                                        
+@view_config(route_name='for_member', permission='member')                                                                                                                                                                                   
 def for_member(request):                                                                                                                                                                                                                     
     response = Response()                                                                                                                                                                                                                    
     response.write(u'<h1>Hi {0}</h1>'.format(request.authenticated_userid))
```

これでログインしないで`/for_member`にアクセスすると403となる

# ログイン/ログアウト処理追加

以下の変更で`/for_member`アクセス時にOAuthが走るようになる。

````diff:oauthtut/routes.py
@@ -2,3 +2,5 @@ def includeme(config):                                                                                                                                                                                                        
     config.add_static_view('static', 'static', cache_max_age=3600)                                                                                                                                                                           
     config.add_route('home', '/')                                                                                                                                                                                                            
     config.add_route('for_member', '/for_member')                                                                                                                                                                                            
+    config.add_route('login', '/login')                                                                                                                                                                                                      
+    config.add_route('logout', '/logout')
```

```py3:oauthtut/views/login.py
from pyramid.response import Response
from authomatic import Authomatic
from authomatic.adapters import WebObAdapter

from pyramid.httpexceptions import (
    HTTPFound,
    HTTPForbidden,
    HTTPBadGateway,
)

from pyramid.view import (
    view_config,
    forbidden_view_config,
)

from pyramid.security import (
    remember,
    forget,
)

from ..config import CONFIG

authomatic = Authomatic(config=CONFIG, secret='some random secret string')

@view_config(route_name='login')
@forbidden_view_config()
def login(request):
    response = Response()

    login_url = request.route_url('login')
    referrer = request.url
    came_from = request.params.get('came_from', referrer)

    result = authomatic.login(WebObAdapter(request, response), 'tw')

    if result:
        if result.error:
            return HTTPBadGateway()
        elif result.user:
            if not (result.user.name and result.user.id):
                result.user.update()

            headers = remember(request, result.user.id)
            return HTTPFound(location=came_from,
                             headers=headers)

    return response

@view_config(route_name='logout')
def logout(request):
    headers = forget(request)
    return HTTPFound(location = request.route_url('home'),
                     headers = headers)
```

```py3:oauthtut/config.py
from authomatic.providers import oauth1

CONFIG = {
    'tw': {
        'class_': oauth1.Twitter,

        'consumer_key': 'XXXXXXXXXXXXXXXXXXXXXX',
        'consumer_secret': 'XXXXXXXXXXXXXXXXXXXXXX',
    },
}
```