谷歌云端点的自定义身份验证(而不是OAuth2)

时间:2023-02-05 23:09:43

We are super excited about App Engine's support for Google Cloud Endpoints.

我们对App Engine对谷歌云端点的支持感到非常兴奋。

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,并且通常使用用户名/密码对用户进行身份验证,以便我们能够支持没有谷歌帐户的客户。

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迁移到谷歌云端点,因为我们可以免费获得所有好处(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令牌,是否有一种优雅的方法来实现这一点?

5 个解决方案

#1


16  

I'm using webapp2 Authentication system for my entire application. So I tried to reuse this for Google Cloud Authentication and I get it!

我正在为整个应用程序使用webapp2身份验证系统。因此,我试图将它用于谷歌云身份验证,我得到了它!

webapp2_extras.auth uses webapp2_extras.sessions to store auth information. And it this session could be stored in 3 different formats: securecookie, datastore or memcache.

webapp2_extras。身份验证使用webapp2_extras。会话存储auth信息。这个会话可以以三种不同的格式存储:securecookie、datastore或memcache。

Securecookie is the default format and which I'm using. I consider it secure enough as webapp2 auth system is used for a lot of GAE application running in production enviroment.

Securecookie是我使用的默认格式。我认为它足够安全,因为webapp2 auth系统用于在生产环境中运行的许多GAE应用程序。

So I decode this securecookie and reuse it from GAE Endpoints. 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.

因此我解码了这个securecookie并从GAE端点重用它。我不知道这是否会产生一些安全问题(我希望不是),但是也许@ bossy龙虾会说,如果它可以看到安全方面。

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)

#2


1  

From my understanding Google Cloud Endpoints provides a way to implement a (RESTful?) API and to generate a mobile client library. Authentication in this case would be OAuth2. OAuth2 provides different 'flows', some of which support mobile clients. 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. Implementing a custom OAuth2 flow to support your case is an approach that could work but is very error prone. 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.

根据我的理解,谷歌云端点提供了一种实现(RESTful?)API并生成移动客户端库。在这种情况下,身份验证将是OAuth2。OAuth2提供不同的‘流’,其中一些支持移动客户端。在使用主体和凭证(用户名和密码)进行身份验证的情况下,这似乎不太合适。我真诚地认为你最好使用OAuth2。实现一个自定义OAuth2流来支持您的案例是一种可行的方法,但是很容易出错。我还没有使用OAuth2,但是也许可以为用户创建一个“API key”,这样他们就可以通过使用移动客户端来使用前端和后端。

#3


1  

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). 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.

Authtopus支持基本的用户名和密码注册和登录,以及通过Facebook或谷歌进行的社交登录(可能会添加更多的社交提供者,而不会带来太多麻烦)。用户帐户根据已验证的电子邮件地址进行合并,因此,如果用户首先按用户名和密码注册,然后使用社会登录,并且帐户的已验证电子邮件地址匹配,则不会创建单独的用户帐户。

#4


0  

you can used jwt for authentication. Solutions here

您可以使用jwt进行身份验证。这里的解决方案

#5


0  

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).

    当服务器收到登录请求时,它会在datastore中查找用户名/密码。如果用户未找到服务器,则使用包含“用户不存在”或类似消息的错误对象进行响应。如果发现它存储在FIFO类型的集合(缓存)中,大小有限,比如100(或1000或10000)。

  2. On successful login request server returns to client sessionid like ";LKJLK345345LKJLKJSDF53KL". Can be Base64 encoded username:password. Client stores it in Cookie named "authString" or "sessionid" (or something less eloquent) with 30 min (any) expiration.

    在成功的登录请求服务器上,返回到客户端sessionid like“;LKJLK345345LKJLKJSDF53KL”。可以是Base64编码的用户名:密码。客户端将其存储在名为“authString”或“sessionid”的Cookie中,有效期为30分钟。

  3. With each request after login client sends Autorization header that it takes from cookie. Each time cookie taken, it renewed -- so it never expires while user active.

    在登录后的每个请求中,客户端会发送它从cookie接收的自定义消息头。每次使用cookie时,它都会更新——因此在用户活动时它永远不会过期。

  4. On server side we will have AuthFilter that will check presence of Authorization header in each request (exclude login, signup, reset_password). If no such header found, filter returns response to client with status code 401 (client shows login screen to user). 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.

    在服务器端,我们将使用AuthFilter检查每个请求中是否存在授权头(排除登录、注册、reset_password)。如果没有找到这样的头,过滤器将用状态码401返回给客户端(客户端向用户显示登录屏幕)。如果header find filter首先检查缓存中用户的存在,然后在datastore中,如果用户发现——什么都不做(请求通过适当的方法处理),而不是找到——401。

Above architecture allows to keep server stateless but still have auto disconnecting sessions.

上面的架构允许保持服务器无状态,但仍然有自动断开会话。

#1


16  

I'm using webapp2 Authentication system for my entire application. So I tried to reuse this for Google Cloud Authentication and I get it!

我正在为整个应用程序使用webapp2身份验证系统。因此,我试图将它用于谷歌云身份验证,我得到了它!

webapp2_extras.auth uses webapp2_extras.sessions to store auth information. And it this session could be stored in 3 different formats: securecookie, datastore or memcache.

webapp2_extras。身份验证使用webapp2_extras。会话存储auth信息。这个会话可以以三种不同的格式存储:securecookie、datastore或memcache。

Securecookie is the default format and which I'm using. I consider it secure enough as webapp2 auth system is used for a lot of GAE application running in production enviroment.

Securecookie是我使用的默认格式。我认为它足够安全,因为webapp2 auth系统用于在生产环境中运行的许多GAE应用程序。

So I decode this securecookie and reuse it from GAE Endpoints. 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.

因此我解码了这个securecookie并从GAE端点重用它。我不知道这是否会产生一些安全问题(我希望不是),但是也许@ bossy龙虾会说,如果它可以看到安全方面。

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)

#2


1  

From my understanding Google Cloud Endpoints provides a way to implement a (RESTful?) API and to generate a mobile client library. Authentication in this case would be OAuth2. OAuth2 provides different 'flows', some of which support mobile clients. 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. Implementing a custom OAuth2 flow to support your case is an approach that could work but is very error prone. 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.

根据我的理解,谷歌云端点提供了一种实现(RESTful?)API并生成移动客户端库。在这种情况下,身份验证将是OAuth2。OAuth2提供不同的‘流’,其中一些支持移动客户端。在使用主体和凭证(用户名和密码)进行身份验证的情况下,这似乎不太合适。我真诚地认为你最好使用OAuth2。实现一个自定义OAuth2流来支持您的案例是一种可行的方法,但是很容易出错。我还没有使用OAuth2,但是也许可以为用户创建一个“API key”,这样他们就可以通过使用移动客户端来使用前端和后端。

#3


1  

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). 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.

Authtopus支持基本的用户名和密码注册和登录,以及通过Facebook或谷歌进行的社交登录(可能会添加更多的社交提供者,而不会带来太多麻烦)。用户帐户根据已验证的电子邮件地址进行合并,因此,如果用户首先按用户名和密码注册,然后使用社会登录,并且帐户的已验证电子邮件地址匹配,则不会创建单独的用户帐户。

#4


0  

you can used jwt for authentication. Solutions here

您可以使用jwt进行身份验证。这里的解决方案

#5


0  

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).

    当服务器收到登录请求时,它会在datastore中查找用户名/密码。如果用户未找到服务器,则使用包含“用户不存在”或类似消息的错误对象进行响应。如果发现它存储在FIFO类型的集合(缓存)中,大小有限,比如100(或1000或10000)。

  2. On successful login request server returns to client sessionid like ";LKJLK345345LKJLKJSDF53KL". Can be Base64 encoded username:password. Client stores it in Cookie named "authString" or "sessionid" (or something less eloquent) with 30 min (any) expiration.

    在成功的登录请求服务器上,返回到客户端sessionid like“;LKJLK345345LKJLKJSDF53KL”。可以是Base64编码的用户名:密码。客户端将其存储在名为“authString”或“sessionid”的Cookie中,有效期为30分钟。

  3. With each request after login client sends Autorization header that it takes from cookie. Each time cookie taken, it renewed -- so it never expires while user active.

    在登录后的每个请求中,客户端会发送它从cookie接收的自定义消息头。每次使用cookie时,它都会更新——因此在用户活动时它永远不会过期。

  4. On server side we will have AuthFilter that will check presence of Authorization header in each request (exclude login, signup, reset_password). If no such header found, filter returns response to client with status code 401 (client shows login screen to user). 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.

    在服务器端,我们将使用AuthFilter检查每个请求中是否存在授权头(排除登录、注册、reset_password)。如果没有找到这样的头,过滤器将用状态码401返回给客户端(客户端向用户显示登录屏幕)。如果header find filter首先检查缓存中用户的存在,然后在datastore中,如果用户发现——什么都不做(请求通过适当的方法处理),而不是找到——401。

Above architecture allows to keep server stateless but still have auto disconnecting sessions.

上面的架构允许保持服务器无状态,但仍然有自动断开会话。