Пользовательская аутентификация для конечных точек Google Cloud (вместо OAuth2)

Мы очень рады поддержке App Engine для Облачных конечных точек Google.

Тем не менее, мы еще не используем OAuth2 и обычно аутентифицируем пользователей с именем пользователя/паролем поэтому мы можем поддерживать клиентов, у которых нет учетных записей Google.

Мы хотим перенести наш API на Google Cloud Endpoints из-за всех преимуществ, которые мы получаем бесплатно (API-консоль, клиентские библиотеки, надежность,...), но наш главный вопрос...

Как добавить пользовательскую аутентификацию к облачным оконечным точкам, где мы ранее проверяем действительный токен пользователя + токен CSRF в нашем существующем API.

Есть ли элегантный способ сделать это, не добавляя в сообщения protoRPC такие вещи, как информация о сеансе и токены CSRF?

Ответ 1

Я использую систему аутентификации webapp2 для всего моего приложения. Поэтому я попытался использовать его для Google Cloud Authentication, и я понял!

webapp2_extras.auth использует webapp2_extras.sessions для хранения информации auth. И этот сеанс может храниться в трех разных форматах: securecookie, datastore или memcache.

Securecookie - это формат по умолчанию и который я использую. Я считаю это достаточно безопасным, так как система webapp2 auth используется для большого количества приложений GAE, работающих в производственной среде.

Итак, я расшифровываю этот 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)

Ответ 2

Из моего понимания Google Cloud Endpoints предоставляет способ реализации API (RESTful?) и создания мобильной клиентской библиотеки. Аутентификация в этом случае будет OAuth2. OAuth2 предоставляет разные "потоки", некоторые из которых поддерживают мобильные клиенты. В случае аутентификации с использованием принципала и учетных данных (имя пользователя и пароль) это не похоже на хорошую подгонку. Я честно думаю, что вам будет лучше, используя OAuth2. Внедрение пользовательского потока OAuth2 для поддержки вашего дела - это подход, который может работать, но очень подвержен ошибкам. Я еще не работал с OAuth2, но, возможно, для пользователя может быть создан "API-ключ", чтобы они могли использовать интерфейс и внешний интерфейс с помощью мобильных клиентов.

Ответ 3

Я написал специальную библиотеку аутентификации python под названием Authtopus, которая может представлять интерес для всех, кто ищет решение этой проблемы: https://github.com/rggibson/Authtopus

Authtopus поддерживает основные регистрационные данные и логины для входа в систему, а также социальные вхождения в систему через Facebook или Google (возможно, дополнительные социальные провайдеры могут быть добавлены без излишних проблем). Учетные записи пользователей объединяются в соответствии с проверенными адресами электронной почты, поэтому, если пользователь сначала регистрируется по имени пользователя и паролю, а затем использует социальный вход, а проверенные адреса электронной почты учетных записей совпадают, то не создается отдельная учетная запись пользователя.

Ответ 4

вы можете использовать jwt для аутентификации. Решения здесь

Ответ 5

Я еще не закодировал его, но он представлял себе следующий путь:

  • Когда сервер получает запрос на вход, он ищет имя пользователя/пароль в хранилище данных. В случае, если пользователь не нашел сервер, отвечает какой-то объект ошибки, содержащий соответствующее сообщение типа "Пользователь не существует" или как. В случае обнаружения он хранится в виде коллекции FIFO (кеш) с ограниченным размером, например 100 (или 1000 или 10000).

  • При успешном завершении запроса сервер возвращается к клиентскому сеансу вроде "; LKJLK345345LKJLKJSDF53KL". Может быть закодированное имя Base64: пароль. Клиент хранит его в Cookie с именем "authString" или "sessionid" (или что-то менее красноречивое) с истечением 30 минут (любое).

  • С каждым запросом после входа клиент отправляет заголовок Autorization, который требуется для cookie. Каждый раз, когда cookie берется, он обновляется - поэтому он никогда не истекает, пока пользователь активен.

  • На стороне сервера у нас будет AuthFilter, который проверяет наличие заголовка авторизации в каждом запросе (исключая логин, регистрацию, reset_password). Если такой заголовок не найден, фильтр возвращает ответ клиенту с кодом состояния 401 (клиент показывает экран входа пользователю). Если фильтр, найденный заголовком, сначала проверяет наличие пользователя в кеше, после того, как в хранилище данных и если пользователь нашел - ничего не делает (запрос обрабатывается соответствующим методом), не найден - 401.

Над архитектурой позволяет сохранить сервер без гражданства, но все же есть сеансы автоматического отключения.