Nous sommes super enthousiastes sur la prise en charge de l'App moteur pour Google Cloud Endpoints .
Cela dit que nous n'utilisons pas encore Oauth2 et authentifiez généralement les utilisateurs avec nom d'utilisateur/mot de passe afin que nous puissions soutenir les clients qui n'ont pas de comptes Google.
Nous voulons migrer notre API sur Google Cloud Endpoints en raison de tous les avantages que nous obtenons gratuitement (console API, bibliothèques clientes, robustesse, ...) mais notre question principale est ...
Comment ajouter une authentification personnalisée aux points d'extrémité du cloud où nous recherchons précédemment une session utilisateur valide + jeton CSRF dans notre API existante.
Existe-t-il un moyen élégant de le faire sans ajouter de choses comme des informations de session et des jetons CSRF aux messages de Protorpc?
J'utilise le système d'authentification WebApp2 pour toute mon application. J'ai donc essayé de réutiliser cela pour l'authentification de Google Cloud et je l'obtiens!
webapp2_extras.auth utilise webapp2_extras.sesstions pour stocker les informations d'autorisation. Et cette session pourrait être stockée dans 3 formats différents: SecureCookie, DataStore ou Memcache.
SecureCookie est le format par défaut et que j'utilise. Je considère qu'il est suffisamment sécurisé lorsque le système d'authentification WebApp2 est utilisé pour beaucoup d'applications GAE en cours d'exécution dans la production Enviroment.
Je découle donc cette sécurité et réutilisez-la des points finaux GAE. Je ne sais pas si cela pourrait générer un problème sûr (j'espère que non), mais peut-être que @bossylobster pourrait dire si cela va bien regarder la sécurité.
Mon 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)
De ma compréhension, Google Cloud Endpoints Fournit un moyen de mettre en œuvre une API (reposante?) Et pour générer une bibliothèque client mobile. L'authentification dans ce cas serait oauth2. OAUTH2 fournit des "flux" différents, dont certains soutiennent les clients mobiles. Dans le cas de l'authentification utilisant une principale et des informations d'identification (nom d'utilisateur et mot de passe), cela ne semble pas être un bon ajustement. Honnêtement, je pense que vous seriez mieux en utilisant OAuth2. La mise en œuvre d'un flux OAUTH2 personnalisé pour soutenir votre cas est une approche qui pourrait fonctionner mais est sujette très erronée. Je n'ai pas encore travaillé avec Oauth2, mais peut-être qu'une "clé API" peut être créée pour un utilisateur afin de pouvoir utiliser le front-end et le back-end par l'utilisation de clients mobiles.
J'ai écrit une personne personnalisée python Bibliothèque d'authentification appelée Authtopus qui peut intéresser toute personne à la recherche d'une solution à ce problème: https://github.com/rggibson/authtopus Englisons
AutoTopus prend en charge les inscriptions et les enregistrements de mot de passe de base, ainsi que les connexions sociales via Facebook ou Google (plus de fournisseurs sociaux pourraient probablement être ajoutés sans trop de tracas). Les comptes d'utilisateurs sont fusionnés en fonction des adresses électroniques vérifiées, de sorte que si un utilisateur enregistre d'abord le nom d'utilisateur et le mot de passe, puis utilise ultérieurement une connexion sociale et les adresses électroniques vérifiées des comptes correspondent, aucun compte d'utilisateur distinct n'est créé.
Je ne l'ai pas encore codé, mais elle imaginait la prochaine façon:
Lorsque Server reçoit la connexion, demandez-la, il recherchez Nom d'utilisateur/mot de passe dans DataStore. Dans le cas de l'utilisateur non trouvé, le serveur répond avec un objet d'erreur contenant un message approprié comme "l'utilisateur n'existe pas" ou comme. Dans le cas où il a été trouvé stocké dans FIFO type de collection (cache) avec une taille limitée telle que 100 (ou 1000 ou 10000).
Sur le succès de la demande de connexion Server retourne à la session client comme "; lkjlk345345lkjlkjsdf53kl". Peut être Nom d'utilisateur codé de base64: mot de passe. Le client le stocke dans le cookie nommé "authtstring" ou "sessionID" (ou quelque chose de moins éloquent) avec une expiration de 30 min (toute).
Avec chaque demande après que le client de connexion envoie une en-tête d'autorisation qu'il prend de cookie. Chaque fois que le cookie pris, il a renouvelé - de sorte qu'il n'expire jamais pendant que l'utilisateur actif.
Sur le côté serveur, nous aurons AUTHFILTER qui vérifiera la présence d'en-tête d'autorisation dans chaque demande (exclure la connexion, l'inscription, le réinitialisation_password). Si aucun autre en-tête trouvé, le filtre renvoie la réponse au client avec le code d'état 401 (Affiche le client affiche l'écran de connexion à l'utilisateur). Si l'en-tête a trouvé le filtre vérifie d'abord la présence de l'utilisateur dans le cache, après dans DataStore et si l'utilisateur trouvé - ne fait rien (la demande traitée par la méthode appropriée), non trouvée - 401.
Au-dessus de l'architecture permet de garder le serveur apatride mais a toujours des sessions de déconnexion automatique.