简体   繁体   English

Google Cloud Endpoints的自定义身份验证(而非OAuth2)

[英]Custom Authentication for Google Cloud Endpoints (instead of OAuth2)

We are super excited about App Engine's support for Google Cloud Endpoints . 我们对App Engine对Google Cloud Endpoints的支持感到非常兴奋。

That said we don't use OAuth2 yet and usually authenticate users with username/password so we can support customers that don't have Google accounts. 也就是说我们还没有使用OAuth2,并且通常使用用户名/密码对用户进行身份验证,以便我们可以支持没有Google帐户的客户。

We want to migrate our API over to Google Cloud Endpoints because of all the benefits we then get for free (API Console, Client Libraries, robustness, …) but our main question is … 我们希望将API迁移到Google Cloud Endpoints,因为我们可以免费获得所有好处(API控制台,客户端库,健壮性......),但我们的主要问题是......

How to add custom authentication to cloud endpoints where we previously check for a valid user session + CSRF token in our existing API. 如何向我们之前在现有API中检查有效用户会话+ CSRF令牌的云端点添加自定义身份验证。

Is there an elegant way to do this without adding stuff like session information and CSRF tokens to the protoRPC messages? 有没有一种优雅的方法来实现这一点,而无需在protoRPC消息中添加会话信息和CSRF令牌等内容?

I'm using webapp2 Authentication system for my entire application. 我正在为我的整个应用程序使用webapp2身份验证系统。 So I tried to reuse this for Google Cloud Authentication and I get it! 所以我尝试将其重复用于Google Cloud身份验证,我就明白了!

webapp2_extras.auth uses webapp2_extras.sessions to store auth information. webapp2_extras.auth使用webapp2_extras.sessions存储身份验证信息。 And it this session could be stored in 3 different formats: securecookie, datastore or memcache. 此会话可以以3种不同的格式存储:securecookie,datastore或memcache。

Securecookie is the default format and which I'm using. Securecookie是我正在使用的默认格式。 I consider it secure enough as webapp2 auth system is used for a lot of GAE application running in production enviroment. 我认为它足够安全,因为webapp2 auth系统用于在生产环境中运行的许多GAE应用程序。

So I decode this securecookie and reuse it from GAE Endpoints. 所以我解码这个securecookie并从GAE端点重用它。 I don't know if this could generate some secure problem (I hope not) but maybe @bossylobster could say if it is ok looking at security side. 我不知道这是否会产生一些安全问题(我希望不会)但也许@bossylobster可以说是否可以看到安全方面。

My Api: 我的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)

From my understanding Google Cloud Endpoints provides a way to implement a (RESTful?) API and to generate a mobile client library. 根据我的理解,Google Cloud Endpoints提供了一种实现(RESTful?)API并生成移动客户端库的方法。 Authentication in this case would be OAuth2. 在这种情况下,身份验证将是OAuth2。 OAuth2 provides different 'flows', some of which support mobile clients. OAuth2提供不同的“流量”,其中一些支持移动客户端。 In the case of authentication using a principal and credentials (username and password) this doesn't seem like a good fit. 在使用委托人和凭证(用户名和密码)进行身份验证的情况下,这似乎不太适合。 I honestly think you would be better off by using OAuth2. 老实说,我认为使用OAuth2会更好。 Implementing a custom OAuth2 flow to support your case is an approach that could work but is very error prone. 实现自定义OAuth2流程以支持您的案例是一种可行的方法,但非常容易出错。 I haven't worked with OAuth2 yet but maybe an 'API key' can be created for a user so they can both use the front-end and the back-end through the use of mobile clients. 我还没有使用过OAuth2,但也许可以为用户创建“API密钥”,这样他们就可以通过使用移动客户端来使用前端和后端。

I wrote a custom python authentication library called Authtopus that may be of interest to anyone looking for a solution to this problem: https://github.com/rggibson/Authtopus 我写了一个名为Authtopus的自定义python身份验证库,对于寻找这个问题的解决方案的人来说可能会感兴趣: https//github.com/rggibson/Authtopus

Authtopus supports basic username and password registrations and logins, as well as social logins via Facebook or Google (more social providers could probably be added without too much hassle too). Authtopus支持基本的用户名和密码注册和登录,以及通过Facebook或Google进行社交登录(可能会添加更多社交提供商而不会有太多麻烦)。 User accounts are merged according to verified email addresses, so if a user first registers by username and password, then later uses a social login, and the verified email addresses of the accounts match up, then no separate User account is created. 用户帐户根据验证的电子邮件地址合并,因此如果用户首先通过用户名和密码进行注册,然后使用社交登录,并且经过验证的帐户电子邮件地址匹配,则不会创建单独的用户帐户。

you can used jwt for authentication. 你可以使用jwt进行身份验证。 Solutions here 方案在这里

I did not coded it yet, but it imagined next way: 我还没编码,但它想象的是下一个方式:

  1. When server receives login request it look up username/password in datastore. 当服务器收到登录请求时,它会在数据存储区中查找用户名/密码。 In case user not found server responds with some error object that contains appropriate message like "User doesn't exist" or like. 如果用户未找到服务器响应某些错误对象,其中包含“用户不存在”等相应的消息。 In case found it stored in FIFO kind of collection (cache) with limited size like 100 (or 1000 or 10000). 如果发现它存储在FIFO类型的集合(缓存)中,其大小有限,如100(或1000或10000)。

  2. On successful login request server returns to client sessionid like ";LKJLK345345LKJLKJSDF53KL". 成功登录请求服务器返回客户端sessionid,如“; LKJLK345345LKJLKJSDF53KL”。 Can be Base64 encoded username:password. 可以是Base64编码的用户名:密码。 Client stores it in Cookie named "authString" or "sessionid" (or something less eloquent) with 30 min (any) expiration. 客户端将其存储在名为“authString”或“sessionid”(或不那么雄辩的东西)的Cookie中,并且有30分钟(任意)到期。

  3. With each request after login client sends Autorization header that it takes from cookie. 登录后,每次请求客户端都会发送从cookie获取的Autorization标头。 Each time cookie taken, it renewed -- so it never expires while user active. 每次使用cookie时,它都会更新 - 因此在用户激活时它永远不会过期。

  4. On server side we will have AuthFilter that will check presence of Authorization header in each request (exclude login, signup, reset_password). 在服务器端,我们将使用AuthFilter来检查每个请求中是否存在Authorization标头(不包括login,signup,reset_password)。 If no such header found, filter returns response to client with status code 401 (client shows login screen to user). 如果找不到这样的标题,过滤器将返回对状态码为401的客户端的响应(客户端向用户显示登录屏幕)。 If header found filter first checks presence of user in the cache, after in datastore and if user found -- does nothing (request handled by appropriate method), not found -- 401. 如果头部找到过滤器首先检查缓存中用户的存在,在数据存储区之后并且如果用户找到 - 什么都不做(请求由适当的方法处理),找不到 - 401。

Above architecture allows to keep server stateless but still have auto disconnecting sessions. 以上架构允许保持服务器无状态但仍然具有自动断开连接的会话。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM