2013-06-06 14 views
55

Google Cloud EndpointsのApp Engineのサポートについて非常に興奮しています。Google Cloud Endpointsのカスタム認証(OAuth2ではなく)

まだOAuth2を使用しておらず、通常はユーザー名/パスワードが のユーザーを認証するため、Googleアカウントを持たない顧客をサポートできると述べています。

我々はすべての利点を私たちはその後、自由(APIコンソール、クライアントライブラリ、堅牢性、...)のために得るが、私たちの主な問題であるため、Googleクラウドエンドポイントに私たちのAPIを経由移行する...カスタムを追加する方法

既存のAPIの有効なユーザーセッション+ CSRFトークンを事前に確認しているクラウドエンドポイントへの認証。

セッション情報やCSRFトークンをprotoRPCメッセージに追加することなく、これを行うためのエレガントな方法はありますか?

+7

実際の回答では動作しますが、TL; DRでは、独自のアカウントを使用している場合は、OAuth 2.0を使用する場合は独自のOAuthトークンを作成する必要があります。 – bossylobster

+1

これについては、新しくtoshと@bossylobsterの何かがありますか?誰でもそれを成功させましたか? –

+0

今は何も新しいことはありませんが、私はここでこれを行う方法について少し詳しい情報を提供しましたが、あなたはすでにこれを知っていたと思います。 http://stackoverflow.com/questions/18716674/facebook-login-in-google-cloud-endpoints/18728482#18728482 – PaulR

答えて

16

私は自分のアプリケーション全体でwebapp2認証システムを使用しています。だから私はGoogle Cloud認証にこれを再利用しようとしました。

webapp2_extras.authは、webapp2_extras.sessionsを使用して認証情報を格納します。また、このセッションは、securecookie、datastore、またはmemcacheの3つの異なるフォーマットで格納できます。

Securecookieは私が使用しているデフォルトのフォーマットです。私はwebapp2 authシステムが多くのGAEアプリケーションを生産環境で実行するために使用されるので、十分に安全だと考えています。

私はこのsecurecookieをデコードし、GAEエンドポイントから再利用します。私はこれが安全な問題を生み出すことができるかどうかは分かりませんが(私は願っていません)、おそらく@bossylobsterはセキュリティ面を見ても大丈夫だと言うことができます。

マイアピ:

import Cookie 
import logging 
import endpoints 
import os 
from google.appengine.ext import ndb 
from protorpc import remote 
import time 
from webapp2_extras.sessions import SessionDict 
from web.frankcrm_api_messages import IdContactMsg, FullContactMsg, ContactList, SimpleResponseMsg 
from web.models import Contact, User 
from webapp2_extras import sessions, securecookie, auth 
import config 

__author__ = 'Douglas S. Correa' 

TOKEN_CONFIG = { 
    'token_max_age': 86400 * 7 * 3, 
    'token_new_age': 86400, 
    'token_cache_age': 3600, 
} 

SESSION_ATTRIBUTES = ['user_id', 'remember', 
         'token', 'token_ts', 'cache_ts'] 

SESSION_SECRET_KEY = '9C3155EFEEB9D9A66A22EDC16AEDA' 


@endpoints.api(name='frank', version='v1', 
       description='FrankCRM API') 
