google application python google-app-engine google-cloud-endpoints

python - application - Autenticación personalizada para Google Cloud Endpoints(en lugar de OAuth2)



google application credentials (5)

Estamos muy entusiasmados con el soporte de App Engine para Google Cloud Endpoints .

Dicho esto, no usamos OAuth2 todavía y generalmente autenticamos a los usuarios con nombre de usuario / contraseña para poder ayudar a los clientes que no tienen cuentas de Google.

Queremos migrar nuestra API a Google Cloud Endpoints debido a todos los beneficios que obtenemos de forma gratuita (Consola API, Bibliotecas cliente, robustez, ...) pero nuestra principal pregunta es ...

Cómo agregar una autenticación personalizada a los puntos finales de la nube donde previamente verificamos una sesión de usuario válida + token CSRF en nuestra API existente.

¿Existe una manera elegante de hacer esto sin agregar cosas como información de sesión y tokens CSRF a los mensajes protoRPC?


Desde mi punto de vista, Google Cloud Endpoints ofrece una forma de implementar una API (RESTful?) Y generar una biblioteca de cliente móvil. La autenticación en este caso sería OAuth2. OAuth2 proporciona diferentes ''flujos'', algunos de los cuales admiten clientes móviles. En el caso de autenticación usando un principal y credenciales (nombre de usuario y contraseña), esto no parece una buena opción. Honestamente creo que estarías mejor usando OAuth2. La implementación de un flujo personalizado de OAuth2 para respaldar su caso es un enfoque que podría funcionar pero es muy propenso a errores. Todavía no he trabajado con OAuth2, pero tal vez se pueda crear una ''clave de API'' para un usuario, de modo que ambos puedan usar el front-end y el back-end mediante el uso de clientes móviles.


Escribí una biblioteca de autenticación de python personalizada llamada Authtopus que puede ser de interés para cualquiera que busque una solución a este problema: https://github.com/rggibson/Authtopus

Authtopus admite registros e inicios de sesión básicos de nombre de usuario y contraseña, así como inicios de sesión sociales a través de Facebook o Google (probablemente también se podrían agregar más proveedores sociales sin demasiada molestia). Las cuentas de usuario se fusionan según direcciones de correo electrónico verificadas, por lo que si un usuario se registra primero por nombre de usuario y contraseña, luego utiliza un inicio de sesión social y las direcciones de correo electrónico verificadas coinciden, entonces no se crea una cuenta de usuario separada.


Estoy usando el sistema de autenticación webapp2 para toda mi aplicación. ¡Intenté reutilizar esto para Google Cloud Authentication y lo conseguí!

webapp2_extras.auth usa webapp2_extras.sessions para almacenar información de autenticación. Y esta sesión podría almacenarse en 3 formatos diferentes: securecookie, datastore o memcache.

Securecookie es el formato predeterminado y que estoy usando. Lo considero lo suficientemente seguro ya que el sistema de autenticación webapp2 se utiliza para una gran cantidad de aplicaciones GAE que se ejecutan en el entorno de producción.

Así que decodifico esta cookie segura y la vuelvo a usar de GAE Endpoints. No sé si esto podría generar algún problema de seguridad (espero que no), pero tal vez @bossylobster podría decir si está bien mirando el lado de la seguridad.

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


No lo codifiqué todavía, pero lo imaginé de la siguiente manera:

  1. Cuando el servidor recibe la solicitud de inicio de sesión, busca el nombre de usuario / contraseña en el almacén de datos. En caso de que el servidor no encontrado del usuario responda con algún objeto de error que contenga un mensaje apropiado como "El usuario no existe" o algo así. En caso de que se encuentre almacenado en un tipo de colección FIFO (caché) con un tamaño limitado como 100 (o 1000 o 10000).

  2. En el servidor de solicitud de inicio de sesión exitoso regresa al sessionid del cliente como "; LKJLK345345LKJLKJSDF53KL". Puede ser nombre de usuario codificado Base64: contraseña. El cliente lo almacena en una cookie llamada "authString" o "sessionid" (o algo menos elocuente) con una caducidad de 30 minutos (cualquiera).

  3. Con cada solicitud después de iniciar sesión, el cliente envía el encabezado de Autorización que toma de la cookie. Cada vez que se toma una cookie, se renueva, por lo que nunca caduca mientras el usuario está activo.

  4. En el lado del servidor tendremos AuthFilter que verificará la presencia del encabezado Authorization en cada solicitud (excluir inicio de sesión, registro, reset_password). Si no se encuentra dicho encabezado, el filtro devuelve la respuesta al cliente con el código de estado 401 (el cliente muestra la pantalla de inicio de sesión al usuario). Si el filtro de encabezado encontrado comprueba primero la presencia del usuario en la memoria caché, después en el almacén de datos y si el usuario encuentra - no hace nada (solicitud manejada por el método apropiado), no se encuentra - 401.

La arquitectura anterior permite mantener el servidor sin estado pero aún tener sesiones de autodesconexión.


puede usar jwt para la autenticación. Soluciones here