Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 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.

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