class FrankApi(remote.Service): 
    user = None 
    token = None 

    @classmethod 
    def get_user_from_cookie(cls): 
     serializer = securecookie.SecureCookieSerializer(SESSION_SECRET_KEY) 
     cookie_string = os.environ.get('HTTP_COOKIE') 
     cookie = Cookie.SimpleCookie() 
     cookie.load(cookie_string) 
     session = cookie['session'].value 
     session_name = cookie['session_name'].value 
     session_name_data = serializer.deserialize('session_name', session_name) 
     session_dict = SessionDict(cls, data=session_name_data, new=False) 

     if session_dict: 
      session_final = dict(zip(SESSION_ATTRIBUTES, session_dict.get('_user'))) 
      _user, _token = cls.validate_token(session_final.get('user_id'), session_final.get('token'), 
               token_ts=session_final.get('token_ts')) 
      cls.user = _user 
      cls.token = _token 

    @classmethod 
    def user_to_dict(cls, user): 
     """Returns a dictionary based on a user object. 

     Extra attributes to be retrieved must be set in this module's 
     configuration. 

     :param user: 
      User object: an instance the custom user model. 
     :returns: 
      A dictionary with user data. 
     """ 
     if not user: 
      return None 

     user_dict = dict((a, getattr(user, a)) for a in []) 
     user_dict['user_id'] = user.get_id() 
     return user_dict 

    @classmethod 
    def get_user_by_auth_token(cls, user_id, token): 
     """Returns a user dict based on user_id and auth token. 

     :param user_id: 
      User id. 
     :param token: 
      Authentication token. 
     :returns: 
      A tuple ``(user_dict, token_timestamp)``. Both values can be None. 
      The token timestamp will be None if the user is invalid or it 
      is valid but the token requires renewal. 
     """ 
     user, ts = User.get_by_auth_token(user_id, token) 
     return cls.user_to_dict(user), ts 

    @classmethod 
    def validate_token(cls, user_id, token, token_ts=None): 
     """Validates a token. 

     Tokens are random strings used to authenticate temporarily. They are 
     used to validate sessions or service requests. 

     :param user_id: 
      User id. 
     :param token: 
      Token to be checked. 
     :param token_ts: 
      Optional token timestamp used to pre-validate the token age. 
     :returns: 
      A tuple ``(user_dict, token)``. 
     """ 
     now = int(time.time()) 
     delete = token_ts and ((now - token_ts) > TOKEN_CONFIG['token_max_age']) 
     create = False 

     if not delete: 
      # Try to fetch the user. 
      user, ts = cls.get_user_by_auth_token(user_id, token) 
      if user: 
       # Now validate the real timestamp. 
       delete = (now - ts) > TOKEN_CONFIG['token_max_age'] 
       create = (now - ts) > TOKEN_CONFIG['token_new_age'] 

     if delete or create or not user: 
      if delete or create: 
       # Delete token from db. 
       User.delete_auth_token(user_id, token) 

       if delete: 
        user = None 

      token = None 

     return user, token 

    @endpoints.method(IdContactMsg, ContactList, 
         path='contact/list', http_method='GET', 
         name='contact.list') 
    def list_contacts(self, request): 

     self.get_user_from_cookie() 

     if not self.user: 
      raise endpoints.UnauthorizedException('Invalid token.') 

     model_list = Contact.query().fetch(20) 
     contact_list = [] 
     for contact in model_list: 
      contact_list.append(contact.to_full_contact_message()) 

     return ContactList(contact_list=contact_list) 

    @endpoints.method(FullContactMsg, IdContactMsg, 
         path='contact/add', http_method='POST', 
         name='contact.add') 
    def add_contact(self, request): 
     self.get_user_from_cookie() 

     if not self.user: 
      raise endpoints.UnauthorizedException('Invalid token.') 


     new_contact = Contact.put_from_message(request) 

     logging.info(new_contact.key.id()) 

     return IdContactMsg(id=new_contact.key.id()) 

    @endpoints.method(FullContactMsg, IdContactMsg, 
         path='contact/update', http_method='POST', 
         name='contact.update') 
    def update_contact(self, request): 
     self.get_user_from_cookie() 

     if not self.user: 
      raise endpoints.UnauthorizedException('Invalid token.') 


     new_contact = Contact.put_from_message(request) 

     logging.info(new_contact.key.id()) 

     return IdContactMsg(id=new_contact.key.id()) 

    @endpoints.method(IdContactMsg, SimpleResponseMsg, 
         path='contact/delete', http_method='POST', 
         name='contact.delete') 
    def delete_contact(self, request): 
     self.get_user_from_cookie() 

     if not self.user: 
      raise endpoints.UnauthorizedException('Invalid token.') 


     if request.id: 
      contact_to_delete_key = ndb.Key(Contact, request.id) 
      if contact_to_delete_key.get(): 
       contact_to_delete_key.delete() 
       return SimpleResponseMsg(success=True) 

     return SimpleResponseMsg(success=False) 


