App Engineの Google Cloud Endpoints のサポートに非常に興奮しています。
とは言っても、私たちはまだOAuth2を使用しておらず、通常はユーザー名/パスワードでユーザーを認証するので、Googleアカウントを持たない顧客をサポートできます。
無料で利用できるすべてのメリット(APIコンソール、クライアントライブラリ、堅牢性など)があるため、APIをGoogle Cloud Endpointsに移行したいと思いますが、主な質問は…
既存のAPIで有効なユーザーセッション+ CSRFトークンを以前に確認したクラウドエンドポイントにカスタム認証を追加する方法。
セッション情報やCSRFトークンなどをprotoRPCメッセージに追加せずにこれを行うエレガントな方法はありますか?
アプリケーション全体にwebapp2認証システムを使用しています。これをGoogle Cloud Authenticationで再利用してみましたが、取得できました。
webapp2_extras.authは、webapp2_extras.sessionsを使用して認証情報を格納します。そして、このセッションは、securecookie、datastore、またはmemcacheの3つの異なる形式で保存できます。
Securecookieはデフォルトのフォーマットであり、私が使用しています。本番環境で実行されている多くのGAEアプリケーションでwebapp2 authシステムが使用されているので、十分に安全だと思います。
そこで、このSecureCookieをデコードして、GAEエンドポイントから再利用します。これが何らかの安全な問題を引き起こす可能性があるかどうかはわかりませんが(できません)、@ bossylobsterがセキュリティ面を見て大丈夫かどうかを言うことができます。
私のAPI:
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)
私の理解から、Google Cloud Endpointsは(RESTful?)APIを実装し、モバイルクライアントライブラリを生成する方法を提供します。この場合の認証はOAuth2です。 OAuth2はさまざまな「フロー」を提供し、その一部はモバイルクライアントをサポートしています。プリンシパルと資格情報(ユーザー名とパスワード)を使用した認証の場合、これは適切とは思われません。正直なところ、OAuth2を使用する方が良いと思います。ケースをサポートするためのカスタムOAuth2フローの実装は、機能する可能性のあるアプローチですが、エラーが発生しやすくなります。私はまだOAuth2を使用していませんが、ユーザー用に「APIキー」を作成して、モバイルクライアントを使用してフロントエンドとバックエンドの両方を使用できるようにすることができます。
私はカスタムpython Authtopusと呼ばれる認証ライブラリを作成しました。このライブラリは、この問題の解決策を探している人に興味があるかもしれません: https://github.com/rggibson/Authtopus =
Authtopusは、基本的なユーザー名とパスワードの登録とログイン、およびFacebookまたはGoogleを介したソーシャルログインをサポートします(あまり面倒なく、ソーシャルプロバイダーを追加することもできます)。ユーザーアカウントは確認済みのメールアドレスに従ってマージされるため、ユーザーが最初にユーザー名とパスワードで登録し、その後ソーシャルログインを使用し、アカウントの確認済みメールアドレスが一致する場合、個別のユーザーアカウントは作成されません。
私はまだそれをコーディングしていませんが、次の方法を想像しました:
サーバーがログイン要求を受信すると、データストアでユーザー名/パスワードを検索します。ユーザーが見つからない場合、サーバーは「ユーザーが存在しません」などの適切なメッセージを含むエラーオブジェクトで応答します。見つかった場合は、FIFOコレクション(キャッシュ)の種類に格納され、サイズは100(または1000または10000)などの制限があります。
ログイン要求が成功すると、サーバーは「; LKJLK345345LKJLKJSDF53KL」のようなクライアントセッションIDに戻ります。 Base64エンコードされたユーザー名:パスワードを使用できます。クライアントは、30分(任意)の有効期限で、「authString」または「sessionid」(またはあまり説得力のないもの)という名前のCookieにそれを保存します。
ログイン後の要求ごとに、クライアントはCookieから取得するAutorizationヘッダーを送信します。 Cookieが取得されるたびに更新されます。つまり、ユーザーがアクティブな間は期限切れになりません。
サーバー側では、各リクエストのAuthorizationヘッダーの存在をチェックするAuthFilterがあります(ログイン、サインアップ、reset_passwordを除く)。そのようなヘッダーが見つからない場合、フィルターはステータスコード401でクライアントに応答を返します(クライアントはユーザーにログイン画面を表示します)。ヘッダーが見つかった場合、フィルターは最初にキャッシュ内のユーザーの存在を確認し、その後データストアで確認します。ユーザーが見つかった場合は何もしません(リクエストは適切なメソッドで処理されます)、見つかりませんでした-401。
上記のアーキテクチャでは、サーバーをステートレスに保つことができますが、自動切断セッションがまだあります。