APPLICATION = endpoints.api_server([FrankApi], 
            restricted=False) 
+0

これはデータストアのセッションフォーマットでも機能しますか? – Korneel

+0

私はそう思っていますが、securecookieからではなく、データストアからセッションを取得する必要があります。私はそれを試みたが、私はデータストアセッションの作業を得ることができませんでした –

+0

私は問題は、(データストア形式)セッションにアクセスするためのオブジェクトを要求する必要があると思う。エンドポイントでは、Requestオブジェクトにアクセスすることはできません。 – Korneel

1

私の理解から、Google Cloud Endpointsは(RESTfulな)APIを実装し、モバイルクライアントライブラリを生成する方法を提供します。この場合の認証はOAuth2になります。 OAuth2はさまざまな「フロー」を提供し、その一部はモバイルクライアントをサポートします。 プリンシパルと資格情報(ユーザー名とパスワード)を使用した認証の場合、これは適切ではないようです。私は正直なところ、OAuth2を使う方が良いと思っています。 あなたのケースをサポートするカスタムOAuth2フローを実装することは、うまくいく可能性がありますが、エラーが発生しやすい方法です。 私はまだOAuth2を使っていませんが、モバイルクライアントを使ってフロントエンドとバックエンドの両方を使うことができるように、ユーザーのために「APIキー」を作成することができます。

+3

OAuth2には、常にユーザーにとって最も面倒な問題であるGoogleアカウントが必要です。 – John

0

認証にjwtを使用することができます。ソリューションhere

1

私はこの問題への解決策を探している人に興味があるかもしれAuthtopusというカスタムPythonの認証ライブラリを書いた:https://github.com/rggibson/Authtopus

Authtopusは、基本的なユーザー名とパスワードの登録とログインだけでなく、社会的なログインをサポートしていますFacebookやGoogleを介して(あまりにも多くの面倒なことがなくても、もっとソーシャルプロバイダーが追加される可能性があります)。ユーザーアカウントは、検証済みのメールアドレスに基づいてマージされるので、ユーザ名とパスワードによる最初の登録ユーザーは、その後の社会ログインを使用して、アカウントの検証、電子メールアドレスが一致した場合には、別途、ユーザーアカウントが作成されません。

+0

あなたはjava用のライブラリを提供できますか? – Harikrishnan

+0

私は大好きですが、すぐにそれをすぐに回避するつもりはありません。 – rggibson

+0

よろしいですか?私はライブラリを作ることができるように、いくつかのドキュメントかもしれません? – Harikrishnan

0

私はまだそれをコード化されたが、それは次の道を想像しませんでした:

  1. サーバーがデータストアにユーザ名/パスワードを調べるログイン要求を受信した場合。ユーザーが見つからない場合は、「ユーザーは存在しません」などの適切なメッセージを含むエラーオブジェクトで応答します。見つかった場合、それは100(または1000または10000)のような限られたサイズのFIFOの種類のコレクション(キャッシュ)に格納されます。

  2. 正常にログインすると、サーバーは "; LKJLK345345LKJLKJSDF53KL"のようにクライアントsessionidに戻ります。 Base64でエンコードされたusername:passwordにすることができます。 クライアントは、「authString」または「sessionid」(またはそれほど雄弁ではないもの)という名前のCookieに30分(任意)の有効期限を付けて保存します。

  3. ログイン後の各リクエストで、クライアントはCookieから取得するAutorizationヘッダーを送信します。 Cookieが取得されるたびに更新されるため、ユーザーがアクティブな間は期限切れになりません。

  4. サーバー側では、各リクエスト(Authorization、signup、reset_passwordの除外)にAuthorizationヘッダーの存在をチェックするAuthFilterがあります。このようなヘッダが見つからない場合、filterはクライアントにステータスコード401(クライアントがログイン画面をユーザに表示)で応答を返します。ヘッダの後、ユーザ見出さデータストアおよび場合、キャッシュ内のユーザのフィルタを最初にチェック存在を発見した場合は - (適切な方法により処理要求)が見つかりません何もない - アーキテクチャ上401

を維持することができませんサーバーはステートレスですが、自動切断セッションはまだあります。

関連する問題